nixos/kanidm: init

Co-Authored-By: Martin Weinelt <mweinelt@users.noreply.github.com>
Co-Authored-By: Flakebi <flakebi@t-online.de>

+424
+2
nixos/doc/manual/release-notes/rl-2205.section.md
··· 135 136 - [nifi](https://nifi.apache.org), an easy to use, powerful, and reliable system to process and distribute data. Available as [services.nifi](options.html#opt-services.nifi.enable). 137 138 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> 139 140 ## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
··· 135 136 - [nifi](https://nifi.apache.org), an easy to use, powerful, and reliable system to process and distribute data. Available as [services.nifi](options.html#opt-services.nifi.enable). 137 138 + - [kanidm](https://kanidm.github.io/kanidm/stable/), an identity management server written in Rust. 139 + 140 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> 141 142 ## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
+1
nixos/modules/module-list.nix
··· 975 ./services/security/hockeypuck.nix 976 ./services/security/hologram-server.nix 977 ./services/security/hologram-agent.nix 978 ./services/security/munge.nix 979 ./services/security/nginx-sso.nix 980 ./services/security/oauth2_proxy.nix
··· 975 ./services/security/hockeypuck.nix 976 ./services/security/hologram-server.nix 977 ./services/security/hologram-agent.nix 978 + ./services/security/kanidm.nix 979 ./services/security/munge.nix 980 ./services/security/nginx-sso.nix 981 ./services/security/oauth2_proxy.nix
+345
nixos/modules/services/security/kanidm.nix
···
··· 1 + { config, lib, options, pkgs, ... }: 2 + let 3 + cfg = config.services.kanidm; 4 + settingsFormat = pkgs.formats.toml { }; 5 + # Remove null values, so we can document optional values that don't end up in the generated TOML file. 6 + filterConfig = lib.converge (lib.filterAttrsRecursive (_: v: v != null)); 7 + serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); 8 + clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); 9 + unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); 10 + 11 + defaultServiceConfig = { 12 + BindReadOnlyPaths = [ 13 + "/nix/store" 14 + "-/etc/resolv.conf" 15 + "-/etc/nsswitch.conf" 16 + "-/etc/hosts" 17 + "-/etc/localtime" 18 + ]; 19 + CapabilityBoundingSet = ""; 20 + # ProtectClock= adds DeviceAllow=char-rtc r 21 + DeviceAllow = ""; 22 + # Implies ProtectSystem=strict, which re-mounts all paths 23 + # DynamicUser = true; 24 + LockPersonality = true; 25 + MemoryDenyWriteExecute = true; 26 + NoNewPrivileges = true; 27 + PrivateDevices = true; 28 + PrivateMounts = true; 29 + PrivateNetwork = true; 30 + PrivateTmp = true; 31 + PrivateUsers = true; 32 + ProcSubset = "pid"; 33 + ProtectClock = true; 34 + ProtectHome = true; 35 + ProtectHostname = true; 36 + # Would re-mount paths ignored by temporary root 37 + #ProtectSystem = "strict"; 38 + ProtectControlGroups = true; 39 + ProtectKernelLogs = true; 40 + ProtectKernelModules = true; 41 + ProtectKernelTunables = true; 42 + ProtectProc = "invisible"; 43 + RestrictAddressFamilies = [ ]; 44 + RestrictNamespaces = true; 45 + RestrictRealtime = true; 46 + RestrictSUIDSGID = true; 47 + SystemCallArchitectures = "native"; 48 + SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ]; 49 + # Does not work well with the temporary root 50 + #UMask = "0066"; 51 + }; 52 + 53 + in 54 + { 55 + options.services.kanidm = { 56 + enableClient = lib.mkEnableOption "the Kanidm client"; 57 + enableServer = lib.mkEnableOption "the Kanidm server"; 58 + enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration."; 59 + 60 + serverSettings = lib.mkOption { 61 + type = lib.types.submodule { 62 + freeformType = settingsFormat.type; 63 + 64 + options = { 65 + bindaddress = lib.mkOption { 66 + description = "Address/port combination the webserver binds to."; 67 + example = "[::1]:8443"; 68 + type = lib.types.str; 69 + }; 70 + # Should be optional but toml does not accept null 71 + ldapbindaddress = lib.mkOption { 72 + description = '' 73 + Address and port the LDAP server is bound to. Setting this to <literal>null</literal> disables the LDAP interface. 74 + ''; 75 + example = "[::1]:636"; 76 + default = null; 77 + type = lib.types.nullOr lib.types.str; 78 + }; 79 + origin = lib.mkOption { 80 + description = "The origin of your Kanidm instance. Must have https as protocol."; 81 + example = "https://idm.example.org"; 82 + type = lib.types.strMatching "^https://.*"; 83 + }; 84 + domain = lib.mkOption { 85 + description = '' 86 + The <literal>domain</literal> that Kanidm manages. Must be below or equal to the domain 87 + specified in <literal>serverSettings.origin</literal>. 88 + This can be left at <literal>null</literal>, only if your instance has the role <literal>ReadOnlyReplica</literal>. 89 + While it is possible to change the domain later on, it requires extra steps! 90 + Please consider the warnings and execute the steps described 91 + <link xlink:href="https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain">in the documentation</link>. 92 + ''; 93 + example = "example.org"; 94 + default = null; 95 + type = lib.types.nullOr lib.types.str; 96 + }; 97 + db_path = lib.mkOption { 98 + description = "Path to Kanidm database."; 99 + default = "/var/lib/kanidm/kanidm.db"; 100 + readOnly = true; 101 + type = lib.types.path; 102 + }; 103 + log_level = lib.mkOption { 104 + description = "Log level of the server."; 105 + default = "default"; 106 + type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ]; 107 + }; 108 + role = lib.mkOption { 109 + description = "The role of this server. This affects the replication relationship and thereby available features."; 110 + default = "WriteReplica"; 111 + type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; 112 + }; 113 + }; 114 + }; 115 + default = { }; 116 + description = '' 117 + Settings for Kanidm, see 118 + <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md">the documentation</link> 119 + and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/server.toml">example configuration</link> 120 + for possible values. 121 + ''; 122 + }; 123 + 124 + clientSettings = lib.mkOption { 125 + type = lib.types.submodule { 126 + freeformType = settingsFormat.type; 127 + 128 + options.uri = lib.mkOption { 129 + description = "Address of the Kanidm server."; 130 + example = "http://127.0.0.1:8080"; 131 + type = lib.types.str; 132 + }; 133 + }; 134 + description = '' 135 + Configure Kanidm clients, needed for the PAM daemon. See 136 + <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration">the documentation</link> 137 + and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/config">example configuration</link> 138 + for possible values. 139 + ''; 140 + }; 141 + 142 + unixSettings = lib.mkOption { 143 + type = lib.types.submodule { 144 + freeformType = settingsFormat.type; 145 + 146 + options.pam_allowed_login_groups = lib.mkOption { 147 + description = "Kanidm groups that are allowed to login using PAM."; 148 + example = "my_pam_group"; 149 + type = lib.types.listOf lib.types.str; 150 + }; 151 + }; 152 + description = '' 153 + Configure Kanidm unix daemon. 154 + See <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon">the documentation</link> 155 + and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/unixd">example configuration</link> 156 + for possible values. 157 + ''; 158 + }; 159 + }; 160 + 161 + config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { 162 + assertions = 163 + [ 164 + { 165 + assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain); 166 + message = '' 167 + <option>services.kanidm.serverSettings.tls_chain</option> points to 168 + a file in the Nix store. You should use a quoted absolute path to 169 + prevent this. 170 + ''; 171 + } 172 + { 173 + assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key); 174 + message = '' 175 + <option>services.kanidm.serverSettings.tls_key</option> points to 176 + a file in the Nix store. You should use a quoted absolute path to 177 + prevent this. 178 + ''; 179 + } 180 + { 181 + assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; 182 + message = '' 183 + <option>services.kanidm.clientSettings</option> needs to be configured 184 + if the client is enabled. 185 + ''; 186 + } 187 + { 188 + assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; 189 + message = '' 190 + <option>services.kanidm.clientSettings</option> needs to be configured 191 + for the PAM daemon to connect to the Kanidm server. 192 + ''; 193 + } 194 + { 195 + assertion = !cfg.enableServer || (cfg.serverSettings.domain == null 196 + -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); 197 + message = '' 198 + <option>services.kanidm.serverSettings.domain</option> can only be set if this instance 199 + is not a ReadOnlyReplica. Otherwise the db would inherit it from 200 + the instance it follows. 201 + ''; 202 + } 203 + ]; 204 + 205 + environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ]; 206 + 207 + systemd.services.kanidm = lib.mkIf cfg.enableServer { 208 + description = "kanidm identity management daemon"; 209 + wantedBy = [ "multi-user.target" ]; 210 + after = [ "network.target" ]; 211 + serviceConfig = defaultServiceConfig // { 212 + StateDirectory = "kanidm"; 213 + StateDirectoryMode = "0700"; 214 + ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; 215 + User = "kanidm"; 216 + Group = "kanidm"; 217 + 218 + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 219 + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 220 + # This would otherwise override the CAP_NET_BIND_SERVICE capability. 221 + PrivateUsers = false; 222 + # Port needs to be exposed to the host network 223 + PrivateNetwork = false; 224 + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; 225 + TemporaryFileSystem = "/:ro"; 226 + }; 227 + environment.RUST_LOG = "info"; 228 + }; 229 + 230 + systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam { 231 + description = "Kanidm PAM daemon"; 232 + wantedBy = [ "multi-user.target" ]; 233 + after = [ "network.target" ]; 234 + restartTriggers = [ unixConfigFile clientConfigFile ]; 235 + serviceConfig = defaultServiceConfig // { 236 + CacheDirectory = "kanidm-unixd"; 237 + CacheDirectoryMode = "0700"; 238 + RuntimeDirectory = "kanidm-unixd"; 239 + ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; 240 + User = "kanidm-unixd"; 241 + Group = "kanidm-unixd"; 242 + 243 + BindReadOnlyPaths = [ 244 + "/nix/store" 245 + "-/etc/resolv.conf" 246 + "-/etc/nsswitch.conf" 247 + "-/etc/hosts" 248 + "-/etc/localtime" 249 + "-/etc/kanidm" 250 + "-/etc/static/kanidm" 251 + ]; 252 + BindPaths = [ 253 + # To create the socket 254 + "/run/kanidm-unixd:/var/run/kanidm-unixd" 255 + ]; 256 + # Needs to connect to kanidmd 257 + PrivateNetwork = false; 258 + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 259 + TemporaryFileSystem = "/:ro"; 260 + }; 261 + environment.RUST_LOG = "info"; 262 + }; 263 + 264 + systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { 265 + description = "Kanidm PAM home management daemon"; 266 + wantedBy = [ "multi-user.target" ]; 267 + after = [ "network.target" "kanidm-unixd.service" ]; 268 + partOf = [ "kanidm-unixd.service" ]; 269 + restartTriggers = [ unixConfigFile clientConfigFile ]; 270 + serviceConfig = { 271 + ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks"; 272 + 273 + BindReadOnlyPaths = [ 274 + "/nix/store" 275 + "-/etc/resolv.conf" 276 + "-/etc/nsswitch.conf" 277 + "-/etc/hosts" 278 + "-/etc/localtime" 279 + "-/etc/kanidm" 280 + "-/etc/static/kanidm" 281 + ]; 282 + BindPaths = [ 283 + # To manage home directories 284 + "/home" 285 + # To connect to kanidm-unixd 286 + "/run/kanidm-unixd:/var/run/kanidm-unixd" 287 + ]; 288 + # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket 289 + CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ]; 290 + IPAddressDeny = "any"; 291 + # Need access to users 292 + PrivateUsers = false; 293 + # Need access to home directories 294 + ProtectHome = false; 295 + RestrictAddressFamilies = [ "AF_UNIX" ]; 296 + TemporaryFileSystem = "/:ro"; 297 + }; 298 + environment.RUST_LOG = "info"; 299 + }; 300 + 301 + # These paths are hardcoded 302 + environment.etc = lib.mkMerge [ 303 + (lib.mkIf options.services.kanidm.clientSettings.isDefined { 304 + "kanidm/config".source = clientConfigFile; 305 + }) 306 + (lib.mkIf cfg.enablePam { 307 + "kanidm/unixd".source = unixConfigFile; 308 + }) 309 + ]; 310 + 311 + system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ]; 312 + 313 + system.nssDatabases.group = lib.optional cfg.enablePam "kanidm"; 314 + system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; 315 + 316 + users.groups = lib.mkMerge [ 317 + (lib.mkIf cfg.enableServer { 318 + kanidm = { }; 319 + }) 320 + (lib.mkIf cfg.enablePam { 321 + kanidm-unixd = { }; 322 + }) 323 + ]; 324 + users.users = lib.mkMerge [ 325 + (lib.mkIf cfg.enableServer { 326 + kanidm = { 327 + description = "Kanidm server"; 328 + isSystemUser = true; 329 + group = "kanidm"; 330 + packages = with pkgs; [ kanidm ]; 331 + }; 332 + }) 333 + (lib.mkIf cfg.enablePam { 334 + kanidm-unixd = { 335 + description = "Kanidm PAM daemon"; 336 + isSystemUser = true; 337 + group = "kanidm-unixd"; 338 + }; 339 + }) 340 + ]; 341 + }; 342 + 343 + meta.maintainers = with lib.maintainers; [ erictapen Flakebi ]; 344 + meta.buildDocsInSandbox = false; 345 + }
+1
nixos/tests/all-tests.nix
··· 253 k3s-single-node = handleTest ./k3s-single-node.nix {}; 254 k3s-single-node-docker = handleTest ./k3s-single-node-docker.nix {}; 255 kafka = handleTest ./kafka.nix {}; 256 kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {}; 257 kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {}; 258 kea = handleTest ./kea.nix {};
··· 253 k3s-single-node = handleTest ./k3s-single-node.nix {}; 254 k3s-single-node-docker = handleTest ./k3s-single-node-docker.nix {}; 255 kafka = handleTest ./kafka.nix {}; 256 + kanidm = handleTest ./kanidm.nix {}; 257 kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {}; 258 kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {}; 259 kea = handleTest ./kea.nix {};
+75
nixos/tests/kanidm.nix
···
··· 1 + import ./make-test-python.nix ({ pkgs, ... }: 2 + let 3 + certs = import ./common/acme/server/snakeoil-certs.nix; 4 + serverDomain = certs.domain; 5 + in 6 + { 7 + name = "kanidm"; 8 + meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi ]; 9 + 10 + nodes.server = { config, pkgs, lib, ... }: { 11 + services.kanidm = { 12 + enableServer = true; 13 + serverSettings = { 14 + origin = "https://${serverDomain}"; 15 + domain = serverDomain; 16 + bindaddress = "[::1]:8443"; 17 + ldapbindaddress = "[::1]:636"; 18 + }; 19 + }; 20 + 21 + services.nginx = { 22 + enable = true; 23 + recommendedProxySettings = true; 24 + virtualHosts."${serverDomain}" = { 25 + forceSSL = true; 26 + sslCertificate = certs."${serverDomain}".cert; 27 + sslCertificateKey = certs."${serverDomain}".key; 28 + locations."/".proxyPass = "http://[::1]:8443"; 29 + }; 30 + }; 31 + 32 + security.pki.certificateFiles = [ certs.ca.cert ]; 33 + 34 + networking.hosts."::1" = [ serverDomain ]; 35 + networking.firewall.allowedTCPPorts = [ 80 443 ]; 36 + 37 + users.users.kanidm.shell = pkgs.bashInteractive; 38 + 39 + environment.systemPackages = with pkgs; [ kanidm openldap ripgrep ]; 40 + }; 41 + 42 + nodes.client = { pkgs, nodes, ... }: { 43 + services.kanidm = { 44 + enableClient = true; 45 + clientSettings = { 46 + uri = "https://${serverDomain}"; 47 + }; 48 + }; 49 + 50 + networking.hosts."${nodes.server.config.networking.primaryIPAddress}" = [ serverDomain ]; 51 + 52 + security.pki.certificateFiles = [ certs.ca.cert ]; 53 + }; 54 + 55 + testScript = { nodes, ... }: 56 + let 57 + ldapBaseDN = builtins.concatStringsSep "," (map (s: "dc=" + s) (pkgs.lib.splitString "." serverDomain)); 58 + 59 + # We need access to the config file in the test script. 60 + filteredConfig = pkgs.lib.converge 61 + (pkgs.lib.filterAttrsRecursive (_: v: v != null)) 62 + nodes.server.config.services.kanidm.serverSettings; 63 + serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig; 64 + 65 + in 66 + '' 67 + start_all() 68 + server.wait_for_unit("kanidm.service") 69 + server.wait_until_succeeds("curl -sf https://${serverDomain} | grep Kanidm") 70 + server.wait_until_succeeds("ldapsearch -H ldap://[::1]:636 -b '${ldapBaseDN}' -x '(name=test)'") 71 + client.wait_until_succeeds("kanidm login -D anonymous && kanidm self whoami | grep anonymous@${serverDomain}") 72 + (rv, result) = server.execute("kanidmd recover_account -d quiet -c ${serverConfigFile} -n admin 2>&1 | rg -o '[A-Za-z0-9]{48}'") 73 + assert rv == 0 74 + ''; 75 + })