41 cfg = config.services.kanidm;
42 settingsFormat = pkgs.formats.toml { };
43 # Remove null values, so we can document optional values that don't end up in the generated TOML file.
44 filterConfig = converge (filterAttrsRecursive (_: v: v != null));
45 serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
46 clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
47 unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
48 certPaths = builtins.map builtins.dirOf [
49 cfg.serverSettings.tls_chain
50 cfg.serverSettings.tls_key
53 # Merge bind mount paths and remove paths where a prefix is already mounted.
54 # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
55 # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
57 list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
61 # If the new path is a prefix to some existing path, we need to filter it out
62 filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
63 # If a prefix of the new path is already in the list, do not add it
64 filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
66 filteredPaths ++ filteredNew
69 defaultServiceConfig = {
70 # Setting the type to notify enables additional healthchecks, ensuring units
71 # after and requiring kanidm-* wait for it to complete startup
75 # For healthcheck notifications
82 CapabilityBoundingSet = [ ];
83 # ProtectClock= adds DeviceAllow=char-rtc r
85 # Implies ProtectSystem=strict, which re-mounts all paths
87 LockPersonality = true;
88 MemoryDenyWriteExecute = true;
89 NoNewPrivileges = true;
90 PrivateDevices = true;
92 PrivateNetwork = true;
98 ProtectHostname = true;
99 # Would re-mount paths ignored by temporary root
100 #ProtectSystem = "strict";
101 ProtectControlGroups = true;
102 ProtectKernelLogs = true;
103 ProtectKernelModules = true;
104 ProtectKernelTunables = true;
105 ProtectProc = "invisible";
106 RestrictAddressFamilies = [ ];
107 RestrictNamespaces = true;
108 RestrictRealtime = true;
109 RestrictSUIDSGID = true;
110 SystemCallArchitectures = "native";
113 "~@privileged @resources @setuid @keyring"
115 # Does not work well with the temporary root
122 description = "Whether to ensure that this ${what} is present or absent.";
127 filterPresent = filterAttrs (_: v: v.present);
129 provisionStateJson = pkgs.writeText "provision-state.json" (
130 builtins.toJSON { inherit (cfg.provision) groups persons systems; }
133 # Only recover the admin account if a password should explicitly be provisioned
134 # for the account. Otherwise it is not needed for provisioning.
135 maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) ''
136 KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
137 # We always reset the admin account password if a desired password was specified.
138 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
139 echo "Failed to recover admin account" >&2
144 # Recover the idm_admin account. If a password should explicitly be provisioned
145 # for the account we set it, otherwise we generate a new one because it is required
148 if cfg.provision.idmAdminPasswordFile != null then
150 KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
151 # We always reset the idm_admin account password if a desired password was specified.
152 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
153 echo "Failed to recover idm_admin account" >&2
159 # Recover idm_admin account
160 if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
161 echo "$recover_out" >&2
162 echo "kanidm provision: Failed to recover admin account" >&2
165 if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then
166 echo "$recover_out" >&2
167 echo "kanidm provision: Failed to parse password for idm_admin account" >&2
172 postStartScript = pkgs.writeShellScript "post-start" ''
175 # Wait for the kanidm server to come online
177 while ! ${getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \
178 ${optionalString cfg.provision.acceptInvalidCerts "--insecure"} \
179 ${cfg.provision.instanceUrl} >/dev/null
182 if [[ "$count" -eq 30 ]]; then
183 echo "Tried for at least 30 seconds, giving up..."
192 KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
193 ${getExe pkgs.kanidm-provision} \
194 ${optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \
195 ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \
196 --url "${cfg.provision.instanceUrl}" \
197 --state ${provisionStateJson}
202 if hasInfix "]:" cfg.serverSettings.bindaddress then
203 last (splitString "]:" cfg.serverSettings.bindaddress)
206 if hasInfix "." cfg.serverSettings.bindaddress then
207 last (splitString ":" cfg.serverSettings.bindaddress)
213 options.services.kanidm = {
214 enableClient = mkEnableOption "the Kanidm client";
215 enableServer = mkEnableOption "the Kanidm server";
216 enablePam = mkEnableOption "the Kanidm PAM and NSS integration";
218 package = mkPackageOption pkgs "kanidm" { };
220 serverSettings = mkOption {
221 type = types.submodule {
222 freeformType = settingsFormat.type;
225 bindaddress = mkOption {
226 description = "Address/port combination the webserver binds to.";
227 example = "[::1]:8443";
230 # Should be optional but toml does not accept null
231 ldapbindaddress = mkOption {
233 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
235 example = "[::1]:636";
237 type = types.nullOr types.str;
240 description = "The origin of your Kanidm instance. Must have https as protocol.";
241 example = "https://idm.example.org";
242 type = types.strMatching "^https://.*";
246 The `domain` that Kanidm manages. Must be below or equal to the domain
247 specified in `serverSettings.origin`.
248 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`.
249 While it is possible to change the domain later on, it requires extra steps!
250 Please consider the warnings and execute the steps described
251 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain).
253 example = "example.org";
255 type = types.nullOr types.str;
258 description = "Path to Kanidm database.";
259 default = "/var/lib/kanidm/kanidm.db";
263 tls_chain = mkOption {
264 description = "TLS chain in pem format.";
268 description = "TLS key in pem format.";
271 log_level = mkOption {
272 description = "Log level of the server.";
281 description = "The role of this server. This affects the replication relationship and thereby available features.";
282 default = "WriteReplica";
291 description = "Path to the output directory for backups.";
293 default = "/var/lib/kanidm/backups";
295 schedule = mkOption {
296 description = "The schedule for backups in cron format.";
298 default = "00 22 * * *";
300 versions = mkOption {
302 Number of backups to keep.
304 The default is set to `0`, in order to disable backups by default.
306 type = types.ints.unsigned;
315 Settings for Kanidm, see
316 [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
317 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
322 clientSettings = mkOption {
323 type = types.submodule {
324 freeformType = settingsFormat.type;
326 options.uri = mkOption {
327 description = "Address of the Kanidm server.";
328 example = "http://127.0.0.1:8080";
333 Configure Kanidm clients, needed for the PAM daemon. See
334 [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
335 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
340 unixSettings = mkOption {
341 type = types.submodule {
342 freeformType = settingsFormat.type;
345 pam_allowed_login_groups = mkOption {
346 description = "Kanidm groups that are allowed to login using PAM.";
347 example = "my_pam_group";
348 type = types.listOf types.str;
350 hsm_pin_path = mkOption {
351 description = "Path to a HSM pin.";
352 default = "/var/cache/kanidm-unixd/hsm-pin";
358 Configure Kanidm unix daemon.
359 See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
360 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
366 enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers";
368 instanceUrl = mkOption {
369 description = "The instance url to which the provisioning tool should connect.";
370 default = "https://localhost:${serverPort}";
371 defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"'';
375 acceptInvalidCerts = mkOption {
377 Whether to allow invalid certificates when provisioning the target instance.
378 By default this is only allowed when the instanceUrl is localhost. This is
379 dangerous when used with an external URL.
382 default = hasPrefix "https://localhost:" cfg.provision.instanceUrl;
383 defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl'';
386 adminPasswordFile = mkOption {
387 description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
388 example = "/run/secrets/kanidm-admin-password";
390 type = types.nullOr types.path;
393 idmAdminPasswordFile = mkOption {
395 Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!
396 If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart.
398 example = "/run/secrets/kanidm-idm-admin-password";
400 type = types.nullOr types.path;
403 autoRemove = mkOption {
405 Determines whether deleting an entity in this provisioning config should automatically
406 cause them to be removed from kanidm, too. This works because the provisioning tool tracks
407 all entities it has ever created. If this is set to false, you need to explicitly specify
408 `present = false` to delete an entity.
415 description = "Provisioning of kanidm groups";
417 type = types.attrsOf (
418 types.submodule (groupSubmod: {
420 present = mkPresentOption "group";
423 description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
424 type = types.listOf types.str;
429 config.members = concatLists (
430 flip mapAttrsToList cfg.provision.persons (
433 personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
442 description = "Provisioning of kanidm persons";
444 type = types.attrsOf (
447 present = mkPresentOption "person";
449 displayName = mkOption {
450 description = "Display name";
455 legalName = mkOption {
456 description = "Full legal name";
457 type = types.nullOr types.str;
458 example = "Jane Doe";
462 mailAddresses = mkOption {
463 description = "Mail addresses. First given address is considered the primary address.";
464 type = types.listOf types.str;
465 example = [ "jane.doe@example.com" ];
470 description = "List of groups this person should belong to.";
471 type = types.listOf types.str;
480 systems.oauth2 = mkOption {
481 description = "Provisioning of oauth2 resource servers";
483 type = types.attrsOf (
486 present = mkPresentOption "oauth2 resource server";
489 description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)";
494 displayName = mkOption {
495 description = "Display name";
497 example = "Some Service";
500 originUrl = mkOption {
501 description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
504 originStrType = types.strMatching ".*://.*/$";
506 types.either originStrType (types.nonEmptyListOf originStrType);
507 example = "https://someservice.example.com/";
510 originLanding = mkOption {
511 description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions.";
513 example = "https://someservice.example.com/home";
516 basicSecretFile = mkOption {
518 The basic secret to use for this service. If null, the random secret generated
519 by kanidm will not be touched. Do NOT use a path from the nix store here!
521 type = types.nullOr types.path;
522 example = "/run/secrets/some-oauth2-basic-secret";
526 enableLocalhostRedirects = mkOption {
527 description = "Allow localhost redirects. Only for public clients.";
532 enableLegacyCrypto = mkOption {
533 description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256.";
538 allowInsecureClientDisablePkce = mkOption {
540 Disable PKCE on this oauth2 resource server to work around insecure clients
541 that may not support it. You should request the client to enable PKCE!
542 Only for non-public clients.
548 preferShortUsername = mkOption {
549 description = "Use 'name' instead of 'spn' in the preferred_username claim";
554 scopeMaps = mkOption {
556 Maps kanidm groups to returned oauth scopes.
557 See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
559 type = types.attrsOf (types.listOf types.str);
563 supplementaryScopeMaps = mkOption {
565 Maps kanidm groups to additionally returned oauth scopes.
566 See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
568 type = types.attrsOf (types.listOf types.str);
572 removeOrphanedClaimMaps = mkOption {
573 description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
578 claimMaps = mkOption {
580 Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to.
581 See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
584 type = types.attrsOf (
587 joinType = mkOption {
589 Determines how multiple values are joined to create the claim value.
590 See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
600 valuesByGroup = mkOption {
601 description = "Maps kanidm groups to values for the claim.";
603 type = types.attrsOf (types.listOf types.str);
616 config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
620 type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
622 entityList "group" cfg.provision.groups
623 ++ entityList "person" cfg.provision.persons
624 ++ entityList "oauth2" cfg.provision.systems.oauth2;
626 # Accumulate entities by name. Track corresponding entity types for later duplicate check.
627 entitiesByName = foldl' (
628 acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
634 knownGroups = attrNames (filterPresent cfg.provision.groups);
635 unknownGroups = subtractLists knownGroups groups;
638 assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ];
639 message = "${opt} refers to unknown groups: ${toString unknownGroups}";
642 assertEntitiesKnown =
645 unknownEntities = subtractLists (attrNames entitiesByName) entities;
648 assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ];
649 message = "${opt} refers to unknown entities: ${toString unknownEntities}";
656 || ((cfg.serverSettings.tls_chain or null) == null)
657 || (!isStorePath cfg.serverSettings.tls_chain);
659 <option>services.kanidm.serverSettings.tls_chain</option> points to
660 a file in the Nix store. You should use a quoted absolute path to
667 || ((cfg.serverSettings.tls_key or null) == null)
668 || (!isStorePath cfg.serverSettings.tls_key);
670 <option>services.kanidm.serverSettings.tls_key</option> points to
671 a file in the Nix store. You should use a quoted absolute path to
676 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
678 <option>services.kanidm.clientSettings</option> needs to be configured
679 if the client is enabled.
683 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
685 <option>services.kanidm.clientSettings</option> needs to be configured
686 for the PAM daemon to connect to the Kanidm server.
693 cfg.serverSettings.domain == null
694 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"
697 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
698 is not a ReadOnlyReplica. Otherwise the db would inherit it from
699 the instance it follows.
703 assertion = cfg.provision.enable -> cfg.enableServer;
704 message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
706 # If any secret is provisioned, the kanidm package must have some required patches applied to it
712 cfg.provision.adminPasswordFile != null
713 || cfg.provision.idmAdminPasswordFile != null
714 || any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2))
717 -> cfg.package.enableSecretProvisioning;
719 Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches.
720 You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
723 # Entity names must be globally unique:
726 # Filter all names that occurred in more than one entity type.
727 duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName;
730 assertion = cfg.provision.enable -> duplicateNames == { };
732 services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
734 mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames
739 ++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
741 assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
743 ++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
745 assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
748 flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
751 (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
752 attrNames oauth2Cfg.scopeMaps
754 (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
755 attrNames oauth2Cfg.supplementaryScopeMaps
759 flip mapAttrsToList oauth2Cfg.claimMaps (
761 (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
762 attrNames claimCfg.valuesByGroup
764 # At least one group must map to a value in each claim map
767 (cfg.provision.enable && cfg.enableServer)
768 -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
769 message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
771 # Public clients cannot define a basic secret
774 (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
775 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
777 # Public clients cannot disable PKCE
780 (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
781 -> !oauth2Cfg.allowInsecureClientDisablePkce;
782 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
784 # Non-public clients cannot enable localhost redirects
787 (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
788 -> !oauth2Cfg.enableLocalhostRedirects;
789 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
797 environment.systemPackages = mkIf cfg.enableClient [ cfg.package ];
799 systemd.tmpfiles.settings."10-kanidm" = {
800 ${cfg.serverSettings.online_backup.path}.d = {
807 systemd.services.kanidm = mkIf cfg.enableServer {
808 description = "kanidm identity management daemon";
809 wantedBy = [ "multi-user.target" ];
810 after = [ "network.target" ];
811 serviceConfig = mkMerge [
812 # Merge paths and ignore existing prefixes needs to sidestep mkMerge
816 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
820 StateDirectory = "kanidm";
821 StateDirectoryMode = "0700";
822 RuntimeDirectory = "kanidmd";
823 ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
824 ExecStartPost = mkIf cfg.provision.enable postStartScript;
829 # To create the socket
830 "/run/kanidmd:/run/kanidmd"
832 cfg.serverSettings.online_backup.path
835 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
836 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
837 # This would otherwise override the CAP_NET_BIND_SERVICE capability.
838 PrivateUsers = mkForce false;
839 # Port needs to be exposed to the host network
840 PrivateNetwork = mkForce false;
841 RestrictAddressFamilies = [
846 TemporaryFileSystem = "/:ro";
849 environment.RUST_LOG = "info";
852 systemd.services.kanidm-unixd = mkIf cfg.enablePam {
853 description = "Kanidm PAM daemon";
854 wantedBy = [ "multi-user.target" ];
855 after = [ "network.target" ];
860 serviceConfig = mkMerge [
863 CacheDirectory = "kanidm-unixd";
864 CacheDirectoryMode = "0700";
865 RuntimeDirectory = "kanidm-unixd";
866 ExecStart = "${cfg.package}/bin/kanidm_unixd";
867 User = "kanidm-unixd";
868 Group = "kanidm-unixd";
870 BindReadOnlyPaths = [
872 "-/etc/static/kanidm"
879 # To create the socket
880 "/run/kanidm-unixd:/var/run/kanidm-unixd"
882 # Needs to connect to kanidmd
883 PrivateNetwork = mkForce false;
884 RestrictAddressFamilies = [
889 TemporaryFileSystem = "/:ro";
892 environment.RUST_LOG = "info";
895 systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam {
896 description = "Kanidm PAM home management daemon";
897 wantedBy = [ "multi-user.target" ];
900 "kanidm-unixd.service"
902 partOf = [ "kanidm-unixd.service" ];
908 ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
910 BindReadOnlyPaths = [
913 "-/etc/nsswitch.conf"
917 "-/etc/static/kanidm"
920 # To manage home directories
922 # To connect to kanidm-unixd
923 "/run/kanidm-unixd:/var/run/kanidm-unixd"
925 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
926 CapabilityBoundingSet = [
930 "CAP_DAC_READ_SEARCH"
932 IPAddressDeny = "any";
933 # Need access to users
934 PrivateUsers = false;
935 # Need access to home directories
937 RestrictAddressFamilies = [ "AF_UNIX" ];
938 TemporaryFileSystem = "/:ro";
939 Restart = "on-failure";
941 environment.RUST_LOG = "info";
944 # These paths are hardcoded
945 environment.etc = mkMerge [
946 (mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; })
947 (mkIf options.services.kanidm.clientSettings.isDefined {
948 "kanidm/config".source = clientConfigFile;
950 (mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; })
953 system.nssModules = mkIf cfg.enablePam [ cfg.package ];
955 system.nssDatabases.group = optional cfg.enablePam "kanidm";
956 system.nssDatabases.passwd = optional cfg.enablePam "kanidm";
958 users.groups = mkMerge [
959 (mkIf cfg.enableServer { kanidm = { }; })
960 (mkIf cfg.enablePam { kanidm-unixd = { }; })
962 users.users = mkMerge [
963 (mkIf cfg.enableServer {
965 description = "Kanidm server";
968 packages = [ cfg.package ];
971 (mkIf cfg.enablePam {
973 description = "Kanidm PAM daemon";
975 group = "kanidm-unixd";
981 meta.maintainers = with lib.maintainers; [
986 meta.buildDocsInSandbox = false;