at 23.11-beta 15 kB view raw
1{ config, lib, options, pkgs, ... }: 2let 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 certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ]; 11 12 # Merge bind mount paths and remove paths where a prefix is already mounted. 13 # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount 14 # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. 15 hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; 16 mergePaths = lib.foldl' (merged: newPath: let 17 # If the new path is a prefix to some existing path, we need to filter it out 18 filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; 19 # If a prefix of the new path is already in the list, do not add it 20 filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath; 21 in filteredPaths ++ filteredNew) []; 22 23 defaultServiceConfig = { 24 BindReadOnlyPaths = [ 25 "/nix/store" 26 "-/etc/resolv.conf" 27 "-/etc/nsswitch.conf" 28 "-/etc/hosts" 29 "-/etc/localtime" 30 ]; 31 CapabilityBoundingSet = []; 32 # ProtectClock= adds DeviceAllow=char-rtc r 33 DeviceAllow = ""; 34 # Implies ProtectSystem=strict, which re-mounts all paths 35 # DynamicUser = true; 36 LockPersonality = true; 37 MemoryDenyWriteExecute = true; 38 NoNewPrivileges = true; 39 PrivateDevices = true; 40 PrivateMounts = true; 41 PrivateNetwork = true; 42 PrivateTmp = true; 43 PrivateUsers = true; 44 ProcSubset = "pid"; 45 ProtectClock = true; 46 ProtectHome = true; 47 ProtectHostname = true; 48 # Would re-mount paths ignored by temporary root 49 #ProtectSystem = "strict"; 50 ProtectControlGroups = true; 51 ProtectKernelLogs = true; 52 ProtectKernelModules = true; 53 ProtectKernelTunables = true; 54 ProtectProc = "invisible"; 55 RestrictAddressFamilies = [ ]; 56 RestrictNamespaces = true; 57 RestrictRealtime = true; 58 RestrictSUIDSGID = true; 59 SystemCallArchitectures = "native"; 60 SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ]; 61 # Does not work well with the temporary root 62 #UMask = "0066"; 63 }; 64 65in 66{ 67 options.services.kanidm = { 68 enableClient = lib.mkEnableOption (lib.mdDoc "the Kanidm client"); 69 enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server"); 70 enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration"); 71 72 package = lib.mkPackageOptionMD pkgs "kanidm" {}; 73 74 serverSettings = lib.mkOption { 75 type = lib.types.submodule { 76 freeformType = settingsFormat.type; 77 78 options = { 79 bindaddress = lib.mkOption { 80 description = lib.mdDoc "Address/port combination the webserver binds to."; 81 example = "[::1]:8443"; 82 type = lib.types.str; 83 }; 84 # Should be optional but toml does not accept null 85 ldapbindaddress = lib.mkOption { 86 description = lib.mdDoc '' 87 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. 88 ''; 89 example = "[::1]:636"; 90 default = null; 91 type = lib.types.nullOr lib.types.str; 92 }; 93 origin = lib.mkOption { 94 description = lib.mdDoc "The origin of your Kanidm instance. Must have https as protocol."; 95 example = "https://idm.example.org"; 96 type = lib.types.strMatching "^https://.*"; 97 }; 98 domain = lib.mkOption { 99 description = lib.mdDoc '' 100 The `domain` that Kanidm manages. Must be below or equal to the domain 101 specified in `serverSettings.origin`. 102 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`. 103 While it is possible to change the domain later on, it requires extra steps! 104 Please consider the warnings and execute the steps described 105 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain). 106 ''; 107 example = "example.org"; 108 default = null; 109 type = lib.types.nullOr lib.types.str; 110 }; 111 db_path = lib.mkOption { 112 description = lib.mdDoc "Path to Kanidm database."; 113 default = "/var/lib/kanidm/kanidm.db"; 114 readOnly = true; 115 type = lib.types.path; 116 }; 117 tls_chain = lib.mkOption { 118 description = lib.mdDoc "TLS chain in pem format."; 119 type = lib.types.path; 120 }; 121 tls_key = lib.mkOption { 122 description = lib.mdDoc "TLS key in pem format."; 123 type = lib.types.path; 124 }; 125 log_level = lib.mkOption { 126 description = lib.mdDoc "Log level of the server."; 127 default = "info"; 128 type = lib.types.enum [ "info" "debug" "trace" ]; 129 }; 130 role = lib.mkOption { 131 description = lib.mdDoc "The role of this server. This affects the replication relationship and thereby available features."; 132 default = "WriteReplica"; 133 type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; 134 }; 135 }; 136 }; 137 default = { }; 138 description = lib.mdDoc '' 139 Settings for Kanidm, see 140 [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html) 141 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml) 142 for possible values. 143 ''; 144 }; 145 146 clientSettings = lib.mkOption { 147 type = lib.types.submodule { 148 freeformType = settingsFormat.type; 149 150 options.uri = lib.mkOption { 151 description = lib.mdDoc "Address of the Kanidm server."; 152 example = "http://127.0.0.1:8080"; 153 type = lib.types.str; 154 }; 155 }; 156 description = lib.mdDoc '' 157 Configure Kanidm clients, needed for the PAM daemon. See 158 [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration) 159 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config) 160 for possible values. 161 ''; 162 }; 163 164 unixSettings = lib.mkOption { 165 type = lib.types.submodule { 166 freeformType = settingsFormat.type; 167 168 options.pam_allowed_login_groups = lib.mkOption { 169 description = lib.mdDoc "Kanidm groups that are allowed to login using PAM."; 170 example = "my_pam_group"; 171 type = lib.types.listOf lib.types.str; 172 }; 173 }; 174 description = lib.mdDoc '' 175 Configure Kanidm unix daemon. 176 See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon) 177 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd) 178 for possible values. 179 ''; 180 }; 181 }; 182 183 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { 184 assertions = 185 [ 186 { 187 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain); 188 message = '' 189 <option>services.kanidm.serverSettings.tls_chain</option> points to 190 a file in the Nix store. You should use a quoted absolute path to 191 prevent this. 192 ''; 193 } 194 { 195 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key); 196 message = '' 197 <option>services.kanidm.serverSettings.tls_key</option> points to 198 a file in the Nix store. You should use a quoted absolute path to 199 prevent this. 200 ''; 201 } 202 { 203 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; 204 message = '' 205 <option>services.kanidm.clientSettings</option> needs to be configured 206 if the client is enabled. 207 ''; 208 } 209 { 210 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; 211 message = '' 212 <option>services.kanidm.clientSettings</option> needs to be configured 213 for the PAM daemon to connect to the Kanidm server. 214 ''; 215 } 216 { 217 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null 218 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); 219 message = '' 220 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance 221 is not a ReadOnlyReplica. Otherwise the db would inherit it from 222 the instance it follows. 223 ''; 224 } 225 ]; 226 227 environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ]; 228 229 systemd.services.kanidm = lib.mkIf cfg.enableServer { 230 description = "kanidm identity management daemon"; 231 wantedBy = [ "multi-user.target" ]; 232 after = [ "network.target" ]; 233 serviceConfig = lib.mkMerge [ 234 # Merge paths and ignore existing prefixes needs to sidestep mkMerge 235 (defaultServiceConfig // { 236 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); 237 }) 238 { 239 StateDirectory = "kanidm"; 240 StateDirectoryMode = "0700"; 241 RuntimeDirectory = "kanidmd"; 242 ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}"; 243 User = "kanidm"; 244 Group = "kanidm"; 245 246 BindPaths = [ 247 # To create the socket 248 "/run/kanidmd:/run/kanidmd" 249 ]; 250 251 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 252 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 253 # This would otherwise override the CAP_NET_BIND_SERVICE capability. 254 PrivateUsers = lib.mkForce false; 255 # Port needs to be exposed to the host network 256 PrivateNetwork = lib.mkForce false; 257 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 258 TemporaryFileSystem = "/:ro"; 259 } 260 ]; 261 environment.RUST_LOG = "info"; 262 }; 263 264 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam { 265 description = "Kanidm PAM daemon"; 266 wantedBy = [ "multi-user.target" ]; 267 after = [ "network.target" ]; 268 restartTriggers = [ unixConfigFile clientConfigFile ]; 269 serviceConfig = lib.mkMerge [ 270 defaultServiceConfig 271 { 272 CacheDirectory = "kanidm-unixd"; 273 CacheDirectoryMode = "0700"; 274 RuntimeDirectory = "kanidm-unixd"; 275 ExecStart = "${cfg.package}/bin/kanidm_unixd"; 276 User = "kanidm-unixd"; 277 Group = "kanidm-unixd"; 278 279 BindReadOnlyPaths = [ 280 "-/etc/kanidm" 281 "-/etc/static/kanidm" 282 "-/etc/ssl" 283 "-/etc/static/ssl" 284 "-/etc/passwd" 285 "-/etc/group" 286 ]; 287 BindPaths = [ 288 # To create the socket 289 "/run/kanidm-unixd:/var/run/kanidm-unixd" 290 ]; 291 # Needs to connect to kanidmd 292 PrivateNetwork = lib.mkForce false; 293 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 294 TemporaryFileSystem = "/:ro"; 295 } 296 ]; 297 environment.RUST_LOG = "info"; 298 }; 299 300 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { 301 description = "Kanidm PAM home management daemon"; 302 wantedBy = [ "multi-user.target" ]; 303 after = [ "network.target" "kanidm-unixd.service" ]; 304 partOf = [ "kanidm-unixd.service" ]; 305 restartTriggers = [ unixConfigFile clientConfigFile ]; 306 serviceConfig = { 307 ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; 308 309 BindReadOnlyPaths = [ 310 "/nix/store" 311 "-/etc/resolv.conf" 312 "-/etc/nsswitch.conf" 313 "-/etc/hosts" 314 "-/etc/localtime" 315 "-/etc/kanidm" 316 "-/etc/static/kanidm" 317 ]; 318 BindPaths = [ 319 # To manage home directories 320 "/home" 321 # To connect to kanidm-unixd 322 "/run/kanidm-unixd:/var/run/kanidm-unixd" 323 ]; 324 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket 325 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ]; 326 IPAddressDeny = "any"; 327 # Need access to users 328 PrivateUsers = false; 329 # Need access to home directories 330 ProtectHome = false; 331 RestrictAddressFamilies = [ "AF_UNIX" ]; 332 TemporaryFileSystem = "/:ro"; 333 Restart = "on-failure"; 334 }; 335 environment.RUST_LOG = "info"; 336 }; 337 338 # These paths are hardcoded 339 environment.etc = lib.mkMerge [ 340 (lib.mkIf cfg.enableServer { 341 "kanidm/server.toml".source = serverConfigFile; 342 }) 343 (lib.mkIf options.services.kanidm.clientSettings.isDefined { 344 "kanidm/config".source = clientConfigFile; 345 }) 346 (lib.mkIf cfg.enablePam { 347 "kanidm/unixd".source = unixConfigFile; 348 }) 349 ]; 350 351 system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ]; 352 353 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm"; 354 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; 355 356 users.groups = lib.mkMerge [ 357 (lib.mkIf cfg.enableServer { 358 kanidm = { }; 359 }) 360 (lib.mkIf cfg.enablePam { 361 kanidm-unixd = { }; 362 }) 363 ]; 364 users.users = lib.mkMerge [ 365 (lib.mkIf cfg.enableServer { 366 kanidm = { 367 description = "Kanidm server"; 368 isSystemUser = true; 369 group = "kanidm"; 370 packages = [ cfg.package ]; 371 }; 372 }) 373 (lib.mkIf cfg.enablePam { 374 kanidm-unixd = { 375 description = "Kanidm PAM daemon"; 376 isSystemUser = true; 377 group = "kanidm-unixd"; 378 }; 379 }) 380 ]; 381 }; 382 383 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ]; 384 meta.buildDocsInSandbox = false; 385}