Unverified Commit 72a5cad1 authored by Adam C. Stephens's avatar Adam C. Stephens Committed by GitHub
Browse files

kanidm: refactor option organization and integrate ssh (#485079)

parents 8ea8ad0a 7ae6c9bf
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
@@ -149,6 +149,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