Commit 7ae6c9bf authored by rkuklik's avatar rkuklik
Browse files

kanidm: refactor option organization and integrate ssh

Change option names to be consistent with other NixOS modules and
option to integrate with Kanidm SSH keys when using the unix module.
parent 5be56669
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -105,7 +105,7 @@ In addition to numerous new and upgraded packages, this release has the followin

- [K40-Whisperer](https://www.scorchworks.com/K40whisperer/k40whisperer.html), a program to control cheap Chinese laser cutters. Available as [programs.k40-whisperer.enable](#opt-programs.k40-whisperer.enable). Users must add themselves to the `k40` group to be able to access the device.

- [kanidm](https://kanidm.github.io/kanidm/stable/), an identity management server written in Rust. Available as [services.kanidm](#opt-services.kanidm.enableServer)
- [kanidm](https://kanidm.github.io/kanidm/stable/), an identity management server written in Rust. Available as [services.kanidm](#opt-services.kanidm.server.enable) (renamed to `services.kanidm.server.enable` in 26.05).

- [Maddy](https://maddy.email/), a free an open source mail server. Available as [services.maddy](#opt-services.maddy.enable).

+5 −0
Original line number Diff line number Diff line
@@ -140,6 +140,11 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.

- `services.slurm` now supports slurmrestd usage through the `services.slurm.rest` NixOS options.

- `services.kanidm` options for server, client and unix were moved under dedicated namespaces.
  For each component `enableComponent` and `componentSettings` are now `component.enable` and
  `component.settings`. The unix module now supports using SSH keys from Kanidm via
  `services.kanidm.unix.sshIntegration = true`.

- `glibc` has been updated to version 2.42.

  This version no longer makes the stack executable when a shared library requires this. A symptom
+5 −5
Original line number Diff line number Diff line
@@ -772,7 +772,7 @@ let
              }
              {
                name = "kanidm";
                enable = config.services.kanidm.enablePam;
                enable = config.services.kanidm.unix.enable;
                control = "sufficient";
                modulePath = "${config.services.kanidm.package}/lib/pam_kanidm.so";
                settings = {
@@ -1139,7 +1139,7 @@ let
                }
                {
                  name = "kanidm";
                  enable = config.services.kanidm.enablePam;
                  enable = config.services.kanidm.unix.enable;
                  control = "sufficient";
                  modulePath = "${config.services.kanidm.package}/lib/pam_kanidm.so";
                  settings = {
@@ -1248,7 +1248,7 @@ let
              }
              {
                name = "kanidm";
                enable = config.services.kanidm.enablePam;
                enable = config.services.kanidm.unix.enable;
                control = "sufficient";
                modulePath = "${config.services.kanidm.package}/lib/pam_kanidm.so";
              }
@@ -1412,7 +1412,7 @@ let
              }
              {
                name = "kanidm";
                enable = config.services.kanidm.enablePam;
                enable = config.services.kanidm.unix.enable;
                control = "optional";
                modulePath = "${config.services.kanidm.package}/lib/pam_kanidm.so";
              }
@@ -2339,7 +2339,7 @@ in
      # Include the PAM modules in the system path mostly for the manpages.
      [ package ]
      ++ lib.optional config.users.ldap.enable pam_ldap
      ++ lib.optional config.services.kanidm.enablePam config.services.kanidm.package
      ++ lib.optional config.services.kanidm.unix.enable config.services.kanidm.package
      ++ lib.optional config.services.sssd.enable pkgs.sssd
      ++ lib.optionals config.security.pam.krb5.enable [
        pam_krb5
+115 −64
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ let
    mkOption
    mkOrder
    mkPackageOption
    mkRenamedOptionModule
    optional
    optionals
    optionalString
@@ -44,9 +45,9 @@ let
  settingsFormat = pkgs.formats.toml { };
  # Remove null values, so we can document optional values that don't end up in the generated TOML file.
  filterConfig = converge (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);
  serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.server.settings);
  clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.client.settings);
  unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unix.settings);
  provisionSecretFiles = filter (x: x != null) (
    [
      cfg.provision.idmAdminPasswordFile
@@ -56,11 +57,11 @@ let
    ++ mapAttrsToList (_: x: x.basicSecretFile) cfg.provision.systems.oauth2
  );
  secretPaths = [
    cfg.serverSettings.tls_chain
    cfg.serverSettings.tls_key
    cfg.server.settings.tls_chain
    cfg.server.settings.tls_key
  ]
  ++ optionals cfg.provision.enable provisionSecretFiles;
  enableServerBackup = cfg.enableServer && (cfg.serverSettings.online_backup.versions != 0);
  enableServerBackup = cfg.server.enable && (cfg.server.settings.online_backup.versions != 0);

  # Merge bind mount paths and remove paths where a prefix is already mounted.
  # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
@@ -218,7 +219,7 @@ let

  serverPort =
    let
      address = cfg.serverSettings.bindaddress;
      address = cfg.server.settings.bindaddress;
    in
    # ipv6:
    if hasInfix "]:" address then
@@ -230,19 +231,42 @@ let
    # default is 8443
    else
      throw "Address not parseable as IPv4 nor IPv6.";

  addKaniSshKeys = cfg.unix.enable && cfg.unix.sshIntegration;
in
{
  options.services.kanidm = {
    enableClient = mkEnableOption "the Kanidm client";
    enableServer = mkEnableOption "the Kanidm server";
    enablePam = mkEnableOption "the Kanidm PAM and NSS integration";
  imports = [
    (mkRenamedOptionModule
      [ "services" "kanidm" "enableServer" ]
      [ "services" "kanidm" "server" "enable" ]
    )
    (mkRenamedOptionModule
      [ "services" "kanidm" "serverSettings" ]
      [ "services" "kanidm" "server" "settings" ]
    )
    (mkRenamedOptionModule
      [ "services" "kanidm" "enableClient" ]
      [ "services" "kanidm" "client" "enable" ]
    )
    (mkRenamedOptionModule
      [ "services" "kanidm" "clientSettings" ]
      [ "services" "kanidm" "client" "settings" ]
    )
    (mkRenamedOptionModule [ "services" "kanidm" "enablePam" ] [ "services" "kanidm" "unix" "enable" ])
    (mkRenamedOptionModule
      [ "services" "kanidm" "unixSettings" ]
      [ "services" "kanidm" "unix" "settings" ]
    )
  ];

  options.services.kanidm = {
    package = mkPackageOption pkgs "kanidm" {
      example = "kanidm_1_8";
      extraDescription = "Must be set to an explicit version.";
    };

    serverSettings = mkOption {
    server.enable = mkEnableOption "the Kanidm server";
    server.settings = mkOption {
      type = types.submodule {
        freeformType = settingsFormat.type;

@@ -270,7 +294,7 @@ in
          domain = mkOption {
            description = ''
              The `domain` that Kanidm manages. Must be below or equal to the domain
              specified in `serverSettings.origin`.
              specified in `server.settings.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
@@ -345,7 +369,8 @@ in
      '';
    };

    clientSettings = mkOption {
    client.enable = mkEnableOption "the Kanidm client";
    client.settings = mkOption {
      type = types.submodule {
        freeformType = settingsFormat.type;

@@ -363,7 +388,11 @@ in
      '';
    };

    unixSettings = mkOption {
    unix = {
      enable = mkEnableOption "the Kanidm PAM and NSS integration";
      sshIntegration = mkEnableOption "Kanidm SSH keys login";
    };
    unix.settings = mkOption {
      type = types.submodule {
        freeformType = settingsFormat.type;

@@ -394,7 +423,7 @@ in
      instanceUrl = mkOption {
        description = "The instance url to which the provisioning tool should connect.";
        default = "https://localhost:${serverPort}";
        defaultText = "https://localhost:<port from serverSettings.bindaddress>";
        defaultText = "https://localhost:<port from server.settings.bindaddress>";
        type = types.str;
      };

@@ -671,11 +700,11 @@ in
    };
  };

  config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
  config = mkIf (cfg.client.enable || cfg.server.enable || cfg.unix.enable) {
    warnings = lib.optionals (cfg.package.eolMessage != "") [ cfg.package.eolMessage ];
    services.kanidm = {
      unixSettings.version = "2";
      serverSettings.version = "2";
      unix.settings.version = "2";
      server.settings.version = "2";
    };

    assertions =
@@ -699,7 +728,7 @@ in
            unknownGroups = subtractLists knownGroups groups;
          in
          {
            assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ];
            assertion = (cfg.server.enable && cfg.provision.enable) -> unknownGroups == [ ];
            message = "${opt} refers to unknown groups: ${toString unknownGroups}";
          };

@@ -709,71 +738,71 @@ in
            unknownEntities = subtractLists (attrNames entitiesByName) entities;
          in
          {
            assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ];
            assertion = (cfg.server.enable && cfg.provision.enable) -> unknownEntities == [ ];
            message = "${opt} refers to unknown entities: ${toString unknownEntities}";
          };
      in
      [
        {
          assertion = cfg.enablePam -> !(cfg.unixSettings ? pam_allowed_login_groups);
          assertion = cfg.unix.enable -> !(cfg.unix.settings ? pam_allowed_login_groups);
          message = ''
            <option>services.kanidm.unixSettings.pam_allowed_login_groups</option> has been renamed
            to <option>services.kanidm.unixSettings.kanidm.pam_allowed_login_groups</option>.
            <option>services.kanidm.unix.settings.pam_allowed_login_groups</option> has been renamed
            to <option>services.kanidm.unix.settings.kanidm.pam_allowed_login_groups</option>.
            Please change your usage.
          '';
        }
        {
          assertion =
            !cfg.enableServer
            || ((cfg.serverSettings.tls_chain or null) == null)
            || (!isStorePath cfg.serverSettings.tls_chain);
            !cfg.server.enable
            || ((cfg.server.settings.tls_chain or null) == null)
            || (!isStorePath cfg.server.settings.tls_chain);
          message = ''
            <option>services.kanidm.serverSettings.tls_chain</option> points to
            <option>services.kanidm.server.settings.tls_chain</option> 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)
            || (!isStorePath cfg.serverSettings.tls_key);
            !cfg.server.enable
            || ((cfg.server.settings.tls_key or null) == null)
            || (!isStorePath cfg.server.settings.tls_key);
          message = ''
            <option>services.kanidm.serverSettings.tls_key</option> points to
            <option>services.kanidm.server.settings.tls_key</option> 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;
          assertion = !cfg.client.enable || options.services.kanidm.client.settings.isDefined;
          message = ''
            <option>services.kanidm.clientSettings</option> needs to be configured
            <option>services.kanidm.client.settings</option> needs to be configured
            if the client is enabled.
          '';
        }
        {
          assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
          assertion = !cfg.unix.enable || options.services.kanidm.client.settings.isDefined;
          message = ''
            <option>services.kanidm.clientSettings</option> needs to be configured
            <option>services.kanidm.client.settings</option> needs to be configured
            for the PAM daemon to connect to the Kanidm server.
          '';
        }
        {
          assertion =
            !cfg.enableServer
            !cfg.server.enable
            || (
              cfg.serverSettings.domain == null
              -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"
              cfg.server.settings.domain == null
              -> cfg.server.settings.role == "WriteReplica" || cfg.server.settings.role == "WriteReplicaNoUI"
            );
          message = ''
            <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
            <option>services.kanidm.server.settings.domain</option> can only be set if this instance
            is not a ReadOnlyReplica. Otherwise the db would inherit it from
            the instance it follows.
          '';
        }
        {
          assertion = cfg.provision.enable -> cfg.enableServer;
          message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
          assertion = cfg.provision.enable -> cfg.server.enable;
          message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.server.enable</option> to be true";
        }
        # If any secret is provisioned, the kanidm package must have some required patches applied to it
        {
@@ -844,27 +873,28 @@ in
                # At least one group must map to a value in each claim map
                (mkIf (cfg.provision.extraJsonFile == null) {
                  assertion =
                    (cfg.provision.enable && cfg.enableServer)
                    (cfg.provision.enable && cfg.server.enable)
                    -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
                  message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
                })
                # Public clients cannot define a basic secret
                {
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
                    (cfg.provision.enable && cfg.server.enable && oauth2Cfg.public)
                    -> oauth2Cfg.basicSecretFile == null;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
                }
                # Public clients cannot disable PKCE
                {
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
                    (cfg.provision.enable && cfg.server.enable && oauth2Cfg.public)
                    -> !oauth2Cfg.allowInsecureClientDisablePkce;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
                }
                # Non-public clients cannot enable localhost redirects
                {
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
                    (cfg.provision.enable && cfg.server.enable && !oauth2Cfg.public)
                    -> !oauth2Cfg.enableLocalhostRedirects;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
                }
@@ -874,17 +904,17 @@ in
        )
      );

    environment.systemPackages = mkIf cfg.enableClient [ cfg.package ];
    environment.systemPackages = mkIf cfg.client.enable [ cfg.package ];

    systemd.tmpfiles.settings."10-kanidm" = mkIf enableServerBackup {
      ${cfg.serverSettings.online_backup.path}.d = {
      ${cfg.server.settings.online_backup.path}.d = {
        mode = "0700";
        user = "kanidm";
        group = "kanidm";
      };
    };

    systemd.services.kanidm = mkIf cfg.enableServer {
    systemd.services.kanidm = mkIf cfg.server.enable {
      description = "kanidm identity management daemon";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
@@ -915,10 +945,10 @@ in
          BindPaths =
            [ ]
            # To store backups
            ++ optional enableServerBackup cfg.serverSettings.online_backup.path
            ++ optional enableServerBackup cfg.server.settings.online_backup.path
            ++ optional (
              cfg.enablePam && cfg.unixSettings ? home_mount_prefix
            ) cfg.unixSettings.home_mount_prefix;
              cfg.unix.enable && cfg.unix.settings ? home_mount_prefix
            ) cfg.unix.settings.home_mount_prefix;

          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
          CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
@@ -936,7 +966,7 @@ in
      ];
    };

    systemd.services.kanidm-unixd = mkIf cfg.enablePam {
    systemd.services.kanidm-unixd = mkIf cfg.unix.enable {
      description = "Kanidm PAM daemon";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
@@ -979,7 +1009,7 @@ in
      environment.RUST_LOG = "info";
    };

    systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam {
    systemd.services.kanidm-unixd-tasks = mkIf cfg.unix.enable {
      description = "Kanidm PAM home management daemon";
      wantedBy = [ "multi-user.target" ];
      after = [
@@ -1033,25 +1063,26 @@ in

    # These paths are hardcoded
    environment.etc = mkMerge [
      (mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; })
      (mkIf options.services.kanidm.clientSettings.isDefined {
      (mkIf cfg.server.enable { "kanidm/server.toml".source = serverConfigFile; })
      (mkIf options.services.kanidm.client.settings.isDefined {
        "kanidm/config".source = clientConfigFile;
      })
      (mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; })
      (mkIf cfg.unix.enable { "kanidm/unixd".source = unixConfigFile; })
    ];

    system.nssModules = mkIf cfg.enablePam [ cfg.package ];
    system.nssModules = mkIf cfg.unix.enable [ cfg.package ];

    # Needs to be before "files" which is `mkBefore`
    system.nssDatabases.group = mkOrder 490 (optional cfg.enablePam "kanidm");
    system.nssDatabases.passwd = mkOrder 490 (optional cfg.enablePam "kanidm");
    system.nssDatabases.group = mkOrder 490 (optional cfg.unix.enable "kanidm");
    system.nssDatabases.passwd = mkOrder 490 (optional cfg.unix.enable "kanidm");

    users.groups = mkMerge [
      (mkIf cfg.enableServer { kanidm = { }; })
      (mkIf cfg.enablePam { kanidm-unixd = { }; })
      (mkIf cfg.server.enable { kanidm = { }; })
      (mkIf cfg.unix.enable { kanidm-unixd = { }; })
      (mkIf addKaniSshKeys { kanidm-authorized-keys = { }; })
    ];
    users.users = mkMerge [
      (mkIf cfg.enableServer {
      (mkIf cfg.server.enable {
        kanidm = {
          description = "Kanidm server";
          isSystemUser = true;
@@ -1059,14 +1090,34 @@ in
          packages = [ cfg.package ];
        };
      })
      (mkIf cfg.enablePam {
      (mkIf cfg.unix.enable {
        kanidm-unixd = {
          description = "Kanidm PAM daemon";
          isSystemUser = true;
          group = "kanidm-unixd";
        };
      })
      (mkIf addKaniSshKeys {
        kanidm-authorized-keys = {
          description = "Kanidm authorized keys delegate";
          isSystemUser = true;
          group = "kanidm-authorized-keys";
        };
      })
    ];

    # AuthorizedKeysCommand is displeased with `/nix/store` permissions
    services.openssh.settings = mkIf addKaniSshKeys {
      AuthorizedKeysCommand = "${config.security.wrapperDir}/kanidm_ssh_authorizedkeys %u";
      AuthorizedKeysCommandUser = "kanidm-authorized-keys";
    };

    security.wrappers.kanidm_ssh_authorizedkeys = mkIf addKaniSshKeys {
      owner = "root";
      group = "root";
      permissions = "a+rx";
      source = "${cfg.package}/bin/kanidm_ssh_authorizedkeys";
    };
  };

  meta.maintainers = with lib.maintainers; [
+18 −14
Original line number Diff line number Diff line
@@ -25,8 +25,9 @@ in
    {
      services.kanidm = {
        package = kanidmPackage;
        enableServer = true;
        serverSettings = {
        server = {
          enable = true;
          settings = {
            origin = "https://${serverDomain}";
            domain = serverDomain;
            bindaddress = "[::]:443";
@@ -34,14 +35,17 @@ in
            tls_chain = "${certsPath}/snakeoil.crt";
            tls_key = "${certsPath}/snakeoil.key";
          };
        };
        # So we can check whether provisioning did what we wanted
        enableClient = true;
        clientSettings = {
        client = {
          enable = true;
          settings = {
            uri = "https://${serverDomain}";
            verify_ca = true;
            verify_hostnames = true;
          };
        };
      };

      specialisation.credentialProvision.configuration =
        { ... }:
@@ -289,7 +293,7 @@ in
      # We need access to the config file in the test script.
      filteredConfig = pkgs.lib.converge (pkgs.lib.filterAttrsRecursive (
        _: v: v != null
      )) nodes.provision.services.kanidm.serverSettings;
      )) nodes.provision.services.kanidm.server.settings;
      serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;

      specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
Loading