nix-configs/modules/services/kanidm.nix
Lucy Hochkamp 28dc0896b9
Some checks failed
ci/woodpecker/push/build-cache Pipeline failed
ci/woodpecker/cron/dependency-pr Pipeline was successful
navidrome
2025-12-04 00:21:41 +01:00

285 lines
8.6 KiB
Nix

{
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"
];
};
};
})
];
}