{ config, lib, options, pkgs, ... }: let 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 = lib.converge (lib.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); 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"; }; in { options.services.kanidm = { enableClient = lib.mkEnableOption "the Kanidm client"; enableServer = lib.mkEnableOption "the Kanidm server"; enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration."; serverSettings = lib.mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options = { bindaddress = lib.mkOption { description = "Address/port combination the webserver binds to."; example = "[::1]:8443"; type = lib.types.str; }; # Should be optional but toml does not accept null ldapbindaddress = lib.mkOption { description = '' Address and port the LDAP server is bound to. Setting this to null disables the LDAP interface. ''; example = "[::1]:636"; default = null; type = lib.types.nullOr lib.types.str; }; origin = lib.mkOption { description = "The origin of your Kanidm instance. Must have https as protocol."; example = "https://idm.example.org"; type = lib.types.strMatching "^https://.*"; }; domain = lib.mkOption { description = '' 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. ''; example = "example.org"; default = null; type = lib.types.nullOr lib.types.str; }; db_path = lib.mkOption { description = "Path to Kanidm database."; default = "/var/lib/kanidm/kanidm.db"; readOnly = true; type = lib.types.path; }; log_level = lib.mkOption { description = "Log level of the server."; default = "default"; type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ]; }; role = lib.mkOption { description = "The role of this server. This affects the replication relationship and thereby available features."; default = "WriteReplica"; type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; }; }; }; default = { }; description = '' Settings for Kanidm, see the documentation and example configuration for possible values. ''; }; clientSettings = lib.mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options.uri = lib.mkOption { description = "Address of the Kanidm server."; example = "http://127.0.0.1:8080"; type = lib.types.str; }; }; description = '' Configure Kanidm clients, needed for the PAM daemon. See the documentation and example configuration for possible values. ''; }; unixSettings = lib.mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options.pam_allowed_login_groups = lib.mkOption { description = "Kanidm groups that are allowed to login using PAM."; example = "my_pam_group"; type = lib.types.listOf lib.types.str; }; }; description = '' Configure Kanidm unix daemon. See the documentation and example configuration for possible values. ''; }; }; config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { assertions = [ { assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.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) || (!lib.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. ''; } ]; environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ]; systemd.services.kanidm = lib.mkIf cfg.enableServer { description = "kanidm identity management daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = defaultServiceConfig // { StateDirectory = "kanidm"; StateDirectoryMode = "0700"; ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; User = "kanidm"; Group = "kanidm"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; # This would otherwise override the CAP_NET_BIND_SERVICE capability. PrivateUsers = false; # Port needs to be exposed to the host network PrivateNetwork = false; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; TemporaryFileSystem = "/:ro"; }; environment.RUST_LOG = "info"; }; systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam { description = "Kanidm PAM daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ unixConfigFile clientConfigFile ]; serviceConfig = defaultServiceConfig // { CacheDirectory = "kanidm-unixd"; CacheDirectoryMode = "0700"; RuntimeDirectory = "kanidm-unixd"; ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; User = "kanidm-unixd"; Group = "kanidm-unixd"; BindReadOnlyPaths = [ "/nix/store" "-/etc/resolv.conf" "-/etc/nsswitch.conf" "-/etc/hosts" "-/etc/localtime" "-/etc/kanidm" "-/etc/static/kanidm" ]; BindPaths = [ # To create the socket "/run/kanidm-unixd:/var/run/kanidm-unixd" ]; # Needs to connect to kanidmd PrivateNetwork = false; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; TemporaryFileSystem = "/:ro"; }; environment.RUST_LOG = "info"; }; systemd.services.kanidm-unixd-tasks = lib.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 = "${pkgs.kanidm}/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"; }; environment.RUST_LOG = "info"; }; # These paths are hardcoded environment.etc = lib.mkMerge [ (lib.mkIf options.services.kanidm.clientSettings.isDefined { "kanidm/config".source = clientConfigFile; }) (lib.mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; }) ]; system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ]; system.nssDatabases.group = lib.optional cfg.enablePam "kanidm"; system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; users.groups = lib.mkMerge [ (lib.mkIf cfg.enableServer { kanidm = { }; }) (lib.mkIf cfg.enablePam { kanidm-unixd = { }; }) ]; users.users = lib.mkMerge [ (lib.mkIf cfg.enableServer { kanidm = { description = "Kanidm server"; isSystemUser = true; group = "kanidm"; packages = with pkgs; [ kanidm ]; }; }) (lib.mkIf cfg.enablePam { kanidm-unixd = { description = "Kanidm PAM daemon"; isSystemUser = true; group = "kanidm-unixd"; }; }) ]; }; meta.maintainers = with lib.maintainers; [ erictapen Flakebi ]; meta.buildDocsInSandbox = false; }