{ pkgs, lib, config, ... }: let inherit (lib) mkEnableOption mkIf mkOption converge mkMerge filterAttrsRecursive literalExpression mapAttrsToList concatStringsSep attrNames ; inherit (lib.types) str nullOr pathWith submodule listOf functionTo attrsOf lines ; absPath = pathWith { inStore = false; absolute = true; }; cfg = config.xyno.services.kanidm; tlsDir = "/run/generated/kanidm-tls"; package = pkgs.kanidmWithSecretProvisioning_1_8.overrideAttrs (old: { doCheck = false; patches = old.patches ++ [ (pkgs.writeText "patch-kanidm-name" '' diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index 86b5a74c1..c83b2f93d 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -64,7 +64,7 @@ lazy_static! { /// Only lowercase+numbers, with limited chars. pub static ref INAME_RE: Regex = { #[allow(clippy::expect_used)] - Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found") + Regex::new("^[a-z0-9-_\\.]{0,64}$").expect("Invalid Iname regex found") }; /// Only alpha-numeric with limited special chars and space '') ]; }); templatePlaceholders = { clientId = ''\($get.attrs.name[0])''; basicSecret = ''\($secret.secret)''; env = v: ''\(env.${v})''; }; in { options.xyno.services.kanidm.enable = mkEnableOption "enables kanidm"; options.xyno.services.kanidm.domain = mkOption { default = "idm.xyno.systems"; type = str; }; options.xyno.services.kanidm.isReplica = mkEnableOption "replica"; # TODO options.xyno.services.kanidm.setupTraefik = mkEnableOption "traefik"; options.xyno.services.kanidm.templates = mkOption { type = attrsOf ( submodule ( { name, ... }: { options = { path = mkOption { type = absPath; default = "/run/generated/kanidmTemplates/${name}"; }; user = mkOption { type = str; default = "root"; }; group = mkOption { type = str; default = "kanidm"; }; chmod = mkOption { type = str; default = "440"; }; wantedBy = mkOption { type = listOf str; default = [ ]; example = [ "traccar.service" ]; }; text = mkOption { type = functionTo lines; description = '' jq templated string current placeholders: ${concatStringsSep ", " (attrNames templatePlaceholders)} ''; example = literalExpression '' p: ${"''"} OAUTH2_PROXY_CLIENT_ID=''${p.clientId} OAUTH2_PROXY_CLIENT_SECRET=''${p.clientSecret} ${"''"} ''; }; environmentFiles = mkOption { type = listOf absPath; default = [ ]; description = '' add environment variables to the template file. the environment variable BANANA would be accessible as ``` COOKIE_SECRET=''${p.env "BANANA"} ``` in the template ''; }; }; } ) ); example = { traccar.text = p: '' OPENID_CLIENTID=${p.clientId} OPENID_SECRET=${p.basicSecret} ''; }; }; options.xyno.services.kanidm.tls = { keyPem = mkOption { type = nullOr absPath; default = null; description = "autogenerated if unset"; }; certPem = mkOption { default = "${tlsDir}/cert.pem"; type = absPath; }; }; config = mkMerge [ (mkIf cfg.enable { services.kanidm = { enableServer = true; enableClient = true; inherit package; clientSettings.uri = "https://${cfg.domain}"; provision = { enable = true; adminPasswordFile = config.sops.secrets."kanidm/adminPassword".path; idmAdminPasswordFile = config.sops.secrets."kanidm/idmAdminPassword".path; instanceUrl = "https://127.0.0.3:8443"; acceptInvalidCerts = true; autoRemove = true; groups.application_admins = {}; }; serverSettings = { trust_x_forward_for = true; tls_key = if cfg.tls.keyPem != null then cfg.tls.keyPem else "${tlsDir}/key.pem"; tls_chain = cfg.tls.certPem; bindaddress = "127.0.0.3:8443"; origin = "https://${cfg.domain}"; domain = cfg.domain; }; }; systemd.tmpfiles.rules = [ "d /run/generated/kanidmTemplates 1755 root kanidm -" ]; systemd.services = mkMerge ( (mapAttrsToList (n: v: { "generate-kanidm-template-${n}" = { serviceConfig = { User = "root"; Group = "kanidm"; Type = "oneshot"; PrivateTmp = true; EnvironmentFile = v.environmentFiles; }; requires = [ "kanidm.service" ] ++ (lib.optional cfg.setupTraefik "traefik.service"); after = [ "kanidm.service" "systemd-tmpfiles-setup.service" ] ++ (lib.optional cfg.setupTraefik "traefik.service"); before = v.wantedBy; partOf = v.wantedBy; wantedBy = if (builtins.length v.wantedBy) == 0 then [ "multi-user.target" ] else v.wantedBy; enableStrictShellChecks = true; path = [ package pkgs.jq ]; environment.KANIDM_TOKEN_CACHE_PATH = "/tmp/kanidm-token-cache"; script = let templateText = v.text templatePlaceholders; in '' KANIDM_PASSWORD=$(cat "${ config.sops.secrets."kanidm/idmAdminPassword".path }") kanidm login -D idm_admin jq -r -s \ -f "${pkgs.writeText "kanidm-template-${n}" ''"${templateText}"''}" \ --argjson get "$(kanidm system oauth2 get --output json "${n}")" \ --argjson secret "$(kanidm system oauth2 show-basic-secret --output json "${n}")" \ > "${v.path}" chown "${v.user}:${v.group}" "${v.path}" chmod "${v.chmod}" "${v.path}" ''; }; }) cfg.templates) ++ [ (mkIf (cfg.tls.keyPem == null) { generate-kanidm-tls = let units = [ "kanidm.service" ] ++ (lib.optional cfg.setupTraefik "traefik.service"); in { serviceConfig = { User = "root"; Group = "kanidm"; Type = "oneshot"; }; wantedBy = units; before = units; script = '' mkdir -p ${tlsDir} cd ${tlsDir} ${config.services.kanidm.package}/bin/kanidmd cert-generate -c ${ let toml = pkgs.formats.toml { }; filterConfig = converge (filterAttrsRecursive (_: v: v != null)); in toml.generate "kanidm-tls.conf" (filterConfig (config.services.kanidm.serverSettings)) } chmod +g ${tlsDir}/* ''; }; }) ] ); sops.secrets."kanidm/adminPassword" = { sopsFile = ../../instances/${config.networking.hostName}/secrets/kanidm.yaml; reloadUnits = [ "kanidm.service" ]; owner = "kanidm"; }; sops.secrets."kanidm/idmAdminPassword" = { sopsFile = ../../instances/${config.networking.hostName}/secrets/kanidm.yaml; reloadUnits = [ "kanidm.service" ]; owner = "kanidm"; }; xyno.impermanence.directories = [ "/var/lib/kanidm" ]; }) (mkIf (cfg.enable && cfg.setupTraefik) { xyno.services.traefik.simpleProxy.kanidm = { host = cfg.domain; internal = "https://127.0.0.3:8443"; transport = "kanidm-https"; }; services.traefik.dynamicConfigOptions.http = mkIf (cfg.tls.keyPem == null) { serversTransports."kanidm-https" = { serverName = cfg.domain; rootcas = [ "${tlsDir}/ca.pem" ]; }; }; }) ]; }