Co-Authored-By: Martin Weinelt <mweinelt@users.noreply.github.com> Co-Authored-By: Flakebi <flakebi@t-online.de>main
parent
3b5fc1fde1
commit
c126babb28
@ -0,0 +1,345 @@ |
|||||||
|
{ 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 <literal>null</literal> 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 <literal>domain</literal> that Kanidm manages. Must be below or equal to the domain |
||||||
|
specified in <literal>serverSettings.origin</literal>. |
||||||
|
This can be left at <literal>null</literal>, only if your instance has the role <literal>ReadOnlyReplica</literal>. |
||||||
|
While it is possible to change the domain later on, it requires extra steps! |
||||||
|
Please consider the warnings and execute the steps described |
||||||
|
<link xlink:href="https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain">in the documentation</link>. |
||||||
|
''; |
||||||
|
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 |
||||||
|
<link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md">the documentation</link> |
||||||
|
and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/server.toml">example configuration</link> |
||||||
|
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 |
||||||
|
<link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration">the documentation</link> |
||||||
|
and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/config">example configuration</link> |
||||||
|
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 <link xlink:href="https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon">the documentation</link> |
||||||
|
and <link xlink:href="https://github.com/kanidm/kanidm/blob/master/examples/unixd">example configuration</link> |
||||||
|
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 = '' |
||||||
|
<option>services.kanidm.serverSettings.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) || (!lib.isStorePath cfg.serverSettings.tls_key); |
||||||
|
message = '' |
||||||
|
<option>services.kanidm.serverSettings.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; |
||||||
|
message = '' |
||||||
|
<option>services.kanidm.clientSettings</option> needs to be configured |
||||||
|
if the client is enabled. |
||||||
|
''; |
||||||
|
} |
||||||
|
{ |
||||||
|
assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; |
||||||
|
message = '' |
||||||
|
<option>services.kanidm.clientSettings</option> 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 = '' |
||||||
|
<option>services.kanidm.serverSettings.domain</option> 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; |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
import ./make-test-python.nix ({ pkgs, ... }: |
||||||
|
let |
||||||
|
certs = import ./common/acme/server/snakeoil-certs.nix; |
||||||
|
serverDomain = certs.domain; |
||||||
|
in |
||||||
|
{ |
||||||
|
name = "kanidm"; |
||||||
|
meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi ]; |
||||||
|
|
||||||
|
nodes.server = { config, pkgs, lib, ... }: { |
||||||
|
services.kanidm = { |
||||||
|
enableServer = true; |
||||||
|
serverSettings = { |
||||||
|
origin = "https://${serverDomain}"; |
||||||
|
domain = serverDomain; |
||||||
|
bindaddress = "[::1]:8443"; |
||||||
|
ldapbindaddress = "[::1]:636"; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
services.nginx = { |
||||||
|
enable = true; |
||||||
|
recommendedProxySettings = true; |
||||||
|
virtualHosts."${serverDomain}" = { |
||||||
|
forceSSL = true; |
||||||
|
sslCertificate = certs."${serverDomain}".cert; |
||||||
|
sslCertificateKey = certs."${serverDomain}".key; |
||||||
|
locations."/".proxyPass = "http://[::1]:8443"; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
security.pki.certificateFiles = [ certs.ca.cert ]; |
||||||
|
|
||||||
|
networking.hosts."::1" = [ serverDomain ]; |
||||||
|
networking.firewall.allowedTCPPorts = [ 80 443 ]; |
||||||
|
|
||||||
|
users.users.kanidm.shell = pkgs.bashInteractive; |
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [ kanidm openldap ripgrep ]; |
||||||
|
}; |
||||||
|
|
||||||
|
nodes.client = { pkgs, nodes, ... }: { |
||||||
|
services.kanidm = { |
||||||
|
enableClient = true; |
||||||
|
clientSettings = { |
||||||
|
uri = "https://${serverDomain}"; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
networking.hosts."${nodes.server.config.networking.primaryIPAddress}" = [ serverDomain ]; |
||||||
|
|
||||||
|
security.pki.certificateFiles = [ certs.ca.cert ]; |
||||||
|
}; |
||||||
|
|
||||||
|
testScript = { nodes, ... }: |
||||||
|
let |
||||||
|
ldapBaseDN = builtins.concatStringsSep "," (map (s: "dc=" + s) (pkgs.lib.splitString "." serverDomain)); |
||||||
|
|
||||||
|
# We need access to the config file in the test script. |
||||||
|
filteredConfig = pkgs.lib.converge |
||||||
|
(pkgs.lib.filterAttrsRecursive (_: v: v != null)) |
||||||
|
nodes.server.config.services.kanidm.serverSettings; |
||||||
|
serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig; |
||||||
|
|
||||||
|
in |
||||||
|
'' |
||||||
|
start_all() |
||||||
|
server.wait_for_unit("kanidm.service") |
||||||
|
server.wait_until_succeeds("curl -sf https://${serverDomain} | grep Kanidm") |
||||||
|
server.wait_until_succeeds("ldapsearch -H ldap://[::1]:636 -b '${ldapBaseDN}' -x '(name=test)'") |
||||||
|
client.wait_until_succeeds("kanidm login -D anonymous && kanidm self whoami | grep anonymous@${serverDomain}") |
||||||
|
(rv, result) = server.execute("kanidmd recover_account -d quiet -c ${serverConfigFile} -n admin 2>&1 | rg -o '[A-Za-z0-9]{48}'") |
||||||
|
assert rv == 0 |
||||||
|
''; |
||||||
|
}) |
Loading…
Reference in new issue