mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-11 07:10:39 +02:00
feat: switch to upstreamed influxdb2 provisioning, add kanidm provisioning module
This commit is contained in:
parent
9533e760e4
commit
522de920bb
11 changed files with 776 additions and 1325 deletions
|
@ -168,7 +168,7 @@ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
|
||||||
```bash
|
```bash
|
||||||
# Recover admin account
|
# Recover admin account
|
||||||
kanidmd recover-account admin
|
kanidmd recover-account admin
|
||||||
> AhNeQgKkwwEHZ85dxj1GPjx58vWsBU8QsvKSyYwUL7bz57bp
|
> FrEELN4tfyVbUAfhGeuUyZyaKk8cbpFufuDwyCPhY3xhb3X2
|
||||||
# Login with recovered root account
|
# Login with recovered root account
|
||||||
kanidm login --name admin
|
kanidm login --name admin
|
||||||
# Generate new credentials for idm_admin account
|
# Generate new credentials for idm_admin account
|
||||||
|
|
48
flake.lock
generated
48
flake.lock
generated
|
@ -47,11 +47,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691966209,
|
"lastModified": 1692783612,
|
||||||
"narHash": "sha256-0L2vP/QEi8W1s9dViB0dEgKRXa7vjPdyxvDD1oyZbwA=",
|
"narHash": "sha256-Mz1xv45Rjzet1D2bMGKapgw1JCHaD60dBs4sE6Dz2+A=",
|
||||||
"owner": "oddlama",
|
"owner": "oddlama",
|
||||||
"repo": "agenix-rekey",
|
"repo": "agenix-rekey",
|
||||||
"rev": "7e8f11a3cf6786477c420f2166a0c713bb706941",
|
"rev": "52695865488742e0b34a56111cd40e229b3ab90a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -159,11 +159,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691999995,
|
"lastModified": 1692199161,
|
||||||
"narHash": "sha256-8DyiH3zEdouwNhW68BkHrfoDYX9Cf1So6u8mCWN0iIo=",
|
"narHash": "sha256-GqKApvQ1JCf5DzH/Q+P4nwuHb6MaQGaWTu41lYzveF4=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "6388d2859c91adab847b4922b726f61920074494",
|
"rev": "4eed2457b053c4bbad7d90d2b3a1d539c2c9009c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -301,11 +301,11 @@
|
||||||
"systems": "systems_3"
|
"systems": "systems_3"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1689068808,
|
"lastModified": 1692799911,
|
||||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -364,11 +364,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1692131549,
|
"lastModified": 1692763155,
|
||||||
"narHash": "sha256-MFjI8NL63/6HjMZpvJgnB/Pgg2dht22t45jOYtipZig=",
|
"narHash": "sha256-qMrGKZ8c/q/mHO3ZdrcBPwiVVXPLLgXjY98Ejqb5kAA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "75cfe974e2ca05a61b66768674032b4c079e55d4",
|
"rev": "6a20e40acaebf067da682661aa67da8b36812606",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -414,11 +414,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691325831,
|
"lastModified": 1692274616,
|
||||||
"narHash": "sha256-/S1A8FpFE6yiIzFIAYTQCSn9uqOUziu92iRTokI0eiQ=",
|
"narHash": "sha256-UttCk5/sl0lLrBVO9kpmtDlFXcI2UkyOaSp7+grLRRE=",
|
||||||
"owner": "astro",
|
"owner": "astro",
|
||||||
"repo": "microvm.nix",
|
"repo": "microvm.nix",
|
||||||
"rev": "d5c5bb4cebbd9f59b7ab81a4b36fea10b6016d38",
|
"rev": "a291d324915f26d1fd86443bd486089099e8b541",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -465,11 +465,11 @@
|
||||||
},
|
},
|
||||||
"nixos-hardware": {
|
"nixos-hardware": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691871742,
|
"lastModified": 1692952286,
|
||||||
"narHash": "sha256-6yDNjfbAMpwzWL4y75fxs6beXHRANfYX8BNSPjYehck=",
|
"narHash": "sha256-TsrtPv3+Q1KR0avZxpiJH+b6fX/R/hEQVHbjl1ebotY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "430a56dd16fe583a812b2df44dca002acab2f4f6",
|
"rev": "817e297fc3352fadc15f2c5306909aa9192d7d97",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -501,11 +501,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1692084312,
|
"lastModified": 1692913444,
|
||||||
"narHash": "sha256-Za++qKVK6ovjNL9poQZtLKRM/re663pxzbJ+9M4Pgwg=",
|
"narHash": "sha256-1SvMQm2DwofNxXVtNWWtIcTh7GctEVrS/Xel/mdc6iY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8353344d3236d3fda429bb471c1ee008857d3b7c",
|
"rev": "18324978d632ffc55ef1d928e81630c620f4f447",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -588,11 +588,11 @@
|
||||||
"nixpkgs-stable": "nixpkgs-stable_2"
|
"nixpkgs-stable": "nixpkgs-stable_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691747570,
|
"lastModified": 1692274144,
|
||||||
"narHash": "sha256-J3fnIwJtHVQ0tK2JMBv4oAmII+1mCdXdpeCxtIsrL2A=",
|
"narHash": "sha256-BxTQuRUANQ81u8DJznQyPmRsg63t4Yc+0kcyq6OLz8s=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "pre-commit-hooks.nix",
|
"repo": "pre-commit-hooks.nix",
|
||||||
"rev": "c5ac3aa3324bd8aebe8622a3fc92eeb3975d317a",
|
"rev": "7e3517c03d46159fdbf8c0e5c97f82d5d4b0c8fa",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -36,16 +36,11 @@ in {
|
||||||
group = "influxdb2";
|
group = "influxdb2";
|
||||||
};
|
};
|
||||||
|
|
||||||
services.influxdb2.provision.ensureApiTokens = [
|
services.influxdb2.provision.organization.servers.auths."grafana servers:telegraf (${config.node.name})" = {
|
||||||
{
|
|
||||||
name = "grafana servers:telegraf (${config.node.name})";
|
|
||||||
org = "servers";
|
|
||||||
user = "admin";
|
|
||||||
readBuckets = ["telegraf"];
|
readBuckets = ["telegraf"];
|
||||||
writeBuckets = ["telegraf"];
|
writeBuckets = ["telegraf"];
|
||||||
tokenFile = nodes.ward-influxdb.config.age.secrets."grafana-influxdb-token-${config.node.name}".path;
|
tokenFile = nodes.ward-influxdb.config.age.secrets."grafana-influxdb-token-${config.node.name}".path;
|
||||||
}
|
};
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.sentinel = {
|
nodes.sentinel = {
|
||||||
|
|
42
hosts/ward/microvms/immich.nix
Normal file
42
hosts/ward/microvms/immich.nix
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
nodes,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
sentinelCfg = nodes.sentinel.config;
|
||||||
|
immichDomain = "immich.${sentinelCfg.repo.secrets.local.personalDomain}";
|
||||||
|
in {
|
||||||
|
meta.wireguard-proxy.sentinel.allowedTCPPorts = [config.services.immich.web_port];
|
||||||
|
|
||||||
|
nodes.sentinel = {
|
||||||
|
networking.providedDomains.immich = immichDomain;
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
upstreams.immich = {
|
||||||
|
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.immich.settings.bind_port}" = {};
|
||||||
|
extraConfig = ''
|
||||||
|
zone immich 64k;
|
||||||
|
keepalive 2;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
virtualHosts.${immichDomain} = {
|
||||||
|
forceSSL = true;
|
||||||
|
useACMEWildcardHost = true;
|
||||||
|
oauth2.enable = true;
|
||||||
|
oauth2.allowedGroups = ["access_immich"];
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://immich";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.immich = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.grafana.serviceConfig.RestartSec = "600"; # Retry every 10 minutes
|
||||||
|
}
|
|
@ -82,17 +82,7 @@ in {
|
||||||
passwordFile = config.age.secrets.influxdb-admin-password.path;
|
passwordFile = config.age.secrets.influxdb-admin-password.path;
|
||||||
tokenFile = config.age.secrets.influxdb-admin-token.path;
|
tokenFile = config.age.secrets.influxdb-admin-token.path;
|
||||||
};
|
};
|
||||||
ensureOrganizations = [
|
organizations.servers.buckets.telegraf = {};
|
||||||
{
|
|
||||||
name = "servers";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
ensureBuckets = [
|
|
||||||
{
|
|
||||||
name = "telegraf";
|
|
||||||
org = "servers";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
disabledModules = ["services/databases/influxdb2.nix"];
|
disabledModules = ["services/security/kanidm.nix"];
|
||||||
imports = [
|
imports = [
|
||||||
../users/root
|
../users/root
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
./config/users.nix
|
./config/users.nix
|
||||||
./config/xdg.nix
|
./config/xdg.nix
|
||||||
|
|
||||||
./meta/influxdb2.nix
|
./meta/kanidm.nix
|
||||||
./meta/microvms.nix
|
./meta/microvms.nix
|
||||||
./meta/nginx.nix
|
./meta/nginx.nix
|
||||||
./meta/oauth2-proxy.nix
|
./meta/oauth2-proxy.nix
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,32 +1,113 @@
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
inherit
|
inherit
|
||||||
(lib)
|
(lib)
|
||||||
all
|
all
|
||||||
|
any
|
||||||
attrNames
|
attrNames
|
||||||
attrValues
|
attrValues
|
||||||
concatLines
|
concatLines
|
||||||
concatLists
|
concatLists
|
||||||
concatMap
|
concatMap
|
||||||
concatMapStrings
|
concatMapStrings
|
||||||
|
converge
|
||||||
elem
|
elem
|
||||||
escapeShellArg
|
escapeShellArg
|
||||||
|
escapeShellArgs
|
||||||
|
filter
|
||||||
|
filterAttrsRecursive
|
||||||
flip
|
flip
|
||||||
|
foldl'
|
||||||
|
getExe
|
||||||
|
hasInfix
|
||||||
|
hasPrefix
|
||||||
|
isStorePath
|
||||||
|
mapAttrs
|
||||||
mapAttrsToList
|
mapAttrsToList
|
||||||
|
mdDoc
|
||||||
|
mkEnableOption
|
||||||
|
mkForce
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
mkOption
|
mkOption
|
||||||
|
mkPackageOptionMD
|
||||||
|
optional
|
||||||
optionals
|
optionals
|
||||||
subtractLists
|
subtractLists
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
|
|
||||||
cfg = config.services.kanidm;
|
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:
|
mkPresentOption = what:
|
||||||
mkOption {
|
mkOption {
|
||||||
description = "Whether to ensure that this ${what} is present or absent.";
|
description = mdDoc "Whether to ensure that this ${what} is present or absent.";
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
@ -39,14 +120,39 @@
|
||||||
default = script;
|
default = script;
|
||||||
};
|
};
|
||||||
|
|
||||||
provisionScript = pkgs.writeShellScript "post-start-provision" ''
|
mappingsJson = pkgs.writeText "mappings.json" (builtins.toJSON {
|
||||||
|
account_credentials.admin = cfg.provision.adminPasswordFile;
|
||||||
|
account_credentials.idm_admin = cfg.provision.idmAdminPasswordFile;
|
||||||
|
oauth2_basic_secrets = mapAttrs (_: x: x.basicSecretFile) cfg.provision.systems.oauth2;
|
||||||
|
});
|
||||||
|
|
||||||
|
preStartScript = pkgs.writeShellScript "pre-start-manipulate" ''
|
||||||
|
if ! test -e ${escapeShellArg cfg.serverSettings.db_path}; then
|
||||||
|
touch "$STATE_DIRECTORY/.first_startup"
|
||||||
|
else
|
||||||
|
${getExe pkgs.kanidm-secret-manipulator} ${escapeShellArg cfg.serverSettings.db_path} ${mappingsJson}
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
restarterScript = pkgs.writeShellScript "post-start-restarter" ''
|
||||||
|
set -euo pipefail
|
||||||
|
if test -e "$STATE_DIRECTORY/.needs_restart"; then
|
||||||
|
rm -f "$STATE_DIRECTORY/.needs_restart"
|
||||||
|
echo "Restarting kanidm.service..."
|
||||||
|
#kill -TERM $MAINPID
|
||||||
|
#echo "Restarting kanidm.service via dbus..."
|
||||||
|
${pkgs.dbus}/bin/dbus-send --system --type=method_call --print-reply --dest=org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager.RestartUnit string:"kanidm.service" string:"replace"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
postStartScript = pkgs.writeShellScript "post-start" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Wait for the kanidm server to come online
|
# Wait for the kanidm server to come online
|
||||||
count=0
|
count=0
|
||||||
while ! test -e /run/kanidmd/sock; do
|
while ! test -e /run/kanidmd/sock; do
|
||||||
if [ "$count" -eq 300 ]; then
|
if [ "$count" -eq 600 ]; then
|
||||||
echo "Tried for 30 seconds, giving up..."
|
echo "Tried for 60 seconds, giving up..."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! kill -0 "$MAINPID"; then
|
if ! kill -0 "$MAINPID"; then
|
||||||
|
@ -60,29 +166,97 @@
|
||||||
# If this is the first start, we login this time by recovering the admin account
|
# If this is the first start, we login this time by recovering the admin account
|
||||||
# and force a restart afterwards to rewrite the password.
|
# and force a restart afterwards to rewrite the password.
|
||||||
if test -e "$STATE_DIRECTORY/.first_startup"; then
|
if test -e "$STATE_DIRECTORY/.first_startup"; then
|
||||||
KANIDM_PASSWORD="$(${cfg.package}/bin/kanidmd recover-account admin)"
|
# Recover admin account
|
||||||
|
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin); then
|
||||||
|
echo "$recover_out" >&2
|
||||||
|
echo "kanidm provision: Failed to recover admin account" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! KANIDM_PASSWORD_ADMIN=$(grep -o '[A-Za-z0-9]\{48\}' <<< "$recover_out"); then
|
||||||
|
echo "$recover_out" >&2
|
||||||
|
echo "kanidm provision: Failed to parse password for admin account" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Recover idm_admin account
|
||||||
|
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin); then
|
||||||
|
echo "$recover_out" >&2
|
||||||
|
echo "kanidm provision: Failed to recover admin account" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! KANIDM_PASSWORD_IDM=$(grep -o '[A-Za-z0-9]\{48\}' <<< "$recover_out"); then
|
||||||
|
echo "$recover_out" >&2
|
||||||
|
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
needs_rewrite=1
|
needs_rewrite=1
|
||||||
rm -f "$STATE_DIRECTORY/.first_startup"
|
rm -f "$STATE_DIRECTORY/.first_startup"
|
||||||
else
|
else
|
||||||
# Login using the admin password
|
# Login using the admin password
|
||||||
KANIDM_PASSWORD="$(< ${escapeShellArg cfg.provision.adminPasswordFile})"
|
KANIDM_PASSWORD_ADMIN="$(< ${escapeShellArg cfg.provision.adminPasswordFile})"
|
||||||
|
KANIDM_PASSWORD_IDM="$(< ${escapeShellArg cfg.provision.idmAdminPasswordFile})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${cfg.package}/bin/kanidm login --name admin <<< "$KANIDM_PASSWORD"
|
# Login to admin and idm_admin
|
||||||
|
export TMPDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf $TMPDIR' EXIT
|
||||||
|
# Set $HOME so kanidm can save the token temporarily
|
||||||
|
export HOME=$TMPDIR
|
||||||
|
KANIDM_PASSWORD=$KANIDM_PASSWORD_ADMIN ${cfg.package}/bin/kanidm login --name admin \
|
||||||
|
|| { echo "kanidm provision: Failed to login as admin, see kanidm logs." >&2; exit 1; }
|
||||||
|
KANIDM_PASSWORD=$KANIDM_PASSWORD_IDM ${cfg.package}/bin/kanidm login --name idm_admin \
|
||||||
|
|| { echo "kanidm provision: Failed to login as idm_admin, see kanidm logs." >&2; exit 1; }
|
||||||
|
|
||||||
known_groups=$(kanidm group list --output=json)
|
# Wrapper function that detects kanidm errors by detecting any output to stderr
|
||||||
|
# (stderr and stdout are swapped when calling this)
|
||||||
|
function kanidm-detect-err() {
|
||||||
|
if ! err=$(${cfg.package}/bin/kanidm "$@" 3>&2 2>&1 1>&3-); then
|
||||||
|
echo "$err"
|
||||||
|
echo "kanidm ''${*@Q}: failed with status $?, see error above"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$err" ]]; then
|
||||||
|
echo "$err"
|
||||||
|
echo "kanidm ''${*@Q}: failed, see error above"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wrapper function to easily execute commands as admin or idm_admin
|
||||||
|
function kanidm-as-user() {
|
||||||
|
name=$1
|
||||||
|
shift
|
||||||
|
kanidm-detect-err "$@" --name "$name" 3>&2 2>&1 1>&3-
|
||||||
|
}
|
||||||
|
|
||||||
|
function kanidm-admin() { kanidm-as-user admin "$@"; }
|
||||||
|
function kanidm-idm() { kanidm-as-user idm_admin "$@"; }
|
||||||
|
|
||||||
|
known_groups=$(kanidm-admin group list --output=json)
|
||||||
function group_exists() {
|
function group_exists() {
|
||||||
[[ -n "$(${getExe jq} <<< "$known_groups" '. | select(.name[0] == "$1")')" ]]
|
if ! x=$(${getExe pkgs.jq} <<< "$known_groups" ".[] | select(.name[0] == \"$1\")"); then
|
||||||
|
echo "kanidm provision: Failed to parse groups list." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ -n "$x" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
known_persons=$(kanidm person list --output=json)
|
known_persons=$(kanidm-admin person list --output=json)
|
||||||
function person_exists() {
|
function person_exists() {
|
||||||
[[ -n "$(${getExe jq} <<< "$known_persons" '. | select(.name[0] == "$1")')" ]]
|
if ! x=$(${getExe pkgs.jq} <<< "$known_persons" ".[] | select(.name[0] == \"$1\")"); then
|
||||||
|
echo "kanidm provision: Failed to parse persons list." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ -n "$x" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
known_oauth2_systems=$(kanidm person list --output=json)
|
known_oauth2_systems=$(kanidm-admin system oauth2 list --output=json)
|
||||||
function oauth2_system_exists() {
|
function oauth2_system_exists() {
|
||||||
[[ -n "$(${getExe jq} <<< "$known_oauth2_systems" '. | select(.oauth2_rs_name[0] == "$1")')" ]]
|
if ! x=$(${getExe pkgs.jq} <<< "$known_oauth2_systems" ".[] | select(.oauth2_rs_name[0] == \"$1\")"); then
|
||||||
|
echo "kanidm provision: Failed to parse oauth2 systems list." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ -n "$x" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
${concatMapStrings (x: x._script) (attrValues cfg.provision.groups)}
|
${concatMapStrings (x: x._script) (attrValues cfg.provision.groups)}
|
||||||
|
@ -94,29 +268,34 @@
|
||||||
touch "$STATE_DIRECTORY/.needs_restart"
|
touch "$STATE_DIRECTORY/.needs_restart"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
restarterScript = pkgs.writeShellScript "post-start-restarter" ''
|
|
||||||
set -euo pipefail
|
|
||||||
if test -e "$STATE_DIRECTORY/.needs_restart"; then
|
|
||||||
rm -f "$STATE_DIRECTORY/.needs_restart"
|
|
||||||
/run/current-system/systemd/bin/systemctl restart kanidm
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
in {
|
in {
|
||||||
options.services.kanidm.provision = {
|
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 = mkPackageOptionMD pkgs "kanidm" {};
|
||||||
|
|
||||||
|
provision = {
|
||||||
enable = mkEnableOption "provisioning of systems (oauth2), groups and users";
|
enable = mkEnableOption "provisioning of systems (oauth2), groups and users";
|
||||||
|
|
||||||
adminPasswordFile = mkOption {
|
adminPasswordFile = mkOption {
|
||||||
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
|
description = mdDoc "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";
|
example = "/run/secrets/kanidm-admin-password";
|
||||||
type = types.path;
|
type = types.path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
idmAdminPasswordFile = mkOption {
|
||||||
|
description = mdDoc "Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!";
|
||||||
|
example = "/run/secrets/kanidm-idm-admin-password";
|
||||||
|
type = types.path;
|
||||||
|
};
|
||||||
|
|
||||||
persons = mkOption {
|
persons = mkOption {
|
||||||
description = "Provisioning of kanidm persons";
|
description = mdDoc "Provisioning of kanidm persons";
|
||||||
default = {};
|
default = {};
|
||||||
type = types.attrsOf (types.submodule (personSubmod: let
|
type = types.attrsOf (types.submodule (personSubmod: let
|
||||||
inherit (personSubmod.module._args) name;
|
inherit (personSubmod.config._module.args) name;
|
||||||
updateArgs =
|
updateArgs =
|
||||||
["--displayname" personSubmod.config.displayName]
|
["--displayname" personSubmod.config.displayName]
|
||||||
++ optionals (personSubmod.config.legalName != null)
|
++ optionals (personSubmod.config.legalName != null)
|
||||||
|
@ -130,17 +309,17 @@ in {
|
||||||
then
|
then
|
||||||
''
|
''
|
||||||
if ! person_exists ${escapeShellArg name}; then
|
if ! person_exists ${escapeShellArg name}; then
|
||||||
kanidm person create ${escapeShellArg name} \
|
kanidm-idm person create ${escapeShellArg name} \
|
||||||
${escapeShellArg personSubmod.config.displayName}
|
${escapeShellArg personSubmod.config.displayName}
|
||||||
fi
|
fi
|
||||||
kanidm person update ${escapeShellArg name} ${escapeShellArgs updateArgs}
|
kanidm-idm person update ${escapeShellArg name} ${escapeShellArgs updateArgs}
|
||||||
''
|
''
|
||||||
+ flip concatMapStrings personSubmod.config.groups (group: ''
|
+ flip concatMapStrings personSubmod.config.groups (group: ''
|
||||||
kanidm group add-members ${escapeShellArg group} ${escapeShellArg name}
|
kanidm-idm group add-members ${escapeShellArg group} ${escapeShellArg name}
|
||||||
'')
|
'')
|
||||||
else ''
|
else ''
|
||||||
if oauth2_system_exists ${escapeShellArg name}; then
|
if person_exists ${escapeShellArg name}; then
|
||||||
kanidm group delete ${escapeShellArg name}
|
kanidm-idm person delete ${escapeShellArg name}
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
@ -148,27 +327,27 @@ in {
|
||||||
present = mkPresentOption "person";
|
present = mkPresentOption "person";
|
||||||
|
|
||||||
displayName = mkOption {
|
displayName = mkOption {
|
||||||
description = "Display name";
|
description = mdDoc "Display name";
|
||||||
type = types.str;
|
type = types.str;
|
||||||
example = "My User";
|
example = "My User";
|
||||||
};
|
};
|
||||||
|
|
||||||
legalName = mkOption {
|
legalName = mkOption {
|
||||||
description = "Full legal name";
|
description = mdDoc "Full legal name";
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
example = "Jane Doe";
|
example = "Jane Doe";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
mailAddresses = mkOption {
|
mailAddresses = mkOption {
|
||||||
description = "Mail addresses. First given address is considered the primary address.";
|
description = mdDoc "Mail addresses. First given address is considered the primary address.";
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
example = ["jane.doe@example.com"];
|
example = ["jane.doe@example.com"];
|
||||||
default = [];
|
default = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
groups = mkOption {
|
groups = mkOption {
|
||||||
description = "List of kanidm groups to which this user belongs.";
|
description = mdDoc "List of kanidm groups to which this user belongs.";
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
default = [];
|
||||||
};
|
};
|
||||||
|
@ -177,22 +356,22 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
groups = mkOption {
|
groups = mkOption {
|
||||||
description = "Provisioning of kanidm groups";
|
description = mdDoc "Provisioning of kanidm groups";
|
||||||
default = {};
|
default = {};
|
||||||
type = types.attrsOf (types.submodule (groupSubmod: let
|
type = types.attrsOf (types.submodule (groupSubmod: let
|
||||||
inherit (groupSubmod.module._args) name;
|
inherit (groupSubmod.config._module.args) name;
|
||||||
in {
|
in {
|
||||||
options = {
|
options = {
|
||||||
_script = mkScript (
|
_script = mkScript (
|
||||||
if groupSubmod.config.present
|
if groupSubmod.config.present
|
||||||
then ''
|
then ''
|
||||||
if ! group_exists ${escapeShellArg name}; then
|
if ! group_exists ${escapeShellArg name}; then
|
||||||
kanidm group create ${escapeShellArg name}
|
kanidm-admin group create ${escapeShellArg name}
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
else ''
|
else ''
|
||||||
if group_exists ${escapeShellArg name}; then
|
if group_exists ${escapeShellArg name}; then
|
||||||
kanidm group delete ${escapeShellArg name}
|
kanidm-admin group delete ${escapeShellArg name}
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
@ -203,10 +382,10 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
systems.oauth2 = mkOption {
|
systems.oauth2 = mkOption {
|
||||||
description = "Provisioning of oauth2 systems";
|
description = mdDoc "Provisioning of oauth2 systems";
|
||||||
default = {};
|
default = {};
|
||||||
type = types.attrsOf (types.submodule (oauth2Submod: let
|
type = types.attrsOf (types.submodule (oauth2Submod: let
|
||||||
inherit (oauth2Submod.module._args) name;
|
inherit (oauth2Submod.config._module.args) name;
|
||||||
in {
|
in {
|
||||||
options = {
|
options = {
|
||||||
_script = mkScript (
|
_script = mkScript (
|
||||||
|
@ -214,7 +393,7 @@ in {
|
||||||
then
|
then
|
||||||
''
|
''
|
||||||
if ! oauth2_system_exists ${escapeShellArg name}; then
|
if ! oauth2_system_exists ${escapeShellArg name}; then
|
||||||
kanidm system oauth2 create \
|
kanidm-admin system oauth2 create \
|
||||||
${escapeShellArg name} \
|
${escapeShellArg name} \
|
||||||
${escapeShellArg oauth2Submod.config.displayName} \
|
${escapeShellArg oauth2Submod.config.displayName} \
|
||||||
${escapeShellArg oauth2Submod.config.originUrl}
|
${escapeShellArg oauth2Submod.config.originUrl}
|
||||||
|
@ -222,16 +401,16 @@ in {
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
+ concatLines (flip mapAttrsToList oauth2Submod.config.scopeMaps (group: scopes: ''
|
+ concatLines (flip mapAttrsToList oauth2Submod.config.scopeMaps (group: scopes: ''
|
||||||
kanidm system oauth2 update-scope-map ${escapeShellArg name} \
|
kanidm-admin system oauth2 update-scope-map ${escapeShellArg name} \
|
||||||
${escapeShellArg group} ${escapeShellArgs scopes}
|
${escapeShellArg group} ${escapeShellArgs scopes}
|
||||||
''))
|
''))
|
||||||
+ concatLines (flip mapAttrsToList oauth2Submod.config.supplementaryScopeMaps (group: scopes: ''
|
+ concatLines (flip mapAttrsToList oauth2Submod.config.supplementaryScopeMaps (group: scopes: ''
|
||||||
kanidm system oauth2 update-sup-scope-map ${escapeShellArg name} \
|
kanidm-admin system oauth2 update-sup-scope-map ${escapeShellArg name} \
|
||||||
${escapeShellArg group} ${escapeShellArgs scopes}
|
${escapeShellArg group} ${escapeShellArgs scopes}
|
||||||
''))
|
''))
|
||||||
else ''
|
else ''
|
||||||
if oauth2_system_exists ${escapeShellArg name}; then
|
if oauth2_system_exists ${escapeShellArg name}; then
|
||||||
kanidm group delete ${escapeShellArg name}
|
kanidm-admin system oauth2 delete ${escapeShellArg name}
|
||||||
fi
|
fi
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
@ -239,33 +418,33 @@ in {
|
||||||
present = mkPresentOption "oauth2 system";
|
present = mkPresentOption "oauth2 system";
|
||||||
|
|
||||||
displayName = mkOption {
|
displayName = mkOption {
|
||||||
description = "Display name";
|
description = mdDoc "Display name";
|
||||||
type = types.str;
|
type = types.str;
|
||||||
example = "Some Service";
|
example = "Some Service";
|
||||||
};
|
};
|
||||||
|
|
||||||
originUrl = mkOption {
|
originUrl = 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!";
|
description = mdDoc "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.str;
|
type = types.str;
|
||||||
example = "https://someservice.example.com/";
|
example = "https://someservice.example.com/";
|
||||||
};
|
};
|
||||||
|
|
||||||
basicSecretFile = mkOption {
|
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!";
|
description = mdDoc "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;
|
type = types.nullOr types.path;
|
||||||
example = "/run/secrets/some-oauth2-basic-secret";
|
example = "/run/secrets/some-oauth2-basic-secret";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
scopeMaps = mkOption {
|
scopeMaps = mkOption {
|
||||||
description = "Maps kanidm groups to provided scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
description = mdDoc "Maps kanidm groups to provided scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
||||||
type = types.attrsOf types.str;
|
type = types.attrsOf (types.listOf types.str);
|
||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
supplementaryScopeMaps = mkOption {
|
supplementaryScopeMaps = mkOption {
|
||||||
description = "Maps kanidm groups to provided supplementary scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
description = mdDoc "Maps kanidm groups to provided supplementary scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
||||||
type = types.attrsOf types.str;
|
type = types.attrsOf (types.listOf types.str);
|
||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -273,47 +452,371 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf (cfg.enableServer && cfg.provision.enable) {
|
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"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
default = {};
|
||||||
|
description = mdDoc ''
|
||||||
|
Settings for Kanidm, see
|
||||||
|
[the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md)
|
||||||
|
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://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = mdDoc ''
|
||||||
|
Configure Kanidm unix daemon.
|
||||||
|
See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon)
|
||||||
|
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
|
||||||
|
for possible values.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
|
||||||
assertions =
|
assertions =
|
||||||
flip mapAttrsToList cfg.provision.persons (person: personCfg: let
|
[
|
||||||
|
{
|
||||||
|
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!isStorePath cfg.serverSettings.tls_chain);
|
||||||
|
message = ''
|
||||||
|
<option>services.kanidm.serverSettings.tls_chain</option> 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 = ''
|
||||||
|
<option>services.kanidm.serverSettings.tls_key</option> 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 = ''
|
||||||
|
<option>services.kanidm.clientSettings</option> needs to be configured
|
||||||
|
if the client is enabled.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
|
||||||
|
message = ''
|
||||||
|
<option>services.kanidm.clientSettings</option> 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 = ''
|
||||||
|
<option>services.kanidm.serverSettings.domain</option> 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 = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.provision.enable -> cfg.enableClient;
|
||||||
|
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableClient</option> to be able to use the kanidm client locally for provisioning.";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ flip mapAttrsToList cfg.provision.persons (person: personCfg: let
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) personCfg.groups;
|
unknownGroups = subtractLists (attrNames cfg.provision.groups) personCfg.groups;
|
||||||
in {
|
in {
|
||||||
assertion = unknownGroups == [];
|
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
||||||
message = "kanidm: provision.persons.${person}.groups: Refers to unknown groups: ${unknownGroups}";
|
message = "kanidm: provision.persons.${person}.groups: Refers to unknown groups: ${unknownGroups}";
|
||||||
})
|
})
|
||||||
+ concatLists (flip mapAttrsToList cfg.provision.systems.oauth2 (oauth2: oauth2Cfg: [
|
++ concatLists (flip mapAttrsToList cfg.provision.systems.oauth2 (oauth2: oauth2Cfg: [
|
||||||
|
{
|
||||||
|
assertion = (cfg.enableServer && cfg.provision.enable) -> hasInfix "://" oauth2Cfg.originUrl;
|
||||||
|
message = "kanidm: provision.systems.oauth2.${oauth2}.originUrl: Missing a schema like 'https://': ${oauth2Cfg.originUrl}";
|
||||||
|
}
|
||||||
(let
|
(let
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.scopeMaps);
|
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.scopeMaps);
|
||||||
in {
|
in {
|
||||||
assertion = unknownGroups == [];
|
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
||||||
message = "kanidm: provision.systems.oauth2.${oauth2}.scopeMaps: Refers to unknown groups: ${unknownGroups}";
|
message = "kanidm: provision.systems.oauth2.${oauth2}.scopeMaps: Refers to unknown groups: ${unknownGroups}";
|
||||||
})
|
})
|
||||||
(let
|
(let
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.supplementaryScopeMaps);
|
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.supplementaryScopeMaps);
|
||||||
in {
|
in {
|
||||||
assertion = unknownGroups == [];
|
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
||||||
message = "kanidm: provision.systems.oauth2.${oauth2}.supplementaryScopeMaps: Refers to unknown groups: ${unknownGroups}";
|
message = "kanidm: provision.systems.oauth2.${oauth2}.supplementaryScopeMaps: Refers to unknown groups: ${unknownGroups}";
|
||||||
})
|
})
|
||||||
]));
|
]));
|
||||||
|
|
||||||
systemd.services.kanidm = {
|
environment.systemPackages = mkIf cfg.enableClient [cfg.package];
|
||||||
serviceConfig.ExecStartPost =
|
|
||||||
[provisioningScript]
|
|
||||||
# Only the restarter runs with elevated privileges
|
|
||||||
++ optional (cfg.provision.systems.oauth2 != {}) "+${restarterScript}";
|
|
||||||
|
|
||||||
preStart = let
|
systemd.services.kanidm = mkIf cfg.enableServer {
|
||||||
mappingsJson = pkgs.writeText "mappings.json" (builtins.toJSON {
|
description = "kanidm identity management daemon";
|
||||||
account_secrets.admin = cfg.provision.adminPasswordFile;
|
wantedBy = ["multi-user.target"];
|
||||||
oauth2_basic_secrets = mapAttrs (_: x: v.basicSecretFile) cfg.provision.systems.oauth2;
|
after = ["network.target"];
|
||||||
});
|
serviceConfig = mkMerge [
|
||||||
in ''
|
# Merge paths and ignore existing prefixes needs to sidestep mkMerge
|
||||||
if ! test -e ${escapeShellArg cfg.serverSettings.db_path}; then
|
(defaultServiceConfig
|
||||||
touch "$STATE_DIRECTORY/.first_startup"
|
// {
|
||||||
else
|
BindReadOnlyPaths = mergePaths (
|
||||||
${getExe pkgs.kanidm-secret-manipulator} ${escapeShellArg cfg.serverSettings.db_path} ${tokenMappings}
|
defaultServiceConfig.BindReadOnlyPaths
|
||||||
fi
|
++ certPaths
|
||||||
'';
|
# If provisioning is enabled, we need access to the client config to use the kanidm cli,
|
||||||
|
# and to the installed system certificates.
|
||||||
|
++ optionals cfg.provision.enable [
|
||||||
|
"-/etc/ssl/certs"
|
||||||
|
"-/etc/static/ssl/certs"
|
||||||
|
"-/etc/kanidm"
|
||||||
|
"-/etc/static/kanidm"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StateDirectory = "kanidm";
|
||||||
|
StateDirectoryMode = "0700";
|
||||||
|
RuntimeDirectory = "kanidmd";
|
||||||
|
ExecStartPre = mkIf cfg.provision.enable [preStartScript];
|
||||||
|
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
|
||||||
|
ExecStartPost =
|
||||||
|
mkIf cfg.provision.enable
|
||||||
|
[
|
||||||
|
postStartScript
|
||||||
|
# Only the restarter runs with elevated privileges
|
||||||
|
"+${restarterScript}"
|
||||||
|
];
|
||||||
|
User = "kanidm";
|
||||||
|
Group = "kanidm";
|
||||||
|
|
||||||
|
BindPaths = [
|
||||||
|
# To create the socket
|
||||||
|
"/run/kanidmd:/run/kanidmd"
|
||||||
|
"/run/dbus/system_bus_socket"
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,16 +65,11 @@ in {
|
||||||
group = "influxdb2";
|
group = "influxdb2";
|
||||||
};
|
};
|
||||||
|
|
||||||
services.influxdb2.provision.ensureApiTokens = [
|
services.influxdb2.provision.organization.servers.auths."telegraf (${config.node.name})" = {
|
||||||
{
|
|
||||||
name = "telegraf (${config.node.name})";
|
|
||||||
org = "servers";
|
|
||||||
user = "admin";
|
|
||||||
readBuckets = ["telegraf"];
|
readBuckets = ["telegraf"];
|
||||||
writeBuckets = ["telegraf"];
|
writeBuckets = ["telegraf"];
|
||||||
tokenFile = nodes.${cfg.influxdb2.node}.config.age.secrets."telegraf-influxdb-token-${config.node.name}".path;
|
tokenFile = nodes.${cfg.influxdb2.node}.config.age.secrets."telegraf-influxdb-token-${config.node.name}".path;
|
||||||
}
|
};
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
age.secrets.telegraf-influxdb-token = {
|
age.secrets.telegraf-influxdb-token = {
|
||||||
|
|
|
@ -47,6 +47,6 @@ in {
|
||||||
networking.providedDomains = mergeFromOthers ["networking" "providedDomains"];
|
networking.providedDomains = mergeFromOthers ["networking" "providedDomains"];
|
||||||
services.nginx.upstreams = mergeFromOthers ["services" "nginx" "upstreams"];
|
services.nginx.upstreams = mergeFromOthers ["services" "nginx" "upstreams"];
|
||||||
services.nginx.virtualHosts = mergeFromOthers ["services" "nginx" "virtualHosts"];
|
services.nginx.virtualHosts = mergeFromOthers ["services" "nginx" "virtualHosts"];
|
||||||
services.influxdb2.provision.ensureApiTokens = mergeFromOthers ["services" "influxdb2" "provision" "ensureApiTokens"];
|
services.influxdb2.provision.organizations = mergeFromOthers ["services" "influxdb2" "organizations"];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
owner = "oddlama";
|
owner = "oddlama";
|
||||||
repo = "kanidm-secret-manipulator";
|
repo = "kanidm-secret-manipulator";
|
||||||
rev = "v${version}";
|
rev = "v${version}";
|
||||||
hash = "sha256-mLOTnOsbUozYRDBXVYtIrUE5jDXEqW8HMO57qWsp1Go=";
|
hash = "sha256-Hn/143YJ0rn9AihuI/wsDlqtnGi/LBzbfdMNTukc34c=";
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-L//ZtfbOxV6Hf5x5tLAQ52MChSclzJlhI7sZKqvByMo=";
|
cargoHash = "sha256-L//ZtfbOxV6Hf5x5tLAQ52MChSclzJlhI7sZKqvByMo=";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue