diff --git a/hosts/elisabeth/guests.nix b/hosts/elisabeth/guests.nix index ea1a6cd..e404110 100644 --- a/hosts/elisabeth/guests.nix +++ b/hosts/elisabeth/guests.nix @@ -8,7 +8,7 @@ ... }: let adguardhomedomain = "adguardhome.${config.secrets.secrets.global.domains.web}"; - giteadomain = "git.${config.secrets.secrets.global.domains.web}"; + forgejoDomain = "git.${config.secrets.secrets.global.domains.web}"; immichdomain = "immich.${config.secrets.secrets.global.domains.web}"; nextclouddomain = "nc.${config.secrets.secrets.global.domains.web}"; ollamadomain = "ollama.${config.secrets.secrets.global.domains.web}"; @@ -44,19 +44,19 @@ in { ''; }; - upstreams.gitea = { - servers."${ipOf "gitea"}:3000" = {}; + upstreams.forgejo = { + servers."${ipOf "forgejo"}:3000" = {}; extraConfig = '' - zone gitea 64k ; + zone forgejo 64k ; keepalive 5 ; ''; }; - virtualHosts.${giteadomain} = { + virtualHosts.${forgejoDomain} = { forceSSL = true; useACMEHost = "web"; locations."/" = { - proxyPass = "http://gitea"; + proxyPass = "http://forgejo"; proxyWebsockets = true; }; extraConfig = '' @@ -326,7 +326,7 @@ in { // mkContainer "paperless" { enableSharedPaperless = true; } - // mkContainer "gitea" { + // mkContainer "forgejo" { enablePanzer = true; } // mkMicrovm "immich" { diff --git a/hosts/elisabeth/secrets/gitea/gitea-passwd.age b/hosts/elisabeth/secrets/forgejo/forgejo-passwd.age similarity index 100% rename from hosts/elisabeth/secrets/gitea/gitea-passwd.age rename to hosts/elisabeth/secrets/forgejo/forgejo-passwd.age diff --git a/hosts/elisabeth/secrets/gitea/generated/forgejoHetznerSsh.age b/hosts/elisabeth/secrets/forgejo/generated/forgejoHetznerSsh.age similarity index 100% rename from hosts/elisabeth/secrets/gitea/generated/forgejoHetznerSsh.age rename to hosts/elisabeth/secrets/forgejo/generated/forgejoHetznerSsh.age diff --git a/hosts/elisabeth/secrets/gitea/generated/resticpasswd.age b/hosts/elisabeth/secrets/forgejo/generated/resticpasswd.age similarity index 100% rename from hosts/elisabeth/secrets/gitea/generated/resticpasswd.age rename to hosts/elisabeth/secrets/forgejo/generated/resticpasswd.age diff --git a/hosts/elisabeth/secrets/gitea/host.pub b/hosts/elisabeth/secrets/forgejo/host.pub similarity index 100% rename from hosts/elisabeth/secrets/gitea/host.pub rename to hosts/elisabeth/secrets/forgejo/host.pub diff --git a/hosts/elisabeth/secrets/forgejo/secrets.nix.age b/hosts/elisabeth/secrets/forgejo/secrets.nix.age new file mode 100644 index 0000000..10ff27b --- /dev/null +++ b/hosts/elisabeth/secrets/forgejo/secrets.nix.age @@ -0,0 +1,15 @@ +age-encryption.org/v1 +-> X25519 gc7a2OUDl5ZLAWsWvg+Vq8OjjQ8gybzm6FFqUWCelWE +BMYy6ekQeRNwNyUMZeSYH2ff5+1dfy+2A885JR5XRXo +-> piv-p256 XTQkUA A0N6GlqsddLw7SHSBKQ8XdYJD/q42y9DY7A4DnAq8cHQ +syaYAv5cXNcXZRs+Nzwj14Eai0EHGjtu3o3IBt1dg5k +-> piv-p256 ZFgiIw A7zvUk0WKnNZG8fHeTTOqNAR7Gf1SVvEVINMOGH6fetZ +6R2zFSXZXDFRhGg8+AFBQvTKWbDPZIYgkDY19cxmqi4 +-> piv-p256 5vmPtQ AoQwhdsUwjX8Dn63egOz92T+XNt5/MMxsL5T+7o1Jrvz +TnN51QUPA1wcHBCR6KYT0xFH5WVT+FAI/QhMTW3ajGE +-> piv-p256 ZFgiIw Azh9/Kn00w3JVQvlarDwwrXRnOGeTHYHtEF2gK6i2qW+ +gIp88ToCDc80WlJikNyKw8up+GWWexzMoaQVuLBRIfQ +-> Zr~B*Za-grease +20yMZ5Gv9nTK1qQVWe7lSAspqwdpUGVnX4r8M8UNvaQGNbSDFghj +--- BleVfNvY9wSlTcM42gt7rKRxmDG4LUlWHHi3vWgz6Og +8#e`\|5 &\0 #HZrnFb<"mSPbLfVs.?n#\0w/.h}wS"E*ֶ<>S>׿hun)맃ERpBG#!n4CKV? \ No newline at end of file diff --git a/hosts/elisabeth/secrets/gitea/generated/openid-secret.age b/hosts/elisabeth/secrets/gitea/generated/openid-secret.age deleted file mode 100644 index 033ff86..0000000 --- a/hosts/elisabeth/secrets/gitea/generated/openid-secret.age +++ /dev/null @@ -1,16 +0,0 @@ -age-encryption.org/v1 --> X25519 nbSYyNcboZYI9JVKkImhppQToS3XQxjiriBS0TAQrE4 -/g8LhlSY1YxQQo47FuzzXNPTVsnBgC+TRh9BBq3WVLI --> piv-p256 XTQkUA AyItco2APIdcG+okYszuK5aqGOlkfD7at+sG53awGFAQ -KTD5Nr2u6U6oVEYDNDLO8U3zVMLWypHHS0Y+9Q36TGM --> piv-p256 ZFgiIw AhZRrPWodlNzQAMgFZDOiamtdsWE8vtjGXNpbGsCLnpY -MmHIE4jSpYZFseJpG2Tdj+CnqjIkk/dSUoOJaRoiWe0 --> piv-p256 5vmPtQ AvgHDtaekIaAbj7gmUJP0xWwOpESKSgQAUd4voq2ee9c -XIFV78uWrOgJrQ7cpYmIrMsGpkji1GqvSuAIKRq+Tt0 --> piv-p256 ZFgiIw ApCcEoQQOcikT1hANSjOmzLiL6tNhk2vJ96HDNtJuMpd -geXW5whCUN4Hr5zvko5B94aHhirsmUXpoMBGKrZtZGA --> '@Uo0`]-grease 5:un}:j -8NEXuaIP6w ---- jQD1YtAJ15d+eIy6nPqHHUMTLJtgAg/+5sqQbw0Q0Ko -sσ=]%ʹLlieM\YCo-s4M -$\w,Mgt4 #Vp \ No newline at end of file diff --git a/hosts/elisabeth/secrets/gitea/secrets.nix.age b/hosts/elisabeth/secrets/gitea/secrets.nix.age deleted file mode 100644 index 6ba5f85..0000000 Binary files a/hosts/elisabeth/secrets/gitea/secrets.nix.age and /dev/null differ diff --git a/hosts/elisabeth/secrets/kanidm/generated/oauth2-forgejo.age b/hosts/elisabeth/secrets/kanidm/generated/oauth2-forgejo.age new file mode 100644 index 0000000..eb731c2 --- /dev/null +++ b/hosts/elisabeth/secrets/kanidm/generated/oauth2-forgejo.age @@ -0,0 +1,16 @@ +age-encryption.org/v1 +-> X25519 vc309RP8AQvszDq+D1f4f5dWrqOpjt2lKmci9507CUQ +TKV60m9WQendmKOKVJuM6JvvQPb7dsAO8+Tn3NNjdt4 +-> piv-p256 XTQkUA A/zSSbrKLxl31vDGaKfSTqJfX/I0ESYjApL1FIcwbRT/ +f7tAcOo7eDCACUjO++5Ca8UYsEMyfEosMEhoRyZlv2s +-> piv-p256 ZFgiIw AveXSkbOt33wUXtCc1IDmssYIJfUOSVm/zamehQDBaN6 +Cq7pLQL5ED3lSZrive0vyoipe1PN4FpbNAKDYIHWE1U +-> piv-p256 5vmPtQ Atez78iMxWLAq114XO7wwU1JiSTU50wfohvJ0LQUL5JI +oHv2dlB91XwPpL8OXFZyCE4CEGe7H+OKkef3m178a/w +-> piv-p256 ZFgiIw AjTxVdEB+Wck0N07i2nvkWtzH13Vfznl7TqS6kqwvGet +gN2xJG0OOfqtimsf9qwoYk8fB4A5GQILt6kWScZ+ono +-> WR-grease ]x3SA4 w6 7\t D2b +4QgiTLjrIaQBKUc3cNs0+Hyqo9ccxdAstepUX1Ps4HLqGBNrIUkM2Q +--- CylSm6+Ki9f+ilXS7UTvDxDlLNLLvRaJNOw9kHt6TVk +(Fg*7"[̸fH}Ʋc*lJRc}5 l +}"S \ No newline at end of file diff --git a/modules/config/users.nix b/modules/config/users.nix index e78d6a5..0b07794 100644 --- a/modules/config/users.nix +++ b/modules/config/users.nix @@ -21,7 +21,7 @@ nextcloud = uidGid 213; redis-nextcloud = uidGid 214; radicale = uidGid 215; - gitea = uidGid 215; + forgejo = uidGid 215; vaultwarden = uidGid 215; redis-paperless = uidGid 216; microvm = uidGid 217; diff --git a/modules/kanidm.nix b/modules/kanidm.nix new file mode 100644 index 0000000..96d7384 --- /dev/null +++ b/modules/kanidm.nix @@ -0,0 +1,854 @@ +{ + config, + lib, + options, + pkgs, + ... +}: let + inherit + (lib) + any + attrNames + attrValues + concatLines + concatLists + converge + filter + filterAttrs + filterAttrsRecursive + flip + foldl' + getExe + hasInfix + hasPrefix + isStorePath + last + mapAttrsToList + mdDoc + mkEnableOption + mkForce + mkIf + mkMerge + mkOption + mkPackageOption + optional + optionalString + splitString + subtractLists + types + unique + ; + + cfg = config.services.kanidm; + settingsFormat = pkgs.formats.toml {}; + # Remove null values, so we can document optional values that don't end up in the generated TOML file. + filterConfig = converge (filterAttrsRecursive (_: v: v != null)); + serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); + clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); + unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); + certPaths = builtins.map builtins.dirOf [cfg.serverSettings.tls_chain cfg.serverSettings.tls_key]; + + # Merge bind mount paths and remove paths where a prefix is already mounted. + # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount + # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. + hasPrefixInList = list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; + mergePaths = foldl' (merged: newPath: let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; + in + filteredPaths ++ filteredNew) []; + + defaultServiceConfig = { + BindReadOnlyPaths = [ + "/nix/store" + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + ]; + CapabilityBoundingSet = []; + # ProtectClock= adds DeviceAllow=char-rtc r + DeviceAllow = ""; + # Implies ProtectSystem=strict, which re-mounts all paths + # DynamicUser = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateNetwork = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + # Would re-mount paths ignored by temporary root + #ProtectSystem = "strict"; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = []; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = ["@system-service" "~@privileged @resources @setuid @keyring"]; + # Does not work well with the temporary root + #UMask = "0066"; + }; + + mkPresentOption = what: + mkOption { + description = mdDoc "Whether to ensure that this ${what} is present or absent."; + type = types.bool; + default = true; + }; + + filterPresent = filterAttrs (_: v: v.present); + + provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON { + inherit (cfg.provision) groups persons systems; + }); + + serverPort = + # ipv6: + if hasInfix "]:" cfg.serverSettings.bindaddress + then last (splitString "]:" cfg.serverSettings.bindaddress) + else + # ipv4: + if hasInfix "." cfg.serverSettings.bindaddress + then last (splitString ":" cfg.serverSettings.bindaddress) + # default is 8443 + else "8443"; + + # Only recover the admin account if a password should explicitly be provisioned + # for the account. Otherwise it is not needed for provisioning. + maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) '' + KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile}) + # We always reset the admin account password if a desired password was specified. + if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then + echo "Failed to recover admin account" >&2 + exit 1 + fi + ''; + + # Recover the idm_admin account. If a password should explicitly be provisioned + # for the account we set it, otherwise we generate a new one because it is required + # for provisioning. + recoverIdmAdmin = + if cfg.provision.idmAdminPasswordFile != null + then '' + KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile}) + # We always reset the idm_admin account password if a desired password was specified. + if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then + echo "Failed to recover idm_admin account" >&2 + exit 1 + fi + '' + else '' + # Recover idm_admin account + if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then + echo "$recover_out" >&2 + echo "kanidm provision: Failed to recover admin account" >&2 + exit 1 + fi + if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then + echo "$recover_out" >&2 + echo "kanidm provision: Failed to parse password for idm_admin account" >&2 + exit 1 + fi + ''; + + postStartScript = pkgs.writeShellScript "post-start" '' + set -euo pipefail + + # Wait for the kanidm server to come online + count=0 + while ! test -e /run/kanidmd/sock; do + sleep 0.1 + if [[ "$count" -eq 600 ]]; then + echo "Tried for 60 seconds, giving up..." + exit 1 + fi + if [[ ! -d "/proc/$MAINPID" ]]; then + echo "Main server died, giving up..." + exit 1 + fi + count=$((count++)) + done + + ${recoverIdmAdmin} + ${maybeRecoverAdmin} + + KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \ + ${getExe pkgs.kanidm-provision} --url "${cfg.provision.instanceUrl}" --state ${provisionStateJson} ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} + ''; +in { + options.services.kanidm = { + enableClient = mkEnableOption (mdDoc "the Kanidm client"); + enableServer = mkEnableOption (mdDoc "the Kanidm server"); + enablePam = mkEnableOption (mdDoc "the Kanidm PAM and NSS integration"); + + package = mkPackageOption pkgs "kanidm" {}; + + serverSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options = { + bindaddress = mkOption { + description = mdDoc "Address/port combination the webserver binds to."; + example = "[::1]:8443"; + type = types.str; + }; + # Should be optional but toml does not accept null + ldapbindaddress = mkOption { + description = mdDoc '' + Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. + ''; + example = "[::1]:636"; + default = null; + type = types.nullOr types.str; + }; + origin = mkOption { + description = mdDoc "The origin of your Kanidm instance. Must have https as protocol."; + example = "https://idm.example.org"; + type = types.strMatching "^https://.*"; + }; + domain = mkOption { + description = mdDoc '' + The `domain` that Kanidm manages. Must be below or equal to the domain + specified in `serverSettings.origin`. + This can be left at `null`, only if your instance has the role `ReadOnlyReplica`. + While it is possible to change the domain later on, it requires extra steps! + Please consider the warnings and execute the steps described + [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain). + ''; + example = "example.org"; + default = null; + type = types.nullOr types.str; + }; + db_path = mkOption { + description = mdDoc "Path to Kanidm database."; + default = "/var/lib/kanidm/kanidm.db"; + readOnly = true; + type = types.path; + }; + tls_chain = mkOption { + description = mdDoc "TLS chain in pem format."; + type = types.path; + }; + tls_key = mkOption { + description = mdDoc "TLS key in pem format."; + type = types.path; + }; + log_level = mkOption { + description = mdDoc "Log level of the server."; + default = "info"; + type = types.enum ["info" "debug" "trace"]; + }; + role = mkOption { + description = mdDoc "The role of this server. This affects the replication relationship and thereby available features."; + default = "WriteReplica"; + type = types.enum ["WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica"]; + }; + online_backup = { + path = mkOption { + description = mdDoc "Path to the output directory for backups."; + type = types.path; + default = "/var/lib/kanidm/backups"; + }; + schedule = mkOption { + description = mdDoc "The schedule for backups in cron format."; + type = types.str; + default = "00 22 * * *"; + }; + versions = mkOption { + description = mdDoc '' + Number of backups to keep. + + The default is set to `0`, in order to disable backups by default. + ''; + type = types.ints.unsigned; + default = 0; + example = 7; + }; + }; + }; + }; + default = {}; + description = mdDoc '' + Settings for Kanidm, see + [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml) + for possible values. + ''; + }; + + clientSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options.uri = mkOption { + description = mdDoc "Address of the Kanidm server."; + example = "http://127.0.0.1:8080"; + type = types.str; + }; + }; + description = mdDoc '' + Configure Kanidm clients, needed for the PAM daemon. See + [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config) + for possible values. + ''; + }; + + unixSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + options = { + pam_allowed_login_groups = mkOption { + description = mdDoc "Kanidm groups that are allowed to login using PAM."; + example = "my_pam_group"; + type = types.listOf types.str; + }; + hsm_pin_path = mkOption { + description = mdDoc "Path to a HSM pin."; + default = "/var/cache/kanidm-unixd/hsm-pin"; + type = types.path; + }; + }; + }; + description = mdDoc '' + Configure Kanidm unix daemon. + See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon) + and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd) + for possible values. + ''; + }; + + provision = { + enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers"; + + instanceUrl = mkOption { + description = "The instance url to which the provisioning tool should connect."; + default = "https://localhost:${serverPort}"; + defaultText = ''"https://localhost:"''; + type = types.str; + }; + + acceptInvalidCerts = mkOption { + description = '' + Whether to allow invalid certificates when provisioning the target instance. + By default this is only allowed when the instanceUrl is localhost. This is + dangerous when used with an external URL. + ''; + type = types.bool; + default = hasPrefix "https://localhost:" cfg.provision.instanceUrl; + defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl''; + }; + + adminPasswordFile = mkOption { + description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!"; + example = "/run/secrets/kanidm-admin-password"; + default = null; + type = types.nullOr types.path; + }; + + idmAdminPasswordFile = mkOption { + description = '' + Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here! + If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart. + ''; + example = "/run/secrets/kanidm-idm-admin-password"; + default = null; + type = types.nullOr types.path; + }; + + autoRemove = mkOption { + description = '' + Determines whether deleting an entity in this provisioning config should automatically + cause them to be removed from kanidm, too. This works because the provisioning tool tracks + all entities it has ever created. If this is set to false, you need to explicitly specify + `present = false` to delete an entity. + ''; + type = types.bool; + default = true; + }; + + groups = mkOption { + description = "Provisioning of kanidm groups"; + default = {}; + type = types.attrsOf (types.submodule (groupSubmod: { + options = { + present = mkPresentOption "group"; + + members = mkOption { + description = "List of kanidm entities (persons, groups, ...) which are part of this group."; + type = types.listOf types.str; + apply = unique; + default = []; + }; + }; + config.members = concatLists (flip mapAttrsToList cfg.provision.persons ( + person: personCfg: + optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person + )); + })); + }; + + persons = mkOption { + description = "Provisioning of kanidm persons"; + default = {}; + type = types.attrsOf (types.submodule { + options = { + present = mkPresentOption "person"; + + displayName = mkOption { + description = "Display name"; + type = types.str; + example = "My User"; + }; + + legalName = mkOption { + description = "Full legal name"; + type = types.nullOr types.str; + example = "Jane Doe"; + default = null; + }; + + mailAddresses = mkOption { + description = "Mail addresses. First given address is considered the primary address."; + type = types.listOf types.str; + example = ["jane.doe@example.com"]; + default = []; + }; + + groups = mkOption { + description = "List of groups this person should belong to."; + type = types.listOf types.str; + apply = unique; + default = []; + }; + }; + }); + }; + + systems.oauth2 = mkOption { + description = "Provisioning of oauth2 resource servers"; + default = {}; + type = types.attrsOf (types.submodule { + options = { + present = mkPresentOption "oauth2 resource server"; + + displayName = mkOption { + description = "Display name"; + type = types.str; + example = "Some Service"; + }; + + originUrl = mkOption { + description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash."; + type = types.strMatching ".*://.*/$"; + example = "https://someservice.example.com/"; + }; + + originLanding = mkOption { + description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions."; + type = types.nullOr types.str; + default = null; + example = "https://someservice.example.com/home"; + }; + + basicSecretFile = mkOption { + description = '' + The basic secret to use for this service. If null, the random secret generated + by kanidm will not be touched. Do NOT use a path from the nix store here! + ''; + type = types.nullOr types.path; + example = "/run/secrets/some-oauth2-basic-secret"; + default = null; + }; + + allowInsecureClientDisablePkce = mkOption { + description = '' + Disable PKCE on this oauth2 resource server to work around insecure clients + that may not support it. You should request the client to enable PKCE! + ''; + type = types.bool; + default = false; + }; + + preferShortUsername = mkOption { + description = "Use 'name' instead of 'spn' in the preferred_username claim"; + type = types.bool; + default = false; + }; + + scopeMaps = mkOption { + description = '' + Maps kanidm groups to returned oauth scopes. + See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. + ''; + type = types.attrsOf (types.listOf types.str); + default = {}; + }; + + supplementaryScopeMaps = mkOption { + description = '' + Maps kanidm groups to additionally returned oauth scopes. + See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. + ''; + type = types.attrsOf (types.listOf types.str); + default = {}; + }; + + removeOrphanedClaimMaps = mkOption { + description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm."; + type = types.bool; + default = true; + }; + + claimMaps = mkOption { + description = '' + Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + default = {}; + type = types.attrsOf (types.submodule { + options = { + joinType = mkOption { + description = '' + Determines how multiple values are joined to create the claim value. + See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. + ''; + type = types.enum ["array" "csv" "ssv"]; + default = "array"; + }; + + valuesByGroup = mkOption { + description = "Maps kanidm groups to values for the claim."; + default = {}; + type = types.attrsOf (types.listOf types.str); + }; + }; + }); + }; + }; + }); + }; + }; + }; + + config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { + assertions = let + entityList = type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: {inherit type name;}); + entities = + entityList "group" cfg.provision.groups + ++ entityList "person" cfg.provision.persons + ++ entityList "oauth2" cfg.provision.systems.oauth2; + + # Accumulate entities by name. Track corresponding entity types for later duplicate check. + entitiesByName = + foldl' ( + acc: { + type, + name, + }: + acc + // { + ${name} = (acc.${name} or []) ++ [type]; + } + ) {} + entities; + + assertGroupsKnown = opt: groups: let + knownGroups = attrNames (filterPresent cfg.provision.groups); + unknownGroups = subtractLists knownGroups groups; + in { + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == []; + message = "${opt} refers to unknown groups: ${toString unknownGroups}"; + }; + + assertEntitiesKnown = opt: entities: let + unknownEntities = subtractLists (attrNames entitiesByName) entities; + in { + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == []; + message = "${opt} refers to unknown entities: ${toString unknownEntities}"; + }; + in + [ + { + assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!isStorePath cfg.serverSettings.tls_chain); + message = '' + points to + a file in the Nix store. You should use a quoted absolute path to + prevent this. + ''; + } + { + assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!isStorePath cfg.serverSettings.tls_key); + message = '' + points to + a file in the Nix store. You should use a quoted absolute path to + prevent this. + ''; + } + { + assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; + message = '' + needs to be configured + if the client is enabled. + ''; + } + { + assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; + message = '' + needs to be configured + for the PAM daemon to connect to the Kanidm server. + ''; + } + { + assertion = + !cfg.enableServer + || (cfg.serverSettings.domain + == null + -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); + message = '' + can only be set if this instance + is not a ReadOnlyReplica. Otherwise the db would inherit it from + the instance it follows. + ''; + } + { + assertion = cfg.provision.enable -> cfg.enableServer; + message = " requires to be true"; + } + # If any secret is provisioned, the kanidm package must have some required patches applied to it + { + assertion = + (cfg.provision.enable + && ( + cfg.provision.adminPasswordFile + != null + || cfg.provision.idmAdminPasswordFile != null + || any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2)) + )) + -> cfg.package.enableSecretProvisioning; + message = '' + Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches. + You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`. + ''; + } + # Entity names must be globally unique: + (let + # Filter all names that occurred in more than one entity type. + duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName; + in { + assertion = cfg.provision.enable -> duplicateNames == {}; + message = '' + services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique! + ${concatLines (mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames)}''; + }) + ] + ++ flip mapAttrsToList (filterPresent cfg.provision.persons) ( + person: personCfg: + assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups + ) + ++ flip mapAttrsToList (filterPresent cfg.provision.groups) ( + group: groupCfg: + assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members + ) + ++ concatLists (flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( + oauth2: oauth2Cfg: + [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (attrNames oauth2Cfg.scopeMaps)) + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (attrNames oauth2Cfg.supplementaryScopeMaps)) + ] + ++ concatLists (flip mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (attrNames claimCfg.valuesByGroup)) + # At least one group must map to a value in each claim map + { + assertion = (cfg.provision.enable && cfg.enableServer) -> any (xs: xs != []) (attrValues claimCfg.valuesByGroup); + message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; + } + ])) + )); + + environment.systemPackages = mkIf cfg.enableClient [cfg.package]; + + systemd.tmpfiles.settings."10-kanidm" = { + ${cfg.serverSettings.online_backup.path}.d = { + mode = "0700"; + user = "kanidm"; + group = "kanidm"; + }; + }; + + systemd.services.kanidm = mkIf cfg.enableServer { + description = "kanidm identity management daemon"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + serviceConfig = mkMerge [ + # Merge paths and ignore existing prefixes needs to sidestep mkMerge + (defaultServiceConfig + // { + BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); + }) + { + StateDirectory = "kanidm"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "kanidmd"; + ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}"; + ExecStartPost = mkIf cfg.provision.enable postStartScript; + User = "kanidm"; + Group = "kanidm"; + + BindPaths = [ + # To create the socket + "/run/kanidmd:/run/kanidmd" + # To store backups + cfg.serverSettings.online_backup.path + ]; + + AmbientCapabilities = ["CAP_NET_BIND_SERVICE"]; + CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; + # This would otherwise override the CAP_NET_BIND_SERVICE capability. + PrivateUsers = mkForce false; + # Port needs to be exposed to the host network + PrivateNetwork = mkForce false; + RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; + TemporaryFileSystem = "/:ro"; + } + ]; + environment.RUST_LOG = "info"; + }; + + systemd.services.kanidm-unixd = mkIf cfg.enablePam { + description = "Kanidm PAM daemon"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + restartTriggers = [unixConfigFile clientConfigFile]; + serviceConfig = mkMerge [ + defaultServiceConfig + { + CacheDirectory = "kanidm-unixd"; + CacheDirectoryMode = "0700"; + RuntimeDirectory = "kanidm-unixd"; + ExecStart = "${cfg.package}/bin/kanidm_unixd"; + User = "kanidm-unixd"; + Group = "kanidm-unixd"; + + BindReadOnlyPaths = [ + "-/etc/kanidm" + "-/etc/static/kanidm" + "-/etc/ssl" + "-/etc/static/ssl" + "-/etc/passwd" + "-/etc/group" + ]; + BindPaths = [ + # To create the socket + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # Needs to connect to kanidmd + PrivateNetwork = mkForce false; + RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; + TemporaryFileSystem = "/:ro"; + } + ]; + environment.RUST_LOG = "info"; + }; + + systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam { + description = "Kanidm PAM home management daemon"; + wantedBy = ["multi-user.target"]; + after = ["network.target" "kanidm-unixd.service"]; + partOf = ["kanidm-unixd.service"]; + restartTriggers = [unixConfigFile clientConfigFile]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; + + BindReadOnlyPaths = [ + "/nix/store" + "-/etc/resolv.conf" + "-/etc/nsswitch.conf" + "-/etc/hosts" + "-/etc/localtime" + "-/etc/kanidm" + "-/etc/static/kanidm" + ]; + BindPaths = [ + # To manage home directories + "/home" + # To connect to kanidm-unixd + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket + CapabilityBoundingSet = ["CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH"]; + IPAddressDeny = "any"; + # Need access to users + PrivateUsers = false; + # Need access to home directories + ProtectHome = false; + RestrictAddressFamilies = ["AF_UNIX"]; + TemporaryFileSystem = "/:ro"; + Restart = "on-failure"; + }; + environment.RUST_LOG = "info"; + }; + + # These paths are hardcoded + environment.etc = mkMerge [ + (mkIf cfg.enableServer { + "kanidm/server.toml".source = serverConfigFile; + }) + (mkIf options.services.kanidm.clientSettings.isDefined { + "kanidm/config".source = clientConfigFile; + }) + (mkIf cfg.enablePam { + "kanidm/unixd".source = unixConfigFile; + }) + ]; + + system.nssModules = mkIf cfg.enablePam [cfg.package]; + + system.nssDatabases.group = optional cfg.enablePam "kanidm"; + system.nssDatabases.passwd = optional cfg.enablePam "kanidm"; + + users.groups = mkMerge [ + (mkIf cfg.enableServer { + kanidm = {}; + }) + (mkIf cfg.enablePam { + kanidm-unixd = {}; + }) + ]; + users.users = mkMerge [ + (mkIf cfg.enableServer { + kanidm = { + description = "Kanidm server"; + isSystemUser = true; + group = "kanidm"; + packages = [cfg.package]; + }; + }) + (mkIf cfg.enablePam { + kanidm-unixd = { + description = "Kanidm PAM daemon"; + isSystemUser = true; + group = "kanidm-unixd"; + }; + }) + ]; + }; + + meta.maintainers = with lib.maintainers; [erictapen Flakebi oddlama]; + meta.buildDocsInSandbox = false; +} diff --git a/modules/optional/streamdeck.nix b/modules/optional/streamdeck.nix index d2f48cc..50cedd7 100644 --- a/modules/optional/streamdeck.nix +++ b/modules/optional/streamdeck.nix @@ -14,7 +14,15 @@ settingsFormat = pkgs.formats.json {}; in { home-manager.sharedModules = [ - ({config, ...}: { + ({config, ...}: let + cfg = settingsFormat.generate "config.json" { + streamdeck_ui_version = 1; + state = config.programs.streamdeck-ui.settings; + }; + preStart = pkgs.writeShellScript "streamdeck-setup-config" '' + cp "${cfg}" "$XDG_RUNTIME_DIR/streamdeck/config.json" + ''; + in { options.programs.streamdeck-ui = { enable = mkEnableOption "streamdeck-ui"; package = mkPackageOption pkgs "streamdeck-ui" {}; @@ -36,20 +44,14 @@ in { Service = { Type = "exec"; ExecStart = "${pkgs.streamdeck-ui}/bin/streamdeck --no-ui"; - Environment = "STREAMDECK_UI_CONFIG=${config.xdg.configHome}/streamdeck-ui/config.json"; + ExecStartPre = preStart; + Environment = ''STREAMDECK_UI_CONFIG=%t/streamdeck/config.json''; + RuntimeDirectory = "streamdeck"; }; Install.WantedBy = ["graphical-session.target"]; }; }; }; - - xdg.configFile.streamdeck-ui = { - target = "streamdeck-ui/config.json"; - source = settingsFormat.generate "config.json" { - streamdeck_ui_version = 1; - state = config.programs.streamdeck-ui.settings; - }; - }; }; }) ]; diff --git a/modules/services/authelia.nix b/modules/services/authelia.nix deleted file mode 100644 index 9a21b4c..0000000 --- a/modules/services/authelia.nix +++ /dev/null @@ -1,104 +0,0 @@ -{ - config, - pkgs, - ... -}: let - stateDir = "/var/lib/authelia-main"; -in { - age.secrets.jwtSecretFile = { - generator.script = "alnum"; - mode = "440"; - inherit (config.services.authelia.instances.main) group; - }; - - age.secrets.sessionSecretFile = { - generator.script = "alnum"; - mode = "440"; - inherit (config.services.authelia.instances.main) group; - }; - - age.secrets.storageEncryptionKeyFile = { - generator.script = "alnum"; - mode = "440"; - inherit (config.services.authelia.instances.main) group; - }; - - age.secrets.oidcHmacSecretFile = { - generator.script = "alnum"; - mode = "440"; - inherit (config.services.authelia.instances.main) group; - }; - - age.secrets.oidcIssuerPrivateKeyFile = { - generator.script = {pkgs, ...}: '' - ${pkgs.openssl}/bin/openssl genrsa 4096 - ''; - mode = "440"; - inherit (config.services.authelia.instances.main) group; - }; - networking.firewall.allowedTCPPorts = [config.services.authelia.instances.main.settings.server.port]; - - services.authelia.instances.main = { - enable = true; - secrets = { - jwtSecretFile = config.age.secrets.jwtSecretFile.path; - sessionSecretFile = config.age.secrets.sessionSecretFile.path; - storageEncryptionKeyFile = config.age.secrets.storageEncryptionKeyFile.path; - oidcHmacSecretFile = config.age.secrets.oidcHmacSecretFile.path; - oidcIssuerPrivateKeyFile = config.age.secrets.oidcIssuerPrivateKeyFile.path; - }; # TODO - settings = { - session = { - domain = config.secrets.secrets.global.domains.web; - }; - webauthn.disable = true; - duo_api.disable = true; - ntp.disable_startup_check = true; - theme = "dark"; - default_2fa_method = "totp"; - server.host = "0.0.0.0"; - access_control.default_policy = "one_factor"; - webauthn = { - attestation_conveyance_preference = "none"; - user_verification = "discouraged"; - }; - - authentication_backend = { - password_reset.disable = true; - file = { - path = pkgs.writeText "user-db" (builtins.toJSON { - users.patrick = { - disabled = false; - displayname = "Patrick"; - password = "$argon2id$v=19$m=4096,t=3,p=1$Ym5yc3VhZHJub2I$ihbPHC697Nybk1H7WHCMKi+2KkvNhdwvScaorkkj5yM"; - email = "patrick@${config.secrets.secrets.global.domains.mail_public}"; - groups = ["admin" "forgejo_admin"]; - }; - users.test = { - disabled = false; - displayname = "Test"; - password = "$argon2id$v=19$m=4096,t=3,p=1$cmJuaWJldGRheA$kG4NCJRryXTCe/8Jc2/BBnEmlWSRwq4pZG7LH7fKs/o"; - email = "test@${config.secrets.secrets.global.domains.mail_public}"; - groups = []; - }; - }); - }; - }; - password_policy.standard = { - enabled = true; - min_length = 32; - }; - notifier.filesystem.filename = "${stateDir}/notifications.txt"; - storage.local.path = "${stateDir}/db.sqlite3"; - identity_providers.oidc.clients = [ - { - id = "forgejo"; - secret = "$argon2id$v=19$m=4096,t=3,p=1$Ym5yc3VhZHJub2I$0gZRilVu8O1rmVxX+ZTMFFHqya6YN8l+8QXFIorhtKM"; - redirect_uris = ["https://git.${config.secrets.secrets.global.domains.web}/user/oauth2/authelia/callback"]; - public = false; - scopes = ["openid" "email" "profile" "groups"]; - } - ]; - }; - }; -} diff --git a/modules/services/gitea.nix b/modules/services/forgejo.nix similarity index 72% rename from modules/services/gitea.nix rename to modules/services/forgejo.nix index 082c869..fc82c78 100644 --- a/modules/services/gitea.nix +++ b/modules/services/forgejo.nix @@ -1,10 +1,11 @@ { config, + nodes, pkgs, lib, ... }: let - giteaDomain = "git.${config.secrets.secrets.global.domains.web}"; + forgejoDomain = "git.${config.secrets.secrets.global.domains.web}"; in { age.secrets.resticpasswd = { generator.script = "alnum"; @@ -28,7 +29,7 @@ in { inherit (config.secrets.secrets.global.hetzner.users.forgejo) subUid path; sshAgeSecret = "forgejoHetznerSsh"; }; - paths = [config.services.gitea.stateDir]; + paths = [config.services.forgejo.stateDir]; pruneOpts = [ "--keep-daily 10" "--keep-weekly 7" @@ -44,29 +45,27 @@ in { environment.persistence."/panzer".directories = [ { - directory = config.services.gitea.stateDir; - user = "gitea"; - group = "gitea"; + directory = config.services.forgejo.stateDir; + user = "forgejo"; + group = "forgejo"; mode = "0700"; } ]; - age.secrets.gitea-mailer-passwd = { - rekeyFile = config.node.secretsDir + "/gitea-passwd.age"; - owner = "gitea"; - group = "gitea"; + age.secrets.forgejo-mailer-passwd = { + rekeyFile = config.node.secretsDir + "/forgejo-passwd.age"; + owner = "forgejo"; + group = "forgejo"; mode = "0700"; }; - services.gitea = { + services.forgejo = { enable = true; - package = pkgs.forgejo; - appName = "Patricks tolles git"; # tungsten inert gas? - stateDir = "/var/lib/forgejo"; # TODO db backups # dump.enable = true; lfs.enable = true; - mailerPasswordFile = config.age.secrets.gitea-mailer-passwd.path; + mailerPasswordFile = config.age.secrets.forgejo-mailer-passwd.path; settings = { + DEFAULT.APP_NAME = "Patricks tolles git"; # tungsten inert gas? actions = { ENABLED = true; DEFAULT_ACTIONS_URL = "github"; @@ -78,17 +77,17 @@ in { # federation.ENABLED = true; mailer = { ENABLED = true; - SMTP_ADDR = config.secrets.secrets.local.gitea.mail.host; - FROM = config.secrets.secrets.local.gitea.mail.from; - USER = config.secrets.secrets.local.gitea.mail.user; + SMTP_ADDR = config.secrets.secrets.local.forgejo.mail.host; + FROM = config.secrets.secrets.local.forgejo.mail.from; + USER = config.secrets.secrets.local.forgejo.mail.user; SEND_AS_PLAIN_TEXT = true; }; oauth2_client = { ACCOUNT_LINKING = "login"; - ENABLE_AUTO_REGISTRATION = true; + ENABLE_AUTO_REGISTRATION = false; REGISTER_EMAIL_CONFIRM = false; UPDATE_AVATAR = true; - USERNAME = "email"; + USERNAME = "nickname"; }; # packages.ENABLED = true; repository = { @@ -99,8 +98,8 @@ in { server = { HTTP_ADDR = "0.0.0.0"; HTTP_PORT = 3000; - DOMAIN = giteaDomain; - ROOT_URL = "https://${giteaDomain}/"; + DOMAIN = forgejoDomain; + ROOT_URL = "https://${forgejoDomain}/"; LANDING_PAGE = "login"; SSH_PORT = 9922; # TODO @@ -108,9 +107,9 @@ in { # port forwarding in elisabeth }; service = { - DISABLE_REGISTRATION = true; + DISABLE_REGISTRATION = false; ALLOW_ONLY_EXTERNAL_REGISTRATION = true; - SHOW_REGISTRATION_BUTTON = true; + SHOW_REGISTRATION_BUTTON = false; REGISTER_EMAIL_CONFIRM = false; ENABLE_NOTIFY_MAIL = true; DEFAULT_KEEP_EMAIL_PRIVATE = true; @@ -126,13 +125,11 @@ in { # XXX: PKCE is currently not supported by gitea/forgejo, # see https://github.com/go-gitea/gitea/issues/21376. - # Disable PKCE manually in kanidm for now. - # `kanidm system oauth2 warning-insecure-client-disable-pkce forgejo` - systemd.services.gitea = { + systemd.services.forgejo = { serviceConfig.RestartSec = "600"; # Retry every 10 minutes preStart = let - exe = lib.getExe config.services.gitea.package; - providerName = "authelia"; + exe = lib.getExe config.services.forgejo.package; + providerName = "kanidm"; clientId = "forgejo"; args = lib.escapeShellArgs [ "--name" @@ -143,18 +140,14 @@ in { clientId "--auto-discover-url" "https://auth.${config.secrets.secrets.global.domains.web}/oauth2/openid/${clientId}/.well-known/openid-configuration" - "--required-claim-name" - "groups" "--scopes" "email" "--scopes" "profile" - "--scopes" - "groups" "--group-claim-name" "groups" "--admin-group" - "forgejo_admin" + "admin" "--skip-local-2fa" ]; in @@ -170,8 +163,8 @@ in { }; age.secrets.openid-secret = { - generator.script = "alnum"; + inherit (nodes.elisabeth-kanidm.config.age.secrets.oauth2-forgejo) rekeyFile; mode = "440"; - inherit (config.services.gitea) group; + inherit (config.services.forgejo) group; }; } diff --git a/modules/services/kanidm.nix b/modules/services/kanidm.nix index 17a9197..3d25551 100644 --- a/modules/services/kanidm.nix +++ b/modules/services/kanidm.nix @@ -1,6 +1,8 @@ {config, ...}: let kanidmdomain = "auth.${config.secrets.secrets.global.domains.web}"; in { + imports = [../kanidm.nix]; + disabledModules = ["services/security/kanidm.nix"]; networking.firewall.allowedTCPPorts = [3000]; environment.persistence."/persist".directories = [ { @@ -21,6 +23,11 @@ in { group = "kanidm"; mode = "440"; }; + oauth2-forgejo = { + generator.script = "alnum"; + mode = "440"; + group = "kanidm"; + }; }; services.kanidm = { enableServer = true; @@ -38,5 +45,39 @@ in { verify_ca = true; verify_hostnames = true; }; + provision = { + enable = true; + + persons = { + "patrick" = { + displayName = "Patrick"; + mailAddresses = ["patrick@${config.secrets.secrets.global.domains.mail}"]; + groups = ["forgejo.admins"]; + }; + "test" = { + displayName = "test"; + mailAddresses = ["test@${config.secrets.secrets.global.domains.mail}"]; + groups = ["forgejo.access"]; + }; + }; + + groups."forgejo.access" = { + members = ["forgejo.admins"]; + }; + groups."forgejo.admins" = {}; + systems.oauth2.forgejo = { + displayName = "Forgejo"; + originUrl = "https://git.${config.secrets.secrets.global.domains.web}/"; + basicSecretFile = config.age.secrets.oauth2-forgejo.path; + scopeMaps."forgejo.access" = ["openid" "email" "profile"]; + allowInsecureClientDisablePkce = true; + preferShortUsername = true; + claimMaps.groups = { + joinType = "array"; + valuesByGroup."forgejo.admins" = ["admin"]; + }; + }; + }; }; + systemd.services.kanidm.serviceConfig.RestartSec = "60"; # Retry every minute } diff --git a/pkgs/default.nix b/pkgs/default.nix index 30a6ec9..febcb70 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -16,5 +16,23 @@ wrapProgram $out/bin/nvim --add-flags "--clean" ''; }); + kanidm = super.kanidm.overrideAttrs (old: let + provisionSrc = super.fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + rev = "aa7a1c8ec04622745b385bd3b0462e1878f56b51"; + hash = "sha256-NRolS3l2kARjkhWP7FYUG//KCEiueh48ZrADdCDb9Zg="; + }; + in { + patches = + old.patches + ++ [ + "${provisionSrc}/patches/${old.version}-oauth2-basic-secret-modify.patch" + "${provisionSrc}/patches/${old.version}-recover-account.patch" + ]; + passthru.enableSecretProvisioning = true; + doCheck = false; + }); + kanidm-provision = super.callPackage ./kanidm-provision.nix {}; }) ] diff --git a/pkgs/kanidm-provision.nix b/pkgs/kanidm-provision.nix new file mode 100644 index 0000000..c3b5891 --- /dev/null +++ b/pkgs/kanidm-provision.nix @@ -0,0 +1,26 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, +}: +rustPlatform.buildRustPackage rec { + pname = "kanidm-provision"; + version = "1.0.0"; + + src = fetchFromGitHub { + owner = "oddlama"; + repo = "kanidm-provision"; + rev = "v${version}"; + hash = "sha256-T6kiBUdOMHCWRUF/vepoPrvaULDQrUGYsd/3I11HCLY="; + }; + + cargoHash = "sha256-nHp3C6szJxOogH/kETIqcQQNhFqBCO0P66j7n3UHuwo="; + + meta = with lib; { + description = "A small utility to help with kanidm provisioning"; + homepage = "https://github.com/oddlama/kanidm-provision"; + license = with licenses; [asl20 mit]; + maintainers = with maintainers; [oddlama]; + mainProgram = "kanidm-provision"; + }; +} diff --git a/secrets/secrets.nix.age b/secrets/secrets.nix.age index 6a38c06..e4d4bf9 100644 Binary files a/secrets/secrets.nix.age and b/secrets/secrets.nix.age differ diff --git a/users/patrick/streamdeck.nix b/users/patrick/streamdeck.nix index cb0b89f..f364db3 100644 --- a/users/patrick/streamdeck.nix +++ b/users/patrick/streamdeck.nix @@ -59,11 +59,12 @@ "1" = { "0" = { icon = config.images.images."back.png"; - switch_page = 0; + switch_page = 1; background_color = config.lib.stylix.colors.withHashtag.base0C; }; }; }; + page = 0; # The startup page brightness = 99; # brighness value between 0 and 99 display_timeout = 0; # dimmer timeout in seconds brightness_dimmed = 99; # dimmed brighness