at 23.11-pre 14 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 = if hasPrefixInList filteredPaths newPath then [] else [ 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 serverSettings = lib.mkOption { 73 type = lib.types.submodule { 74 freeformType = settingsFormat.type; 75 76 options = { 77 bindaddress = lib.mkOption { 78 description = lib.mdDoc "Address/port combination the webserver binds to."; 79 example = "[::1]:8443"; 80 type = lib.types.str; 81 }; 82 # Should be optional but toml does not accept null 83 ldapbindaddress = lib.mkOption { 84 description = lib.mdDoc '' 85 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. 86 ''; 87 example = "[::1]:636"; 88 default = null; 89 type = lib.types.nullOr lib.types.str; 90 }; 91 origin = lib.mkOption { 92 description = lib.mdDoc "The origin of your Kanidm instance. Must have https as protocol."; 93 example = "https://idm.example.org"; 94 type = lib.types.strMatching "^https://.*"; 95 }; 96 domain = lib.mkOption { 97 description = lib.mdDoc '' 98 The `domain` that Kanidm manages. Must be below or equal to the domain 99 specified in `serverSettings.origin`. 100 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`. 101 While it is possible to change the domain later on, it requires extra steps! 102 Please consider the warnings and execute the steps described 103 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain). 104 ''; 105 example = "example.org"; 106 default = null; 107 type = lib.types.nullOr lib.types.str; 108 }; 109 db_path = lib.mkOption { 110 description = lib.mdDoc "Path to Kanidm database."; 111 default = "/var/lib/kanidm/kanidm.db"; 112 readOnly = true; 113 type = lib.types.path; 114 }; 115 tls_chain = lib.mkOption { 116 description = lib.mdDoc "TLS chain in pem format."; 117 type = lib.types.path; 118 }; 119 tls_key = lib.mkOption { 120 description = lib.mdDoc "TLS key in pem format."; 121 type = lib.types.path; 122 }; 123 log_level = lib.mkOption { 124 description = lib.mdDoc "Log level of the server."; 125 default = "default"; 126 type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ]; 127 }; 128 role = lib.mkOption { 129 description = lib.mdDoc "The role of this server. This affects the replication relationship and thereby available features."; 130 default = "WriteReplica"; 131 type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; 132 }; 133 }; 134 }; 135 default = { }; 136 description = lib.mdDoc '' 137 Settings for Kanidm, see 138 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md) 139 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml) 140 for possible values. 141 ''; 142 }; 143 144 clientSettings = lib.mkOption { 145 type = lib.types.submodule { 146 freeformType = settingsFormat.type; 147 148 options.uri = lib.mkOption { 149 description = lib.mdDoc "Address of the Kanidm server."; 150 example = "http://127.0.0.1:8080"; 151 type = lib.types.str; 152 }; 153 }; 154 description = lib.mdDoc '' 155 Configure Kanidm clients, needed for the PAM daemon. See 156 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration) 157 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config) 158 for possible values. 159 ''; 160 }; 161 162 unixSettings = lib.mkOption { 163 type = lib.types.submodule { 164 freeformType = settingsFormat.type; 165 166 options.pam_allowed_login_groups = lib.mkOption { 167 description = lib.mdDoc "Kanidm groups that are allowed to login using PAM."; 168 example = "my_pam_group"; 169 type = lib.types.listOf lib.types.str; 170 }; 171 }; 172 description = lib.mdDoc '' 173 Configure Kanidm unix daemon. 174 See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon) 175 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd) 176 for possible values. 177 ''; 178 }; 179 }; 180 181 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { 182 assertions = 183 [ 184 { 185 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain); 186 message = '' 187 <option>services.kanidm.serverSettings.tls_chain</option> points to 188 a file in the Nix store. You should use a quoted absolute path to 189 prevent this. 190 ''; 191 } 192 { 193 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key); 194 message = '' 195 <option>services.kanidm.serverSettings.tls_key</option> points to 196 a file in the Nix store. You should use a quoted absolute path to 197 prevent this. 198 ''; 199 } 200 { 201 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; 202 message = '' 203 <option>services.kanidm.clientSettings</option> needs to be configured 204 if the client is enabled. 205 ''; 206 } 207 { 208 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; 209 message = '' 210 <option>services.kanidm.clientSettings</option> needs to be configured 211 for the PAM daemon to connect to the Kanidm server. 212 ''; 213 } 214 { 215 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null 216 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); 217 message = '' 218 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance 219 is not a ReadOnlyReplica. Otherwise the db would inherit it from 220 the instance it follows. 221 ''; 222 } 223 ]; 224 225 environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ]; 226 227 systemd.services.kanidm = lib.mkIf cfg.enableServer { 228 description = "kanidm identity management daemon"; 229 wantedBy = [ "multi-user.target" ]; 230 after = [ "network.target" ]; 231 serviceConfig = lib.mkMerge [ 232 # Merge paths and ignore existing prefixes needs to sidestep mkMerge 233 (defaultServiceConfig // { 234 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); 235 }) 236 { 237 StateDirectory = "kanidm"; 238 StateDirectoryMode = "0700"; 239 ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; 240 User = "kanidm"; 241 Group = "kanidm"; 242 243 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 244 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 245 # This would otherwise override the CAP_NET_BIND_SERVICE capability. 246 PrivateUsers = lib.mkForce false; 247 # Port needs to be exposed to the host network 248 PrivateNetwork = lib.mkForce false; 249 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; 250 TemporaryFileSystem = "/:ro"; 251 } 252 ]; 253 environment.RUST_LOG = "info"; 254 }; 255 256 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam { 257 description = "Kanidm PAM daemon"; 258 wantedBy = [ "multi-user.target" ]; 259 after = [ "network.target" ]; 260 restartTriggers = [ unixConfigFile clientConfigFile ]; 261 serviceConfig = lib.mkMerge [ 262 defaultServiceConfig 263 { 264 CacheDirectory = "kanidm-unixd"; 265 CacheDirectoryMode = "0700"; 266 RuntimeDirectory = "kanidm-unixd"; 267 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; 268 User = "kanidm-unixd"; 269 Group = "kanidm-unixd"; 270 271 BindReadOnlyPaths = [ 272 "-/etc/kanidm" 273 "-/etc/static/kanidm" 274 "-/etc/ssl" 275 "-/etc/static/ssl" 276 ]; 277 BindPaths = [ 278 # To create the socket 279 "/run/kanidm-unixd:/var/run/kanidm-unixd" 280 ]; 281 # Needs to connect to kanidmd 282 PrivateNetwork = lib.mkForce false; 283 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 284 TemporaryFileSystem = "/:ro"; 285 } 286 ]; 287 environment.RUST_LOG = "info"; 288 }; 289 290 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { 291 description = "Kanidm PAM home management daemon"; 292 wantedBy = [ "multi-user.target" ]; 293 after = [ "network.target" "kanidm-unixd.service" ]; 294 partOf = [ "kanidm-unixd.service" ]; 295 restartTriggers = [ unixConfigFile clientConfigFile ]; 296 serviceConfig = { 297 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks"; 298 299 BindReadOnlyPaths = [ 300 "/nix/store" 301 "-/etc/resolv.conf" 302 "-/etc/nsswitch.conf" 303 "-/etc/hosts" 304 "-/etc/localtime" 305 "-/etc/kanidm" 306 "-/etc/static/kanidm" 307 ]; 308 BindPaths = [ 309 # To manage home directories 310 "/home" 311 # To connect to kanidm-unixd 312 "/run/kanidm-unixd:/var/run/kanidm-unixd" 313 ]; 314 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket 315 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ]; 316 IPAddressDeny = "any"; 317 # Need access to users 318 PrivateUsers = false; 319 # Need access to home directories 320 ProtectHome = false; 321 RestrictAddressFamilies = [ "AF_UNIX" ]; 322 TemporaryFileSystem = "/:ro"; 323 }; 324 environment.RUST_LOG = "info"; 325 }; 326 327 # These paths are hardcoded 328 environment.etc = lib.mkMerge [ 329 (lib.mkIf options.services.kanidm.clientSettings.isDefined { 330 "kanidm/config".source = clientConfigFile; 331 }) 332 (lib.mkIf cfg.enablePam { 333 "kanidm/unixd".source = unixConfigFile; 334 }) 335 ]; 336 337 system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ]; 338 339 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm"; 340 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; 341 342 users.groups = lib.mkMerge [ 343 (lib.mkIf cfg.enableServer { 344 kanidm = { }; 345 }) 346 (lib.mkIf cfg.enablePam { 347 kanidm-unixd = { }; 348 }) 349 ]; 350 users.users = lib.mkMerge [ 351 (lib.mkIf cfg.enableServer { 352 kanidm = { 353 description = "Kanidm server"; 354 isSystemUser = true; 355 group = "kanidm"; 356 packages = with pkgs; [ kanidm ]; 357 }; 358 }) 359 (lib.mkIf cfg.enablePam { 360 kanidm-unixd = { 361 description = "Kanidm PAM daemon"; 362 isSystemUser = true; 363 group = "kanidm-unixd"; 364 }; 365 }) 366 ]; 367 }; 368 369 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ]; 370 meta.buildDocsInSandbox = false; 371}