protect vaultwarden with oauth2proxy
This commit is contained in:
parent
a93a9cc7c5
commit
52af93898c
6 changed files with 333 additions and 23 deletions
|
@ -23,6 +23,7 @@ let
|
|||
mkPHPFPMService = callPackage ./php-fpm/unit.nix {inherit utils;};
|
||||
|
||||
mkKeycloakService = callPackage ./keycloak/unit.nix {inherit utils;};
|
||||
mkOauth2Proxy = callPackage ./oauth2-proxy/unit.nix {inherit utils;};
|
||||
|
||||
mkKeycloakHaproxyService = callPackage ./keycloak-haproxy/unit.nix {inherit utils;};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
, roles ? {}
|
||||
, clients ? {}
|
||||
, users ? {}
|
||||
, groups ? []
|
||||
}:
|
||||
|
||||
with builtins;
|
||||
|
@ -17,7 +18,7 @@ let
|
|||
iscomposite = (length v) > 0;
|
||||
in {
|
||||
name = k;
|
||||
composite = if iscomposite then "true" else "false";
|
||||
composite = if iscomposite then true else false;
|
||||
} // optionalAttrs iscomposite {
|
||||
composites = {
|
||||
realm = v;
|
||||
|
@ -26,18 +27,24 @@ let
|
|||
|
||||
mkClientRole =
|
||||
let
|
||||
roles = config:
|
||||
if (hasAttr "roles" config)
|
||||
then config.roles
|
||||
else [];
|
||||
roles = config: config.roles or [];
|
||||
|
||||
c = v:
|
||||
{
|
||||
name = v;
|
||||
clientRole = "true";
|
||||
clientRole = true;
|
||||
};
|
||||
in k: config: map c (roles config);
|
||||
|
||||
mkGroup = name: {
|
||||
inherit name;
|
||||
path = "/${name}";
|
||||
attributes = {};
|
||||
realmRoles = [];
|
||||
clientRoles = {};
|
||||
subGroups = [];
|
||||
};
|
||||
|
||||
mkClient = k: config:
|
||||
let
|
||||
url = "https://${k}.${domain}";
|
||||
|
@ -46,21 +53,134 @@ let
|
|||
clientId = k;
|
||||
rootUrl = url;
|
||||
clientAuthenticatorType = "client-secret";
|
||||
redirectUris = ["${url}/*"];
|
||||
redirectUris = ["${url}/oauth2/callback"];
|
||||
webOrigins = [url];
|
||||
authorizationServicesEnabled = "true";
|
||||
serviceAccountsEnabled = "true";
|
||||
authorizationServicesEnabled = true;
|
||||
serviceAccountsEnabled = true;
|
||||
protocol = "openid-connect";
|
||||
publicClient = "false";
|
||||
publicClient = false;
|
||||
protocolMappers = [
|
||||
{
|
||||
name = "Client ID";
|
||||
protocol = "openid-connect";
|
||||
protocolMapper = "oidc-usersessionmodel-note-mapper";
|
||||
consentRequired = false;
|
||||
config = {
|
||||
"user.session.note" = "clientId";
|
||||
"id.token.claim" = "true";
|
||||
"access.token.claim" = "true";
|
||||
"claim.name" = "clientId";
|
||||
"jsonType.label" = "String";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "Client Host";
|
||||
protocol = "openid-connect";
|
||||
protocolMapper = "oidc-usersessionmodel-note-mapper";
|
||||
consentRequired = false;
|
||||
config = {
|
||||
"user.session.note" = "clientHost";
|
||||
"id.token.claim" = "true";
|
||||
"access.token.claim" = "true";
|
||||
"claim.name" = "clientHost";
|
||||
"jsonType.label" = "String";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "Client IP Address";
|
||||
protocol = "openid-connect";
|
||||
protocolMapper = "oidc-usersessionmodel-note-mapper";
|
||||
consentRequired = false;
|
||||
config = {
|
||||
"user.session.note" = "clientAddress";
|
||||
"id.token.claim" = "true";
|
||||
"access.token.claim" = "true";
|
||||
"claim.name" = "clientAddress";
|
||||
"jsonType.label" = "String";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "Audience";
|
||||
protocol = "openid-connect";
|
||||
protocolMapper = "oidc-audience-mapper";
|
||||
config = {
|
||||
"included.client.audience" = k;
|
||||
"id.token.claim" = "false";
|
||||
"access.token.claim" = "true";
|
||||
"included.custom.audience" = k;
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "Group";
|
||||
protocol = "openid-connect";
|
||||
protocolMapper = "oidc-group-membership-mapper";
|
||||
config = {
|
||||
"full.path" = "true";
|
||||
"id.token.claim" = "true";
|
||||
"access.token.claim" = "true";
|
||||
"claim.name" = "groups";
|
||||
"userinfo.token.claim" = "true";
|
||||
};
|
||||
}
|
||||
];
|
||||
authorizationSettings = {
|
||||
policyEnforcementMode = "ENFORCING";
|
||||
|
||||
resources =
|
||||
let
|
||||
mkResource = name: uris: {
|
||||
inherit name;
|
||||
type = "urn:${k}:resources:${name}";
|
||||
ownerManagedAccess = false;
|
||||
inherit uris;
|
||||
};
|
||||
in
|
||||
mapAttrsToList mkResource (config.resourcesUris or {});
|
||||
|
||||
policies =
|
||||
let
|
||||
mkPolicyRole = role: {
|
||||
id = role;
|
||||
required = true;
|
||||
};
|
||||
|
||||
mkPolicy = name: roles: {
|
||||
name = "${concatStringsSep "," roles} has access";
|
||||
type = "role";
|
||||
logic = "POSITIVE";
|
||||
decisionStrategy = "UNANIMOUS";
|
||||
config = {
|
||||
roles = toJSON (map mkPolicyRole roles);
|
||||
};
|
||||
};
|
||||
|
||||
mkPermission = name: roles: resources: {
|
||||
name = "${concatStringsSep "," roles} has access to ${concatStringsSep "," resources}";
|
||||
type = "resource";
|
||||
logic = "POSITIVE";
|
||||
decisionStrategy = "UNANIMOUS";
|
||||
config = {
|
||||
resources = toJSON resources;
|
||||
applyPolicies = toJSON (map (r: "${concatStringsSep "," roles} has access") roles);
|
||||
};
|
||||
};
|
||||
in
|
||||
(mapAttrsToList (name: {roles, ...}: mkPolicy name roles) (config.access or {}))
|
||||
++ (mapAttrsToList (name: {roles, resources}: mkPermission name roles resources) (config.access or {}));
|
||||
};
|
||||
};
|
||||
|
||||
mkUser = k: config:
|
||||
{
|
||||
username = k;
|
||||
enabled = "true";
|
||||
enabled = true;
|
||||
|
||||
inherit (config) email firstName lastName realmRoles;
|
||||
} // optionalAttrs (hasAttr "initialPassword" config && config.initialPassword) {
|
||||
inherit (config) email firstName lastName;
|
||||
} // optionalAttrs (config ? "groups") {
|
||||
inherit (config) groups;
|
||||
} // optionalAttrs (config ? "roles") {
|
||||
realmRoles = config.roles;
|
||||
} // optionalAttrs (config ? "initialPassword") {
|
||||
credentials = [
|
||||
{
|
||||
type = "password";
|
||||
|
@ -74,7 +194,7 @@ in
|
|||
{
|
||||
inherit realm;
|
||||
id = realm;
|
||||
enabled = "true";
|
||||
enabled = true;
|
||||
|
||||
clients = mapAttrsToList mkClient clients;
|
||||
|
||||
|
@ -83,5 +203,7 @@ in
|
|||
client = mapAttrs mkClientRole clients;
|
||||
};
|
||||
|
||||
groups = map mkGroup groups;
|
||||
|
||||
users = mapAttrsToList mkUser users;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
, postgresServiceName
|
||||
, initialAdminUsername ? null
|
||||
, keys
|
||||
, listenPort ? 8080
|
||||
|
||||
, logLevel ? "INFO"
|
||||
, metricsEnabled ? false
|
||||
|
@ -38,6 +39,7 @@ in
|
|||
inherit name;
|
||||
|
||||
inherit initialAdminUsername;
|
||||
inherit hostname listenPort;
|
||||
|
||||
systemdUnitFile = "${name}.service";
|
||||
|
||||
|
@ -59,6 +61,9 @@ in
|
|||
|
||||
# HTTP
|
||||
|
||||
http-host=127.0.0.1
|
||||
http-port=${builtins.toString listenPort}
|
||||
|
||||
# The file path to a server certificate or certificate chain in PEM format.
|
||||
#https-certificate-file=''${kc.home.dir}conf/server.crt.pem
|
||||
|
||||
|
|
154
oauth2-proxy/unit.nix
Normal file
154
oauth2-proxy/unit.nix
Normal file
|
@ -0,0 +1,154 @@
|
|||
{ stdenv
|
||||
, pkgs
|
||||
, utils
|
||||
}:
|
||||
{ name
|
||||
, serviceName
|
||||
, keycloakSubdomain ? "keycloak"
|
||||
, domain
|
||||
, realm
|
||||
, allowed_roles ? []
|
||||
|
||||
, ingress
|
||||
, egress
|
||||
, metricsPort
|
||||
, keys
|
||||
|
||||
, distribution
|
||||
, KeycloakService
|
||||
, KeycloakCliService
|
||||
|
||||
, debug ? true
|
||||
}:
|
||||
|
||||
with builtins;
|
||||
with pkgs.lib.lists;
|
||||
with pkgs.lib.strings;
|
||||
rec {
|
||||
inherit name;
|
||||
|
||||
pkg =
|
||||
{ KeycloakService
|
||||
, KeycloakCliService
|
||||
}:
|
||||
let
|
||||
formatted_allowed_roles = builtins.toJSON (concatStringsSep ", " allowed_roles);
|
||||
|
||||
config = pkgs.writeText "${serviceName}.cfg" (''
|
||||
provider = "keycloak-oidc"
|
||||
provider_display_name="Keycloak"
|
||||
http_address = "${ingress}"
|
||||
upstreams = [ "${concatStringsSep " " egress}" ]
|
||||
metrics_address = "127.0.0.1:${toString metricsPort}"
|
||||
|
||||
client_id = "${serviceName}"
|
||||
scope="openid"
|
||||
|
||||
redirect_url = "https://${serviceName}.${domain}/oauth2/callback"
|
||||
oidc_issuer_url = "https://${keycloakSubdomain}.${domain}/realms/${realm}"
|
||||
|
||||
email_domains = [ "*" ]
|
||||
allowed_roles = ${formatted_allowed_roles}
|
||||
# skip_auth_routes = [ "^/api" ]
|
||||
|
||||
reverse_proxy = "true"
|
||||
# trusted_ips = "@"
|
||||
|
||||
skip_provider_button = "true"
|
||||
|
||||
pass_authorization_header = true
|
||||
pass_access_token = true
|
||||
pass_user_headers = true
|
||||
set_authorization_header = true
|
||||
set_xauthrequest = true
|
||||
'' + (if !debug then "" else ''
|
||||
auth_logging = "true"
|
||||
request_logging = "true"
|
||||
''));
|
||||
|
||||
exec = pkgs.writeShellApplication {
|
||||
name = "oauth2proxy-wrapper";
|
||||
runtimeInputs = with pkgs; [curl coreutils];
|
||||
text = ''
|
||||
while ! curl --silent ${KeycloakService.hostname}:${builtins.toString KeycloakService.listenPort} > /dev/null; do
|
||||
echo "Waiting for port ${builtins.toString KeycloakService.listenPort} to open..."
|
||||
sleep 10
|
||||
done
|
||||
sleep 2
|
||||
'';
|
||||
};
|
||||
|
||||
oauth2-proxy =
|
||||
let
|
||||
version = "f93166229fe9b57f7d54fb0a9c42939f3f30340f";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "ibizaman";
|
||||
repo = "oauth2-proxy";
|
||||
rev = version;
|
||||
sha256 = "sha256-RI34N+YmUqAanuJOGUA+rUTS1TpUoy8rw6EFGeLh5L0=";
|
||||
# sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
in
|
||||
(pkgs.callPackage "${pkgs.path}/pkgs/tools/backup/kopia" {
|
||||
buildGoModule = args: pkgs.buildGo118Module (args // {
|
||||
vendorSha256 = "sha256-2WUd2RxeOal0lpp/TuGSyfP1ppvG/Vd3bgsSsNO8ejo=";
|
||||
inherit src version;
|
||||
});
|
||||
});
|
||||
|
||||
oauth2proxyBin = "${oauth2-proxy}/bin/oauth2-proxy";
|
||||
in utils.systemd.mkService rec {
|
||||
name = "oauth2proxy-${serviceName}";
|
||||
|
||||
content = ''
|
||||
[Unit]
|
||||
Description=Oauth2 proxy for ${serviceName}
|
||||
After=${KeycloakService.systemdUnitFile}
|
||||
Wants=${KeycloakService.systemdUnitFile}
|
||||
After=${utils.keyServiceDependencies keys}
|
||||
Wants=${utils.keyServiceDependencies keys}
|
||||
|
||||
[Service]
|
||||
ExecStartPre=${exec}/bin/oauth2proxy-wrapper
|
||||
TimeoutStartSec=8m
|
||||
ExecStart=${oauth2proxyBin} --config ${config}
|
||||
DynamicUser=true
|
||||
RuntimeDirectory=oauth2proxy-${serviceName}
|
||||
${utils.keyEnvironmentFiles keys}
|
||||
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
PrivateUsers=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectHostname=yes
|
||||
ProtectClock=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RemoveIPC=yes
|
||||
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
'';
|
||||
};
|
||||
|
||||
dependsOn = {
|
||||
inherit KeycloakService KeycloakCliService;
|
||||
};
|
||||
type = "systemd-unit";
|
||||
}
|
|
@ -180,7 +180,6 @@ rec {
|
|||
keycloakCliConfig = {
|
||||
clients = {
|
||||
ttrss = {
|
||||
roles = ["uma_protection"];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
}:
|
||||
{ serviceName ? "Vaultwarden"
|
||||
, subdomain ? "vaultwarden"
|
||||
, domain ? ""
|
||||
, ingress ? 18005
|
||||
, signupsAllowed ? false
|
||||
, signupsVerify ? true
|
||||
|
@ -25,12 +24,15 @@
|
|||
, sso ? {}
|
||||
|
||||
, distribution ? {}
|
||||
, KeycloakService ? null
|
||||
, KeycloakCliService ? null
|
||||
}:
|
||||
let
|
||||
mkVaultwardenWeb = pkgs.callPackage ./web.nix {inherit utils;};
|
||||
|
||||
ssoIngress = if sso != {} then ingress else null;
|
||||
serviceIngress = if sso != {} then ingress+1 else ingress;
|
||||
metricsPort = if sso != {} then ingress+2 else ingress+1;
|
||||
|
||||
smtpConfig = smtp;
|
||||
in
|
||||
|
@ -174,18 +176,43 @@ rec {
|
|||
};
|
||||
};
|
||||
|
||||
oauth2Proxy = {
|
||||
name = "${serviceName}Oauth2Proxy";
|
||||
serviceName = subdomain;
|
||||
inherit domain;
|
||||
cookieSecret = "${serviceName}_oauth2proxy_cookiesecret";
|
||||
clientSecret = "${serviceName}_oauth2proxy_clientsecret";
|
||||
oauth2Proxy =
|
||||
let
|
||||
name = "${serviceName}Oauth2Proxy";
|
||||
in customPkgs.mkOauth2Proxy {
|
||||
inherit name;
|
||||
serviceName = subdomain;
|
||||
domain = utils.getDomain distribution name;
|
||||
ingress = "127.0.0.1:${toString ssoIngress}";
|
||||
egress = [ "http://127.0.0.1:${toString serviceIngress}" ];
|
||||
realm = sso.realm;
|
||||
allowed_roles = [ "user" "/admin|admin" ];
|
||||
inherit metricsPort;
|
||||
keys = {
|
||||
cookieSecret = "${serviceName}_oauth2proxy_cookiesecret";
|
||||
clientSecret = "${serviceName}_oauth2proxy_clientsecret";
|
||||
};
|
||||
|
||||
inherit distribution KeycloakService KeycloakCliService;
|
||||
};
|
||||
|
||||
keycloakCliConfig = {
|
||||
clients = {
|
||||
vaultwarden = {
|
||||
roles = ["uma_protection"];
|
||||
resourcesUris = {
|
||||
adminPath = ["/admin/*"];
|
||||
userPath = ["/*"];
|
||||
};
|
||||
access = {
|
||||
admin = {
|
||||
roles = [ "admin" ];
|
||||
resources = [ "adminPath" ];
|
||||
};
|
||||
user = {
|
||||
roles = [ "user" ];
|
||||
resources = [ "userPath" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -210,11 +237,13 @@ rec {
|
|||
${db.name} = db;
|
||||
${web.name} = web;
|
||||
${service.name} = service;
|
||||
${oauth2Proxy.name} = oauth2Proxy;
|
||||
};
|
||||
|
||||
distribute = on: {
|
||||
${db.name} = on;
|
||||
${web.name} = on;
|
||||
${service.name} = on;
|
||||
${oauth2Proxy.name} = on;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue