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;};
|
mkPHPFPMService = callPackage ./php-fpm/unit.nix {inherit utils;};
|
||||||
|
|
||||||
mkKeycloakService = callPackage ./keycloak/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;};
|
mkKeycloakHaproxyService = callPackage ./keycloak-haproxy/unit.nix {inherit utils;};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
, roles ? {}
|
, roles ? {}
|
||||||
, clients ? {}
|
, clients ? {}
|
||||||
, users ? {}
|
, users ? {}
|
||||||
|
, groups ? []
|
||||||
}:
|
}:
|
||||||
|
|
||||||
with builtins;
|
with builtins;
|
||||||
|
@ -17,7 +18,7 @@ let
|
||||||
iscomposite = (length v) > 0;
|
iscomposite = (length v) > 0;
|
||||||
in {
|
in {
|
||||||
name = k;
|
name = k;
|
||||||
composite = if iscomposite then "true" else "false";
|
composite = if iscomposite then true else false;
|
||||||
} // optionalAttrs iscomposite {
|
} // optionalAttrs iscomposite {
|
||||||
composites = {
|
composites = {
|
||||||
realm = v;
|
realm = v;
|
||||||
|
@ -26,18 +27,24 @@ let
|
||||||
|
|
||||||
mkClientRole =
|
mkClientRole =
|
||||||
let
|
let
|
||||||
roles = config:
|
roles = config: config.roles or [];
|
||||||
if (hasAttr "roles" config)
|
|
||||||
then config.roles
|
|
||||||
else [];
|
|
||||||
|
|
||||||
c = v:
|
c = v:
|
||||||
{
|
{
|
||||||
name = v;
|
name = v;
|
||||||
clientRole = "true";
|
clientRole = true;
|
||||||
};
|
};
|
||||||
in k: config: map c (roles config);
|
in k: config: map c (roles config);
|
||||||
|
|
||||||
|
mkGroup = name: {
|
||||||
|
inherit name;
|
||||||
|
path = "/${name}";
|
||||||
|
attributes = {};
|
||||||
|
realmRoles = [];
|
||||||
|
clientRoles = {};
|
||||||
|
subGroups = [];
|
||||||
|
};
|
||||||
|
|
||||||
mkClient = k: config:
|
mkClient = k: config:
|
||||||
let
|
let
|
||||||
url = "https://${k}.${domain}";
|
url = "https://${k}.${domain}";
|
||||||
|
@ -46,21 +53,134 @@ let
|
||||||
clientId = k;
|
clientId = k;
|
||||||
rootUrl = url;
|
rootUrl = url;
|
||||||
clientAuthenticatorType = "client-secret";
|
clientAuthenticatorType = "client-secret";
|
||||||
redirectUris = ["${url}/*"];
|
redirectUris = ["${url}/oauth2/callback"];
|
||||||
webOrigins = [url];
|
webOrigins = [url];
|
||||||
authorizationServicesEnabled = "true";
|
authorizationServicesEnabled = true;
|
||||||
serviceAccountsEnabled = "true";
|
serviceAccountsEnabled = true;
|
||||||
protocol = "openid-connect";
|
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:
|
mkUser = k: config:
|
||||||
{
|
{
|
||||||
username = k;
|
username = k;
|
||||||
enabled = "true";
|
enabled = true;
|
||||||
|
|
||||||
inherit (config) email firstName lastName realmRoles;
|
inherit (config) email firstName lastName;
|
||||||
} // optionalAttrs (hasAttr "initialPassword" config && config.initialPassword) {
|
} // optionalAttrs (config ? "groups") {
|
||||||
|
inherit (config) groups;
|
||||||
|
} // optionalAttrs (config ? "roles") {
|
||||||
|
realmRoles = config.roles;
|
||||||
|
} // optionalAttrs (config ? "initialPassword") {
|
||||||
credentials = [
|
credentials = [
|
||||||
{
|
{
|
||||||
type = "password";
|
type = "password";
|
||||||
|
@ -74,7 +194,7 @@ in
|
||||||
{
|
{
|
||||||
inherit realm;
|
inherit realm;
|
||||||
id = realm;
|
id = realm;
|
||||||
enabled = "true";
|
enabled = true;
|
||||||
|
|
||||||
clients = mapAttrsToList mkClient clients;
|
clients = mapAttrsToList mkClient clients;
|
||||||
|
|
||||||
|
@ -83,5 +203,7 @@ in
|
||||||
client = mapAttrs mkClientRole clients;
|
client = mapAttrs mkClientRole clients;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
groups = map mkGroup groups;
|
||||||
|
|
||||||
users = mapAttrsToList mkUser users;
|
users = mapAttrsToList mkUser users;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
, postgresServiceName
|
, postgresServiceName
|
||||||
, initialAdminUsername ? null
|
, initialAdminUsername ? null
|
||||||
, keys
|
, keys
|
||||||
|
, listenPort ? 8080
|
||||||
|
|
||||||
, logLevel ? "INFO"
|
, logLevel ? "INFO"
|
||||||
, metricsEnabled ? false
|
, metricsEnabled ? false
|
||||||
|
@ -38,6 +39,7 @@ in
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
||||||
inherit initialAdminUsername;
|
inherit initialAdminUsername;
|
||||||
|
inherit hostname listenPort;
|
||||||
|
|
||||||
systemdUnitFile = "${name}.service";
|
systemdUnitFile = "${name}.service";
|
||||||
|
|
||||||
|
@ -59,6 +61,9 @@ in
|
||||||
|
|
||||||
# HTTP
|
# 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.
|
# The file path to a server certificate or certificate chain in PEM format.
|
||||||
#https-certificate-file=''${kc.home.dir}conf/server.crt.pem
|
#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 = {
|
keycloakCliConfig = {
|
||||||
clients = {
|
clients = {
|
||||||
ttrss = {
|
ttrss = {
|
||||||
roles = ["uma_protection"];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
}:
|
}:
|
||||||
{ serviceName ? "Vaultwarden"
|
{ serviceName ? "Vaultwarden"
|
||||||
, subdomain ? "vaultwarden"
|
, subdomain ? "vaultwarden"
|
||||||
, domain ? ""
|
|
||||||
, ingress ? 18005
|
, ingress ? 18005
|
||||||
, signupsAllowed ? false
|
, signupsAllowed ? false
|
||||||
, signupsVerify ? true
|
, signupsVerify ? true
|
||||||
|
@ -25,12 +24,15 @@
|
||||||
, sso ? {}
|
, sso ? {}
|
||||||
|
|
||||||
, distribution ? {}
|
, distribution ? {}
|
||||||
|
, KeycloakService ? null
|
||||||
|
, KeycloakCliService ? null
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
mkVaultwardenWeb = pkgs.callPackage ./web.nix {inherit utils;};
|
mkVaultwardenWeb = pkgs.callPackage ./web.nix {inherit utils;};
|
||||||
|
|
||||||
ssoIngress = if sso != {} then ingress else null;
|
ssoIngress = if sso != {} then ingress else null;
|
||||||
serviceIngress = if sso != {} then ingress+1 else ingress;
|
serviceIngress = if sso != {} then ingress+1 else ingress;
|
||||||
|
metricsPort = if sso != {} then ingress+2 else ingress+1;
|
||||||
|
|
||||||
smtpConfig = smtp;
|
smtpConfig = smtp;
|
||||||
in
|
in
|
||||||
|
@ -174,18 +176,43 @@ rec {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
oauth2Proxy = {
|
oauth2Proxy =
|
||||||
name = "${serviceName}Oauth2Proxy";
|
let
|
||||||
serviceName = subdomain;
|
name = "${serviceName}Oauth2Proxy";
|
||||||
inherit domain;
|
in customPkgs.mkOauth2Proxy {
|
||||||
cookieSecret = "${serviceName}_oauth2proxy_cookiesecret";
|
inherit name;
|
||||||
clientSecret = "${serviceName}_oauth2proxy_clientsecret";
|
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 = {
|
keycloakCliConfig = {
|
||||||
clients = {
|
clients = {
|
||||||
vaultwarden = {
|
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;
|
${db.name} = db;
|
||||||
${web.name} = web;
|
${web.name} = web;
|
||||||
${service.name} = service;
|
${service.name} = service;
|
||||||
|
${oauth2Proxy.name} = oauth2Proxy;
|
||||||
};
|
};
|
||||||
|
|
||||||
distribute = on: {
|
distribute = on: {
|
||||||
${db.name} = on;
|
${db.name} = on;
|
||||||
${web.name} = on;
|
${web.name} = on;
|
||||||
${service.name} = on;
|
${service.name} = on;
|
||||||
|
${oauth2Proxy.name} = on;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue