From 522de920bbe88f0c20b78629db2b139789313ab5 Mon Sep 17 00:00:00 2001 From: oddlama Date: Sat, 26 Aug 2023 20:25:38 +0200 Subject: [PATCH] feat: switch to upstreamed influxdb2 provisioning, add kanidm provisioning module --- README.md | 2 +- flake.lock | 48 +- hosts/ward/microvms/grafana.nix | 15 +- hosts/ward/microvms/immich.nix | 42 ++ hosts/ward/microvms/influxdb.nix | 12 +- modules/default.nix | 4 +- modules/meta/influxdb2.nix | 1074 --------------------------- modules/meta/kanidm.nix | 885 +++++++++++++++++----- modules/meta/telegraf.nix | 15 +- modules/repo/distributed-config.nix | 2 +- pkgs/kanidm-secret-manipulator.nix | 2 +- 11 files changed, 776 insertions(+), 1325 deletions(-) create mode 100644 hosts/ward/microvms/immich.nix delete mode 100644 modules/meta/influxdb2.nix diff --git a/README.md b/README.md index 70f73a8..6fa8f6f 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ ```bash # Recover admin account kanidmd recover-account admin -> AhNeQgKkwwEHZ85dxj1GPjx58vWsBU8QsvKSyYwUL7bz57bp +> FrEELN4tfyVbUAfhGeuUyZyaKk8cbpFufuDwyCPhY3xhb3X2 # Login with recovered root account kanidm login --name admin # Generate new credentials for idm_admin account diff --git a/flake.lock b/flake.lock index de105e6..c9b68d0 100644 --- a/flake.lock +++ b/flake.lock @@ -47,11 +47,11 @@ ] }, "locked": { - "lastModified": 1691966209, - "narHash": "sha256-0L2vP/QEi8W1s9dViB0dEgKRXa7vjPdyxvDD1oyZbwA=", + "lastModified": 1692783612, + "narHash": "sha256-Mz1xv45Rjzet1D2bMGKapgw1JCHaD60dBs4sE6Dz2+A=", "owner": "oddlama", "repo": "agenix-rekey", - "rev": "7e8f11a3cf6786477c420f2166a0c713bb706941", + "rev": "52695865488742e0b34a56111cd40e229b3ab90a", "type": "github" }, "original": { @@ -159,11 +159,11 @@ ] }, "locked": { - "lastModified": 1691999995, - "narHash": "sha256-8DyiH3zEdouwNhW68BkHrfoDYX9Cf1So6u8mCWN0iIo=", + "lastModified": 1692199161, + "narHash": "sha256-GqKApvQ1JCf5DzH/Q+P4nwuHb6MaQGaWTu41lYzveF4=", "owner": "nix-community", "repo": "disko", - "rev": "6388d2859c91adab847b4922b726f61920074494", + "rev": "4eed2457b053c4bbad7d90d2b3a1d539c2c9009c", "type": "github" }, "original": { @@ -301,11 +301,11 @@ "systems": "systems_3" }, "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", "owner": "numtide", "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", "type": "github" }, "original": { @@ -364,11 +364,11 @@ ] }, "locked": { - "lastModified": 1692131549, - "narHash": "sha256-MFjI8NL63/6HjMZpvJgnB/Pgg2dht22t45jOYtipZig=", + "lastModified": 1692763155, + "narHash": "sha256-qMrGKZ8c/q/mHO3ZdrcBPwiVVXPLLgXjY98Ejqb5kAA=", "owner": "nix-community", "repo": "home-manager", - "rev": "75cfe974e2ca05a61b66768674032b4c079e55d4", + "rev": "6a20e40acaebf067da682661aa67da8b36812606", "type": "github" }, "original": { @@ -414,11 +414,11 @@ ] }, "locked": { - "lastModified": 1691325831, - "narHash": "sha256-/S1A8FpFE6yiIzFIAYTQCSn9uqOUziu92iRTokI0eiQ=", + "lastModified": 1692274616, + "narHash": "sha256-UttCk5/sl0lLrBVO9kpmtDlFXcI2UkyOaSp7+grLRRE=", "owner": "astro", "repo": "microvm.nix", - "rev": "d5c5bb4cebbd9f59b7ab81a4b36fea10b6016d38", + "rev": "a291d324915f26d1fd86443bd486089099e8b541", "type": "github" }, "original": { @@ -465,11 +465,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1691871742, - "narHash": "sha256-6yDNjfbAMpwzWL4y75fxs6beXHRANfYX8BNSPjYehck=", + "lastModified": 1692952286, + "narHash": "sha256-TsrtPv3+Q1KR0avZxpiJH+b6fX/R/hEQVHbjl1ebotY=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "430a56dd16fe583a812b2df44dca002acab2f4f6", + "rev": "817e297fc3352fadc15f2c5306909aa9192d7d97", "type": "github" }, "original": { @@ -501,11 +501,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1692084312, - "narHash": "sha256-Za++qKVK6ovjNL9poQZtLKRM/re663pxzbJ+9M4Pgwg=", + "lastModified": 1692913444, + "narHash": "sha256-1SvMQm2DwofNxXVtNWWtIcTh7GctEVrS/Xel/mdc6iY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8353344d3236d3fda429bb471c1ee008857d3b7c", + "rev": "18324978d632ffc55ef1d928e81630c620f4f447", "type": "github" }, "original": { @@ -588,11 +588,11 @@ "nixpkgs-stable": "nixpkgs-stable_2" }, "locked": { - "lastModified": 1691747570, - "narHash": "sha256-J3fnIwJtHVQ0tK2JMBv4oAmII+1mCdXdpeCxtIsrL2A=", + "lastModified": 1692274144, + "narHash": "sha256-BxTQuRUANQ81u8DJznQyPmRsg63t4Yc+0kcyq6OLz8s=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "c5ac3aa3324bd8aebe8622a3fc92eeb3975d317a", + "rev": "7e3517c03d46159fdbf8c0e5c97f82d5d4b0c8fa", "type": "github" }, "original": { diff --git a/hosts/ward/microvms/grafana.nix b/hosts/ward/microvms/grafana.nix index b017682..aa63d8f 100644 --- a/hosts/ward/microvms/grafana.nix +++ b/hosts/ward/microvms/grafana.nix @@ -36,16 +36,11 @@ in { group = "influxdb2"; }; - services.influxdb2.provision.ensureApiTokens = [ - { - name = "grafana servers:telegraf (${config.node.name})"; - org = "servers"; - user = "admin"; - readBuckets = ["telegraf"]; - writeBuckets = ["telegraf"]; - tokenFile = nodes.ward-influxdb.config.age.secrets."grafana-influxdb-token-${config.node.name}".path; - } - ]; + services.influxdb2.provision.organization.servers.auths."grafana servers:telegraf (${config.node.name})" = { + readBuckets = ["telegraf"]; + writeBuckets = ["telegraf"]; + tokenFile = nodes.ward-influxdb.config.age.secrets."grafana-influxdb-token-${config.node.name}".path; + }; }; nodes.sentinel = { diff --git a/hosts/ward/microvms/immich.nix b/hosts/ward/microvms/immich.nix new file mode 100644 index 0000000..504d3da --- /dev/null +++ b/hosts/ward/microvms/immich.nix @@ -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 +} diff --git a/hosts/ward/microvms/influxdb.nix b/hosts/ward/microvms/influxdb.nix index 7c4cc10..01038e7 100644 --- a/hosts/ward/microvms/influxdb.nix +++ b/hosts/ward/microvms/influxdb.nix @@ -82,17 +82,7 @@ in { passwordFile = config.age.secrets.influxdb-admin-password.path; tokenFile = config.age.secrets.influxdb-admin-token.path; }; - ensureOrganizations = [ - { - name = "servers"; - } - ]; - ensureBuckets = [ - { - name = "telegraf"; - org = "servers"; - } - ]; + organizations.servers.buckets.telegraf = {}; }; }; diff --git a/modules/default.nix b/modules/default.nix index d5dc0fe..358d69a 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,5 +1,5 @@ { - disabledModules = ["services/databases/influxdb2.nix"]; + disabledModules = ["services/security/kanidm.nix"]; imports = [ ../users/root @@ -19,7 +19,7 @@ ./config/users.nix ./config/xdg.nix - ./meta/influxdb2.nix + ./meta/kanidm.nix ./meta/microvms.nix ./meta/nginx.nix ./meta/oauth2-proxy.nix diff --git a/modules/meta/influxdb2.nix b/modules/meta/influxdb2.nix deleted file mode 100644 index ac71525..0000000 --- a/modules/meta/influxdb2.nix +++ /dev/null @@ -1,1074 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit - (lib) - concatMap - concatMapStrings - count - elem - escapeShellArg - escapeShellArgs - filter - flip - genAttrs - getExe - hasAttr - hasInfix - head - literalExpression - mkBefore - mkEnableOption - mkIf - mkOption - optional - optionals - optionalString - types - unique - ; - - format = pkgs.formats.json {}; - cfg = config.services.influxdb2; - configFile = format.generate "config.json" cfg.settings; - - # A helper utility to allow provisioning tokens with deterministic secrets - tokenManipulator = pkgs.buildGoModule rec { - pname = "influxdb-token-manipulator"; - version = "1.0.0"; - - src = pkgs.fetchFromGitHub { - owner = "oddlama"; - repo = "influxdb-token-manipulator"; - rev = "v${version}"; - hash = "sha256-yKIvDNwwFb2teU7JvI92ie61m39VtrOYdeUz0v8uU3E="; - }; - vendorHash = "sha256-zBZk7JbNILX18g9+2ukiESnFtnIVWhdN/J/MBhIITh8="; - - postPatch = '' - sed -i '/Add token secrets here/ r ${ - pkgs.writeText "influxdb-token-paths" (concatMapStrings - (x: ''"${x.id}": "${x.tokenFile}",''\n'') - (filter (x: x.tokenFile != null) cfg.provision.ensureApiTokens)) - }' main.go - ''; - - meta = with lib; { - description = "Utility program to manipulate influxdb api tokens for declarative setups"; - license = with licenses; [mit]; - maintainers = with maintainers; [oddlama]; - }; - }; - - provisioningScript = pkgs.writeShellScript "post-start-provision" ('' - set -euo pipefail - export INFLUX_HOST="http://"${escapeShellArg ( - if - ! hasAttr "http-bind-address" cfg.settings - || hasInfix "0.0.0.0" cfg.settings.http-bind-address - then "localhost:8086" - else cfg.settings.http-bind-address - )} - - # Wait for the influxdb server to come online - count=0 - while ! influx ping &>/dev/null; do - if [ "$count" -eq 300 ]; then - echo "Tried for 30 seconds, giving up..." - exit 1 - fi - - if ! kill -0 "$MAINPID"; then - echo "Main server died, giving up..." - exit 1 - fi - - sleep 0.1 - count=$((count++)) - done - - # Do the initial database setup. Pass /dev/null as configs-path to - # avoid saving the token as the active config. - if test -e "$STATE_DIRECTORY/.first_startup"; then - influx setup \ - --configs-path /dev/null \ - --org ${escapeShellArg cfg.provision.initialSetup.organization} \ - --bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \ - --username ${escapeShellArg cfg.provision.initialSetup.username} \ - --password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \ - --token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \ - --retention ${escapeShellArg cfg.provision.initialSetup.retention} \ - --force >/dev/null - - rm -f "$STATE_DIRECTORY/.first_startup" - fi - - export INFLUX_TOKEN=$(< "$CREDENTIALS_DIRECTORY/admin-token") - - ${concatMapStrings (x: x._script) cfg.provision.deleteApiTokens} - ${concatMapStrings (x: x._script) cfg.provision.deleteReplications} - ${concatMapStrings (x: x._script) cfg.provision.deleteRemotes} - ${concatMapStrings (x: x._script) cfg.provision.deleteUsers} - ${concatMapStrings (x: x._script) cfg.provision.deleteBuckets} - ${concatMapStrings (x: x._script) cfg.provision.deleteOrganizations} - - ${concatMapStrings (x: x._script) cfg.provision.ensureOrganizations} - ${concatMapStrings (x: x._script) cfg.provision.ensureBuckets} - ${concatMapStrings (x: x._script) cfg.provision.ensureUsers} - ${concatMapStrings (x: x._script) cfg.provision.ensureRemotes} - ${concatMapStrings (x: x._script) cfg.provision.ensureReplications} - ${concatMapStrings (x: x._script) cfg.provision.ensureApiTokens} - '' - + optionalString (cfg.provision.ensureApiTokens != []) '' - if [[ ''${any_tokens_created-0} == 1 ]]; then - echo "Created new tokens, queueing service restart so we can manipulate secrets" - touch "$STATE_DIRECTORY/.needs_restart" - fi - ''); - restarterScript = pkgs.writeShellScript "post-start-restarter" '' - set -euo pipefail - if test -e "$STATE_DIRECTORY/.needs_restart"; then - rm -f "$STATE_DIRECTORY/.needs_restart" - systemctl restart influxdb2 - fi - ''; -in { - options = { - services.influxdb2 = { - enable = mkEnableOption (lib.mdDoc "the influxdb2 server"); - - package = mkOption { - default = pkgs.influxdb2-server; - defaultText = literalExpression "pkgs.influxdb2"; - description = lib.mdDoc "influxdb2 derivation to use."; - type = types.package; - }; - - settings = mkOption { - default = {}; - description = lib.mdDoc ''configuration options for influxdb2, see for details.''; - inherit (format) type; - }; - - provision = { - enable = mkEnableOption "initial database setup and provisioning"; - - initialSetup = { - organization = mkOption { - type = types.str; - example = "main"; - description = "Primary organization name"; - }; - - bucket = mkOption { - type = types.str; - example = "example"; - description = "Primary bucket name"; - }; - - username = mkOption { - type = types.str; - default = "admin"; - description = "Primary username"; - }; - - retention = mkOption { - type = types.str; - default = "0"; - description = '' - The duration for which the bucket will retain data (0 is infinite). - Accepted units are `ns` (nanoseconds), `us` or `µs` (microseconds), `ms` (milliseconds), - `s` (seconds), `m` (minutes), `h` (hours), `d` (days) and `w` (weeks). - ''; - }; - - passwordFile = mkOption { - type = types.path; - description = "Password for primary user. Don't use a file from the nix store!"; - }; - - tokenFile = mkOption { - type = types.path; - description = "API Token to set for the admin user. Don't use a file from the nix store!"; - }; - }; - - deleteOrganizations = mkOption { - description = "List of organizations that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - name = mkOption { - description = "Name of the organization to delete."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx org list --json --name ${escapeShellArg config.name} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx org delete --id "$id" >/dev/null - echo "Deleted org name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - deleteBuckets = mkOption { - description = "List of buckets that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "The organization to which the bucket belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the bucket."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx bucket list --json --org ${escapeShellArg config.org} --name ${escapeShellArg config.name} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx bucket delete --id "$id" >/dev/null - echo "Deleted bucket org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - deleteUsers = mkOption { - description = "List of users that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - name = mkOption { - description = "Name of the user to delete."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx user list --json --name ${escapeShellArg config.name} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx user delete --id "$id" >/dev/null - echo "Deleted user name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - deleteRemotes = mkOption { - description = "List of remotes that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "The organization to which the remote belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the remote."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx remote list --json --org ${escapeShellArg config.org} --name ${escapeShellArg config.name} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx remote delete --id "$id" >/dev/null - echo "Deleted remote org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - deleteReplications = mkOption { - description = "List of replications that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "The organization to which the replication belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the replication."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx replication list --json --org ${escapeShellArg config.org} --name ${escapeShellArg config.name} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx replication delete --id "$id" >/dev/null - echo "Deleted replication org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - deleteApiTokens = mkOption { - description = "List of api tokens that should be deleted."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - id = mkOption { - description = "A unique identifier for this token. See `ensureApiTokens.*.name` for more information."; - readOnly = true; - default = builtins.substring 0 32 (builtins.hashString "sha256" "${config.user}:${config.org}:${config.name}"); - defaultText = ""; - type = types.str; - }; - - org = mkOption { - description = "The organization to which the api token belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the api token."; - type = types.str; - }; - - user = mkOption { - description = "The user to which the api token belongs."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = '' - if id=$( - influx auth list --json --org ${escapeShellArg config.org} 2>/dev/null \ - | jq -r '.[] | select(.description | contains("${config.id}")) | .id' - ) && [[ -n "$id" ]]; then - influx auth delete --id "$id" >/dev/null - echo "Deleted api token id="${escapeShellArg config.id} - fi - ''; - }; - }; - })); - }; - - ensureOrganizations = mkOption { - description = "List of organizations that should be created. Future changes to the name will not be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - name = mkOption { - description = "Name of the organization."; - type = types.str; - }; - - description = mkOption { - description = "Optional description for the organization."; - default = null; - type = types.nullOr types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = ["--name" config.name]; - updateArgs = optionals (config.description != null) [ - "--description" - config.description - ]; - createArgs = listArgs ++ updateArgs; - in '' - if id=$( - influx org list --json ${escapeShellArgs listArgs} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx org update --id "$id" ${escapeShellArgs updateArgs} >/dev/null - else - influx org create ${escapeShellArgs createArgs} >/dev/null - echo "Created org name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - ensureBuckets = mkOption { - description = "List of buckets that should be created. Future changes to the name or org will not be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "The organization the bucket belongs to."; - type = types.str; - }; - - name = mkOption { - description = "Name of the bucket."; - type = types.str; - }; - - description = mkOption { - description = "Optional description for the bucket."; - default = null; - type = types.nullOr types.str; - }; - - retention = mkOption { - type = types.str; - default = "0"; - description = '' - The duration for which the bucket will retain data (0 is infinite). - Accepted units are `ns` (nanoseconds), `us` or `µs` (microseconds), `ms` (milliseconds), - `s` (seconds), `m` (minutes), `h` (hours), `d` (days) and `w` (weeks). - ''; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = [ - "--org" - config.org - "--name" - config.name - ]; - updateArgs = - [ - "--retention" - config.retention - ] - ++ optionals (config.description != null) [ - "--description" - config.description - ]; - createArgs = listArgs ++ updateArgs; - in '' - if id=$( - influx bucket list --json ${escapeShellArgs listArgs} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx bucket update --id "$id" ${escapeShellArgs updateArgs} >/dev/null - else - influx bucket create ${escapeShellArgs createArgs} >/dev/null - echo "Created bucket org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - ensureUsers = mkOption { - description = "List of users that should be created. Future changes to the name or primary org will not be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "Primary organization to which the user will be added as a member."; - type = types.str; - }; - - name = mkOption { - description = "Name of the user."; - type = types.str; - }; - - passwordFile = mkOption { - description = "Password for the user. If unset, the user will not be able to log in until a password is set by an operator! Don't use a file from the nix store!"; - type = types.nullOr types.path; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = ["--name" config.name]; - createArgs = - listArgs - ++ [ - "--org" - config.org - ]; - in - '' - if id=$( - influx user list --json ${escapeShellArgs listArgs} 2>/dev/null \ - | jq -r ".[0].id" - ); then - true # No updateable args - else - influx user create ${escapeShellArgs createArgs} >/dev/null - echo "Created user name="${escapeShellArg config.name} - fi - '' - + optionalString (config.passwordFile != null) '' - influx user password ${escapeShellArgs listArgs} \ - --password "$(< ${escapeShellArg config.passwordFile})" >/dev/null - ''; - }; - }; - })); - }; - - ensureRemotes = mkOption { - description = "List of remotes that should be created. Future changes to the name, org or remoteOrg will not be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "Organization to which the remote belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the remote."; - type = types.str; - }; - - description = mkOption { - description = "Optional description for the remote."; - default = null; - type = types.nullOr types.str; - }; - - remoteUrl = mkOption { - description = "The url where the remote instance can be reached"; - type = types.str; - }; - - remoteOrg = mkOption { - description = '' - Corresponding remote organization. If this is used instead of `remoteOrgId`, - the remote organization id must be queried first which means the provided remote - token must have the `read-orgs` flag. - ''; - type = types.nullOr types.str; - default = null; - }; - - remoteOrgId = mkOption { - description = "Corresponding remote organization id."; - type = types.nullOr types.str; - default = null; - }; - - remoteTokenFile = mkOption { - type = types.path; - description = "API token used to authenticate with the remote."; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = [ - "--name" - config.name - "--org" - config.org - ]; - updateArgs = - ["--remote-url" config.remoteUrl] - ++ optionals (config.remoteOrgId != null) ["--remote-org-id" config.remoteOrgId] - ++ optionals (config.description != null) ["--description" config.description]; - createArgs = listArgs ++ updateArgs; - in - '' - if id=$( - influx remote list --json ${escapeShellArgs listArgs} 2>/dev/null \ - | jq -r ".[0].id" - ); then - influx remote update --id "$id" ${escapeShellArgs updateArgs} >/dev/null \ - --remote-api-token "$(< ${escapeShellArg config.remoteTokenFile})" - else - extraArgs=() - '' - + optionalString (config.remoteOrg != null) '' - remote_org_id=$( - influx org list --json \ - --host ${escapeShellArg config.remoteUrl} \ - --token "$(< ${escapeShellArg config.remoteTokenFile})" \ - --name ${escapeShellArg config.remoteOrg} \ - | jq -r ".[0].id" - ) - extraArgs+=("--remote-org-id" "$remote_org_id") - '' - + '' - influx remote create ${escapeShellArgs createArgs} >/dev/null \ - --remote-api-token "$(< ${escapeShellArg config.remoteTokenFile})" \ - "''${extraArgs[@]}" - echo "Created remote org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - ensureReplications = mkOption { - description = "List of replications that should be created. Future changes to name, org or buckets will not be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - org = mkOption { - description = "Organization to which the replication belongs."; - type = types.str; - }; - - name = mkOption { - description = "Name of the remote."; - type = types.str; - }; - - remote = mkOption { - description = "The remote to replicate to."; - type = types.str; - }; - - localBucket = mkOption { - description = "The local bucket to replicate from."; - type = types.str; - }; - - remoteBucket = mkOption { - description = "The remte bucket to replicate to."; - type = types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = [ - "--name" - config.name - "--org" - config.org - ]; - createArgs = - listArgs - ++ [ - "--remote-bucket" - config.remoteBucket - ]; - in '' - if id=$( - influx replication list --json ${escapeShellArgs listArgs} 2>/dev/null \ - | jq -r ".[0].id" - ); then - true # No updateable args - else - remote_id=$( - influx remote list --json --org ${escapeShellArg config.org} --name ${escapeShellArg config.remote} \ - | jq -r ".[0].id" - ) - local_bucket_id=$( - influx bucket list --json --org ${escapeShellArg config.org} --name ${escapeShellArg config.localBucket} \ - | jq -r ".[0].id" - ) - influx replication create ${escapeShellArgs createArgs} >/dev/null \ - --remote-id "$remote_id" \ - --local-bucket-id "$local_bucket_id" - echo "Created replication org="${escapeShellArg config.org}" name="${escapeShellArg config.name} - fi - ''; - }; - }; - })); - }; - - ensureApiTokens = mkOption { - description = "List of api tokens that should be created. Future changes to existing tokens cannot be reflected."; - default = []; - type = types.listOf (types.submodule ({config, ...}: { - options = { - id = mkOption { - description = "A unique identifier for this token. Since influx doesn't store names for tokens, this will be hashed and appended to the description to identify the token."; - readOnly = true; - default = builtins.substring 0 32 (builtins.hashString "sha256" "${config.user}:${config.org}:${config.name}"); - defaultText = ""; - type = types.str; - }; - - org = mkOption { - description = "Organization to which the token belongs."; - type = types.str; - }; - - name = mkOption { - description = "A name to identify this token. Not an actual influxdb attribute, but needed to calculate a stable id (see `id`)."; - type = types.str; - }; - - user = mkOption { - description = "The user to which the token belongs."; - type = types.str; - }; - - description = mkOption { - description = '' - Optional description for the api token. - Note that the actual token will always be created with a description regardless - of whether this is given or not. A unique suffix has to be appended to later identify - the token to track whether it has already been created. - ''; - default = null; - type = types.nullOr types.str; - }; - - tokenFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "The token value. If not given, influx will automatically generate one."; - }; - - operator = mkOption { - description = "Grants all permissions in all organizations."; - default = false; - type = types.bool; - }; - - allAccess = mkOption { - description = "Grants all permissions in the associated organization."; - default = false; - type = types.bool; - }; - - readPermissions = mkOption { - description = '' - The read permissions to include for this token. Access is usually granted only - for resources in the associated organization. - - Available permissions are `authorizations`, `buckets`, `dashboards`, - `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`, - `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`, - `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`. - - Refer to `influx auth create --help` for a full list with descriptions. - - `buckets` grants read access to all associated buckets. Use `readBuckets` to define - more granular access permissions. - ''; - default = []; - type = types.listOf types.str; - }; - - writePermissions = mkOption { - description = '' - The read permissions to include for this token. Access is usually granted only - for resources in the associated organization. - - Available permissions are `authorizations`, `buckets`, `dashboards`, - `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`, - `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`, - `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`. - - Refer to `influx auth create --help` for a full list with descriptions. - - `buckets` grants write access to all associated buckets. Use `writeBuckets` to define - more granular access permissions. - ''; - default = []; - type = types.listOf types.str; - }; - - readBuckets = mkOption { - description = "The organization's buckets which should be allowed to be read"; - default = []; - type = types.listOf types.str; - }; - - writeBuckets = mkOption { - description = "The organization's buckets which should be allowed to be written"; - default = []; - type = types.listOf types.str; - }; - - _script = mkOption { - internal = true; - readOnly = true; - type = types.str; - default = let - listArgs = [ - "--user" - config.user - "--org" - config.org - ]; - fullDescription = - "${config.name} - " - + optionalString (config.description != null) "${config.description} - " - + config.id; - createArgs = - listArgs - ++ ["--description" fullDescription] - ++ optional config.operator "--operator" - ++ optional config.allAccess "--all-access" - ++ map (x: "--read-${x}") config.readPermissions - ++ map (x: "--write-${x}") config.writePermissions; - in - '' - if id=$( - influx auth list --json --org ${escapeShellArg config.org} 2>/dev/null \ - | jq -r '.[] | select(.description | contains("${config.id}")) | .id' - ) && [[ -n "$id" ]]; then - true # No updateable args - else - declare -A bucketIds - '' - + flip concatMapStrings (unique (config.readBuckets ++ config.writeBuckets)) (bucket: '' - bucketIds[${escapeShellArg bucket}]=$( - influx bucket list --json --org ${escapeShellArg config.org} --name ${escapeShellArg bucket} \ - | jq -r ".[0].id" - ) - '') - + '' - extraArgs=( - ${flip concatMapStrings config.readBuckets (bucket: ''"--read-bucket" "''${bucketIds[${escapeShellArg bucket}]}"''\n'')} - ${flip concatMapStrings config.writeBuckets (bucket: ''"--write-bucket" "''${bucketIds[${escapeShellArg bucket}]}"''\n'')} - ) - influx auth create ${escapeShellArgs createArgs} >/dev/null "''${extraArgs[@]}" - echo "Created api token org="${escapeShellArg config.org}" user="${escapeShellArg config.user} - ${ - # Force restart to update tokens if necessary - optionalString (config.tokenFile != null) "any_tokens_created=1" - } - fi - ''; - }; - }; - })); - }; - }; - }; - }; - - config = mkIf cfg.enable { - assertions = let - validPermissions = flip genAttrs (x: true) [ - "authorizations" - "buckets" - "dashboards" - "orgs" - "tasks" - "telegrafs" - "users" - "variables" - "secrets" - "labels" - "views" - "documents" - "notificationRules" - "notificationEndpoints" - "checks" - "dbrp" - "annotations" - "sources" - "scrapers" - "notebooks" - "remotes" - "replications" - ]; - - knownOrgs = map (x: x.name) cfg.provision.ensureOrganizations; - knownRemotes = map (x: x.name) cfg.provision.ensureRemotes; - knownBucketsFor = org: map (x: x.name) (filter (x: x.org == org) cfg.provision.ensureBuckets); - in - [ - { - assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings); - message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd"; - } - ] - ++ flip concatMap cfg.provision.ensureBuckets (bucket: [ - { - assertion = elem bucket.org knownOrgs; - message = "The influxdb bucket '${bucket.name}' refers to an unknown organization '${bucket.org}'."; - } - ]) - ++ flip concatMap cfg.provision.ensureUsers (user: [ - { - assertion = elem user.org knownOrgs; - message = "The influxdb user '${user.name}' refers to an unknown organization '${user.org}'."; - } - ]) - ++ flip concatMap cfg.provision.ensureRemotes (remote: [ - { - assertion = (remote.remoteOrgId == null) != (remote.remoteOrg == null); - message = "The influxdb remote '${remote.name}' must specify exactly one of remoteOrgId or remoteOrg."; - } - { - assertion = elem remote.org knownOrgs; - message = "The influxdb remote '${remote.name}' refers to an unknown organization '${remote.org}'."; - } - ]) - ++ flip concatMap cfg.provision.ensureReplications (replication: [ - { - assertion = elem replication.remote knownRemotes; - message = "The influxdb replication '${replication.name}' refers to an unknown remote '${replication.remote}'."; - } - (let - remote = head (filter (x: x.name == replication.remote) cfg.provision.ensureRemotes); - in { - assertion = elem replication.localBucket (knownBucketsFor remote.org); - message = "The influxdb replication '${replication.name}' refers to an unknown bucket '${replication.localBucket}' in organization '${remote.org}'."; - }) - ]) - ++ flip concatMap cfg.provision.ensureApiTokens (apiToken: let - validBuckets = flip genAttrs (x: true) (knownBucketsFor apiToken.org); - in [ - { - assertion = elem apiToken.org knownOrgs; - message = "The influxdb apiToken '${apiToken.name}' refers to an unknown organization '${apiToken.org}'."; - } - { - assertion = - 1 - == count (x: x) [ - apiToken.operator - apiToken.allAccess - (apiToken.readPermissions - != [] - || apiToken.writePermissions != [] - || apiToken.readBuckets != [] - || apiToken.writeBuckets != []) - ]; - message = "The influxdb apiToken '${apiToken.name}' in organization '${apiToken.org}' uses mutually exclusive options. The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings."; - } - (let - unknownBuckets = filter (x: !hasAttr x validBuckets) apiToken.readBuckets; - in { - assertion = unknownBuckets == []; - message = "The influxdb apiToken '${apiToken.name}' refers to invalid buckets in readBuckets: ${toString unknownBuckets}"; - }) - (let - unknownBuckets = filter (x: !hasAttr x validBuckets) apiToken.writeBuckets; - in { - assertion = unknownBuckets == []; - message = "The influxdb apiToken '${apiToken.name}' refers to invalid buckets in writeBuckets: ${toString unknownBuckets}"; - }) - (let - unknownPerms = filter (x: !hasAttr x validPermissions) apiToken.readPermissions; - in { - assertion = unknownPerms == []; - message = "The influxdb apiToken '${apiToken.name}' refers to invalid read permissions: ${toString unknownPerms}"; - }) - (let - unknownPerms = filter (x: !hasAttr x validPermissions) apiToken.writePermissions; - in { - assertion = unknownPerms == []; - message = "The influxdb apiToken '${apiToken.name}' refers to invalid write permissions: ${toString unknownPerms}"; - }) - ]); - - systemd.services.influxdb2 = { - description = "InfluxDB is an open-source, distributed, time series database"; - documentation = ["https://docs.influxdata.com/influxdb/"]; - wantedBy = ["multi-user.target"]; - after = ["network.target"]; - environment = { - INFLUXD_CONFIG_PATH = configFile; - ZONEINFO = "${pkgs.tzdata}/share/zoneinfo"; - }; - serviceConfig = { - ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine"; - StateDirectory = "influxdb2"; - User = "influxdb2"; - Group = "influxdb2"; - CapabilityBoundingSet = ""; - SystemCallFilter = "@system-service"; - LimitNOFILE = 65536; - KillMode = "control-group"; - Restart = "on-failure"; - LoadCredential = [ - "admin-password:${cfg.provision.initialSetup.passwordFile}" - "admin-token:${cfg.provision.initialSetup.tokenFile}" - ]; - - ExecStartPost = mkIf cfg.provision.enable ( - [provisioningScript] - ++ - # Only the restarter runs with elevated privileges - optional (cfg.provision.ensureApiTokens != []) "+${restarterScript}" - ); - }; - - path = [ - pkgs.influxdb2-cli - pkgs.jq - ]; - - # Mark if this is the first startup so postStart can do the initial setup - preStart = mkIf cfg.provision.enable '' - if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then - touch "$STATE_DIRECTORY/.first_startup" - else - # Manipulate provisioned api tokens if necessary - ${tokenManipulator}/bin/influxdb-token-manipulator "$STATE_DIRECTORY/influxd.bolt" - fi - ''; - }; - - users.extraUsers.influxdb2 = { - isSystemUser = true; - group = "influxdb2"; - }; - - users.extraGroups.influxdb2 = {}; - }; - - meta.maintainers = with lib.maintainers; [nickcao oddlama]; -} diff --git a/modules/meta/kanidm.nix b/modules/meta/kanidm.nix index f63a0af..78f0d6f 100644 --- a/modules/meta/kanidm.nix +++ b/modules/meta/kanidm.nix @@ -1,32 +1,113 @@ { config, lib, + options, + pkgs, ... }: let inherit (lib) all + any attrNames attrValues concatLines concatLists concatMap concatMapStrings + converge elem escapeShellArg + escapeShellArgs + filter + filterAttrsRecursive flip + foldl' + getExe + hasInfix + hasPrefix + isStorePath + mapAttrs mapAttrsToList + mdDoc + mkEnableOption + mkForce + mkIf + mkMerge mkOption + mkPackageOptionMD + optional optionals subtractLists types ; 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 = "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; default = true; }; @@ -39,14 +120,39 @@ 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 # Wait for the kanidm server to come online count=0 while ! test -e /run/kanidmd/sock; do - if [ "$count" -eq 300 ]; then - echo "Tried for 30 seconds, giving up..." + if [ "$count" -eq 600 ]; then + echo "Tried for 60 seconds, giving up..." exit 1 fi if ! kill -0 "$MAINPID"; then @@ -60,29 +166,97 @@ # If this is the first start, we login this time by recovering the admin account # and force a restart afterwards to rewrite the password. 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 rm -f "$STATE_DIRECTORY/.first_startup" else # 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 - ${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() { - [[ -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() { - [[ -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() { - [[ -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)} @@ -94,226 +268,555 @@ touch "$STATE_DIRECTORY/.needs_restart" 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 { - options.services.kanidm.provision = { - enable = mkEnableOption "provisioning of systems (oauth2), groups and users"; + options.services.kanidm = { + enableClient = mkEnableOption (mdDoc "the Kanidm client"); + enableServer = mkEnableOption (mdDoc "the Kanidm server"); + enablePam = mkEnableOption (mdDoc "the Kanidm PAM and NSS integration"); - 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"; - type = types.path; + package = mkPackageOptionMD pkgs "kanidm" {}; + + provision = { + enable = mkEnableOption "provisioning of systems (oauth2), groups and users"; + + adminPasswordFile = mkOption { + 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"; + 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 { + description = mdDoc "Provisioning of kanidm persons"; + default = {}; + type = types.attrsOf (types.submodule (personSubmod: let + inherit (personSubmod.config._module.args) name; + updateArgs = + ["--displayname" personSubmod.config.displayName] + ++ optionals (personSubmod.config.legalName != null) + ["--legalname" personSubmod.config.legalName] + # mail addresses + ++ concatMap (addr: ["--mail" addr]) personSubmod.config.mailAddresses; + in { + options = { + _script = mkScript ( + if personSubmod.config.present + then + '' + if ! person_exists ${escapeShellArg name}; then + kanidm-idm person create ${escapeShellArg name} \ + ${escapeShellArg personSubmod.config.displayName} + fi + kanidm-idm person update ${escapeShellArg name} ${escapeShellArgs updateArgs} + '' + + flip concatMapStrings personSubmod.config.groups (group: '' + kanidm-idm group add-members ${escapeShellArg group} ${escapeShellArg name} + '') + else '' + if person_exists ${escapeShellArg name}; then + kanidm-idm person delete ${escapeShellArg name} + fi + '' + ); + + present = mkPresentOption "person"; + + displayName = mkOption { + description = mdDoc "Display name"; + type = types.str; + example = "My User"; + }; + + legalName = mkOption { + description = mdDoc "Full legal name"; + type = types.nullOr types.str; + example = "Jane Doe"; + default = null; + }; + + mailAddresses = mkOption { + description = mdDoc "Mail addresses. First given address is considered the primary address."; + type = types.listOf types.str; + example = ["jane.doe@example.com"]; + default = []; + }; + + groups = mkOption { + description = mdDoc "List of kanidm groups to which this user belongs."; + type = types.listOf types.str; + default = []; + }; + }; + })); + }; + + groups = mkOption { + description = mdDoc "Provisioning of kanidm groups"; + default = {}; + type = types.attrsOf (types.submodule (groupSubmod: let + inherit (groupSubmod.config._module.args) name; + in { + options = { + _script = mkScript ( + if groupSubmod.config.present + then '' + if ! group_exists ${escapeShellArg name}; then + kanidm-admin group create ${escapeShellArg name} + fi + '' + else '' + if group_exists ${escapeShellArg name}; then + kanidm-admin group delete ${escapeShellArg name} + fi + '' + ); + + present = mkPresentOption "group"; + }; + })); + }; + + systems.oauth2 = mkOption { + description = mdDoc "Provisioning of oauth2 systems"; + default = {}; + type = types.attrsOf (types.submodule (oauth2Submod: let + inherit (oauth2Submod.config._module.args) name; + in { + options = { + _script = mkScript ( + if oauth2Submod.config.present + then + '' + if ! oauth2_system_exists ${escapeShellArg name}; then + kanidm-admin system oauth2 create \ + ${escapeShellArg name} \ + ${escapeShellArg oauth2Submod.config.displayName} \ + ${escapeShellArg oauth2Submod.config.originUrl} + needs_rewrite=1 + fi + '' + + concatLines (flip mapAttrsToList oauth2Submod.config.scopeMaps (group: scopes: '' + kanidm-admin system oauth2 update-scope-map ${escapeShellArg name} \ + ${escapeShellArg group} ${escapeShellArgs scopes} + '')) + + concatLines (flip mapAttrsToList oauth2Submod.config.supplementaryScopeMaps (group: scopes: '' + kanidm-admin system oauth2 update-sup-scope-map ${escapeShellArg name} \ + ${escapeShellArg group} ${escapeShellArgs scopes} + '')) + else '' + if oauth2_system_exists ${escapeShellArg name}; then + kanidm-admin system oauth2 delete ${escapeShellArg name} + fi + '' + ); + + present = mkPresentOption "oauth2 system"; + + displayName = mkOption { + description = mdDoc "Display name"; + type = types.str; + example = "Some Service"; + }; + + originUrl = mkOption { + 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; + example = "https://someservice.example.com/"; + }; + + basicSecretFile = mkOption { + 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; + example = "/run/secrets/some-oauth2-basic-secret"; + default = null; + }; + + scopeMaps = mkOption { + 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.listOf types.str); + default = {}; + }; + + supplementaryScopeMaps = mkOption { + 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.listOf types.str); + default = {}; + }; + }; + })); + }; }; - persons = mkOption { - description = "Provisioning of kanidm persons"; - default = {}; - type = types.attrsOf (types.submodule (personSubmod: let - inherit (personSubmod.module._args) name; - updateArgs = - ["--displayname" personSubmod.config.displayName] - ++ optionals (personSubmod.config.legalName != null) - ["--legalname" personSubmod.config.legalName] - # mail addresses - ++ concatMap (addr: ["--mail" addr]) personSubmod.config.mailAddresses; - in { + serverSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { - _script = mkScript ( - if personSubmod.config.present - then - '' - if ! person_exists ${escapeShellArg name}; then - kanidm person create ${escapeShellArg name} \ - ${escapeShellArg personSubmod.config.displayName} - fi - kanidm person update ${escapeShellArg name} ${escapeShellArgs updateArgs} - '' - + flip concatMapStrings personSubmod.config.groups (group: '' - kanidm group add-members ${escapeShellArg group} ${escapeShellArg name} - '') - else '' - if oauth2_system_exists ${escapeShellArg name}; then - kanidm group delete ${escapeShellArg name} - fi - '' - ); - - present = mkPresentOption "person"; - - displayName = mkOption { - description = "Display name"; + bindaddress = mkOption { + description = mdDoc "Address/port combination the webserver binds to."; + example = "[::1]:8443"; type = types.str; - example = "My User"; }; - - legalName = mkOption { - description = "Full legal name"; + # 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; - example = "Jane Doe"; + }; + 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; }; - - mailAddresses = mkOption { - description = "Mail addresses. First given address is considered the primary address."; - type = types.listOf types.str; - example = ["jane.doe@example.com"]; - default = []; + db_path = mkOption { + description = mdDoc "Path to Kanidm database."; + default = "/var/lib/kanidm/kanidm.db"; + readOnly = true; + type = types.path; }; - - groups = mkOption { - description = "List of kanidm groups to which this user belongs."; - type = types.listOf types.str; - default = []; + 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. + ''; }; - groups = mkOption { - description = "Provisioning of kanidm groups"; - default = {}; - type = types.attrsOf (types.submodule (groupSubmod: let - inherit (groupSubmod.module._args) name; - in { - options = { - _script = mkScript ( - if groupSubmod.config.present - then '' - if ! group_exists ${escapeShellArg name}; then - kanidm group create ${escapeShellArg name} - fi - '' - else '' - if group_exists ${escapeShellArg name}; then - kanidm group delete ${escapeShellArg name} - fi - '' - ); + clientSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; - present = mkPresentOption "group"; + 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. + ''; }; - systems.oauth2 = mkOption { - description = "Provisioning of oauth2 systems"; - default = {}; - type = types.attrsOf (types.submodule (oauth2Submod: let - inherit (oauth2Submod.module._args) name; - in { - options = { - _script = mkScript ( - if oauth2Submod.config.present - then - '' - if ! oauth2_system_exists ${escapeShellArg name}; then - kanidm system oauth2 create \ - ${escapeShellArg name} \ - ${escapeShellArg oauth2Submod.config.displayName} \ - ${escapeShellArg oauth2Submod.config.originUrl} - needs_rewrite=1 - fi - '' - + concatLines (flip mapAttrsToList oauth2Submod.config.scopeMaps (group: scopes: '' - kanidm system oauth2 update-scope-map ${escapeShellArg name} \ - ${escapeShellArg group} ${escapeShellArgs scopes} - '')) - + concatLines (flip mapAttrsToList oauth2Submod.config.supplementaryScopeMaps (group: scopes: '' - kanidm system oauth2 update-sup-scope-map ${escapeShellArg name} \ - ${escapeShellArg group} ${escapeShellArgs scopes} - '')) - else '' - if oauth2_system_exists ${escapeShellArg name}; then - kanidm group delete ${escapeShellArg name} - fi - '' - ); + unixSettings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; - present = mkPresentOption "oauth2 system"; - - displayName = mkOption { - description = "Display name"; - type = types.str; - example = "Some Service"; - }; - - 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!"; - type = types.str; - example = "https://someservice.example.com/"; - }; - - 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; - }; - - 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."; - type = types.attrsOf types.str; - default = {}; - }; - - 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."; - type = types.attrsOf types.str; - default = {}; - }; + 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.enableServer && cfg.provision.enable) { + config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { 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 = '' + 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"; + } + { + assertion = cfg.provision.enable -> cfg.enableClient; + message = " requires 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; in { - assertion = unknownGroups == []; + assertion = (cfg.enableServer && cfg.provision.enable) -> 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 unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.scopeMaps); in { - assertion = unknownGroups == []; + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == []; message = "kanidm: provision.systems.oauth2.${oauth2}.scopeMaps: Refers to unknown groups: ${unknownGroups}"; }) (let unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.supplementaryScopeMaps); in { - assertion = unknownGroups == []; + assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == []; message = "kanidm: provision.systems.oauth2.${oauth2}.supplementaryScopeMaps: Refers to unknown groups: ${unknownGroups}"; }) ])); - systemd.services.kanidm = { - serviceConfig.ExecStartPost = - [provisioningScript] - # Only the restarter runs with elevated privileges - ++ optional (cfg.provision.systems.oauth2 != {}) "+${restarterScript}"; + environment.systemPackages = mkIf cfg.enableClient [cfg.package]; - preStart = let - mappingsJson = pkgs.writeText "mappings.json" (builtins.toJSON { - account_secrets.admin = cfg.provision.adminPasswordFile; - oauth2_basic_secrets = mapAttrs (_: x: v.basicSecretFile) cfg.provision.systems.oauth2; - }); - in '' - 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} ${tokenMappings} - fi - ''; + 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 + # 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; } diff --git a/modules/meta/telegraf.nix b/modules/meta/telegraf.nix index 2ca47dc..bfeeb97 100644 --- a/modules/meta/telegraf.nix +++ b/modules/meta/telegraf.nix @@ -65,16 +65,11 @@ in { group = "influxdb2"; }; - services.influxdb2.provision.ensureApiTokens = [ - { - name = "telegraf (${config.node.name})"; - org = "servers"; - user = "admin"; - readBuckets = ["telegraf"]; - writeBuckets = ["telegraf"]; - tokenFile = nodes.${cfg.influxdb2.node}.config.age.secrets."telegraf-influxdb-token-${config.node.name}".path; - } - ]; + services.influxdb2.provision.organization.servers.auths."telegraf (${config.node.name})" = { + readBuckets = ["telegraf"]; + writeBuckets = ["telegraf"]; + tokenFile = nodes.${cfg.influxdb2.node}.config.age.secrets."telegraf-influxdb-token-${config.node.name}".path; + }; }; age.secrets.telegraf-influxdb-token = { diff --git a/modules/repo/distributed-config.nix b/modules/repo/distributed-config.nix index 0b7ce02..c11117e 100644 --- a/modules/repo/distributed-config.nix +++ b/modules/repo/distributed-config.nix @@ -47,6 +47,6 @@ in { networking.providedDomains = mergeFromOthers ["networking" "providedDomains"]; services.nginx.upstreams = mergeFromOthers ["services" "nginx" "upstreams"]; services.nginx.virtualHosts = mergeFromOthers ["services" "nginx" "virtualHosts"]; - services.influxdb2.provision.ensureApiTokens = mergeFromOthers ["services" "influxdb2" "provision" "ensureApiTokens"]; + services.influxdb2.provision.organizations = mergeFromOthers ["services" "influxdb2" "organizations"]; }; } diff --git a/pkgs/kanidm-secret-manipulator.nix b/pkgs/kanidm-secret-manipulator.nix index 5b1a328..d0b7be8 100644 --- a/pkgs/kanidm-secret-manipulator.nix +++ b/pkgs/kanidm-secret-manipulator.nix @@ -13,7 +13,7 @@ rustPlatform.buildRustPackage rec { owner = "oddlama"; repo = "kanidm-secret-manipulator"; rev = "v${version}"; - hash = "sha256-mLOTnOsbUozYRDBXVYtIrUE5jDXEqW8HMO57qWsp1Go="; + hash = "sha256-Hn/143YJ0rn9AihuI/wsDlqtnGi/LBzbfdMNTukc34c="; }; cargoHash = "sha256-L//ZtfbOxV6Hf5x5tLAQ52MChSclzJlhI7sZKqvByMo=";