From 52af93898c0da7746bf4af39caaf2938f8a91efb Mon Sep 17 00:00:00 2001 From: ibizaman Date: Sat, 11 Feb 2023 21:28:00 -0800 Subject: [PATCH] protect vaultwarden with oauth2proxy --- all-packages.nix | 1 + keycloak-cli-config/configcreator.nix | 150 ++++++++++++++++++++++--- keycloak/unit.nix | 5 + oauth2-proxy/unit.nix | 154 ++++++++++++++++++++++++++ ttrss/default.nix | 1 - vaultwarden/default.nix | 45 ++++++-- 6 files changed, 333 insertions(+), 23 deletions(-) create mode 100644 oauth2-proxy/unit.nix diff --git a/all-packages.nix b/all-packages.nix index 82818e9..ccd35d5 100644 --- a/all-packages.nix +++ b/all-packages.nix @@ -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;}; diff --git a/keycloak-cli-config/configcreator.nix b/keycloak-cli-config/configcreator.nix index f8e95ff..27395d2 100644 --- a/keycloak-cli-config/configcreator.nix +++ b/keycloak-cli-config/configcreator.nix @@ -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; } diff --git a/keycloak/unit.nix b/keycloak/unit.nix index af95bb9..8e8829e 100644 --- a/keycloak/unit.nix +++ b/keycloak/unit.nix @@ -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 diff --git a/oauth2-proxy/unit.nix b/oauth2-proxy/unit.nix new file mode 100644 index 0000000..1c0f98d --- /dev/null +++ b/oauth2-proxy/unit.nix @@ -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"; +} diff --git a/ttrss/default.nix b/ttrss/default.nix index 9b9b411..148bcd1 100644 --- a/ttrss/default.nix +++ b/ttrss/default.nix @@ -180,7 +180,6 @@ rec { keycloakCliConfig = { clients = { ttrss = { - roles = ["uma_protection"]; }; }; }; diff --git a/vaultwarden/default.nix b/vaultwarden/default.nix index 3df6271..5401a7b 100644 --- a/vaultwarden/default.nix +++ b/vaultwarden/default.nix @@ -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; }; }