nixpkgs/nixos/modules/config/ldap.nix

310 lines
10 KiB
Nix

{ config, lib, pkgs, ... }:
let
inherit (lib) mkEnableOption mkIf mkMerge mkOption mkRenamedOptionModule types;
cfg = config.users.ldap;
# Careful: OpenLDAP seems to be very picky about the indentation of
# this file. Directives HAVE to start in the first column!
ldapConfig = {
target = "ldap.conf";
source = pkgs.writeText "ldap.conf" ''
uri ${config.users.ldap.server}
base ${config.users.ldap.base}
timelimit ${toString config.users.ldap.timeLimit}
bind_timelimit ${toString config.users.ldap.bind.timeLimit}
bind_policy ${config.users.ldap.bind.policy}
${lib.optionalString config.users.ldap.useTLS ''
ssl start_tls
''}
${lib.optionalString (config.users.ldap.bind.distinguishedName != "") ''
binddn ${config.users.ldap.bind.distinguishedName}
''}
${lib.optionalString (cfg.extraConfig != "") cfg.extraConfig }
'';
};
nslcdConfig = pkgs.writeText "nslcd.conf" ''
uri ${cfg.server}
base ${cfg.base}
timelimit ${toString cfg.timeLimit}
bind_timelimit ${toString cfg.bind.timeLimit}
${lib.optionalString (cfg.bind.distinguishedName != "")
"binddn ${cfg.bind.distinguishedName}" }
${lib.optionalString (cfg.daemon.rootpwmoddn != "")
"rootpwmoddn ${cfg.daemon.rootpwmoddn}" }
${lib.optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig }
'';
# nslcd normally reads configuration from /etc/nslcd.conf.
# this file might contain secrets. We append those at runtime,
# so redirect its location to something more temporary.
nslcdWrapped = pkgs.runCommand "nslcd-wrapped" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
mkdir -p $out/bin
makeWrapper ${pkgs.nss_pam_ldapd}/sbin/nslcd $out/bin/nslcd \
--set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so" \
--set NIX_REDIRECTS "/etc/nslcd.conf=/run/nslcd/nslcd.conf"
'';
in
{
###### interface
options = {
users.ldap = {
enable = mkEnableOption "authentication against an LDAP server";
loginPam = mkOption {
type = types.bool;
default = true;
description = "Whether to include authentication against LDAP in login PAM.";
};
nsswitch = mkOption {
type = types.bool;
default = true;
description = "Whether to include lookup against LDAP in NSS.";
};
server = mkOption {
type = types.str;
example = "ldap://ldap.example.org/";
description = "The URL of the LDAP server.";
};
base = mkOption {
type = types.str;
example = "dc=example,dc=org";
description = "The distinguished name of the search base.";
};
useTLS = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, use TLS (encryption) over an LDAP (port 389)
connection. The alternative is to specify an LDAPS server (port
636) in {option}`users.ldap.server` or to forego
security.
'';
};
timeLimit = mkOption {
default = 0;
type = types.int;
description = ''
Specifies the time limit (in seconds) to use when performing
searches. A value of zero (0), which is the default, is to
wait indefinitely for searches to be completed.
'';
};
daemon = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to let the nslcd daemon (nss-pam-ldapd) handle the
LDAP lookups for NSS and PAM. This can improve performance,
and if you need to bind to the LDAP server with a password,
it increases security, since only the nslcd user needs to
have access to the bindpw file, not everyone that uses NSS
and/or PAM. If this option is enabled, a local nscd user is
created automatically, and the nslcd service is started
automatically when the network get up.
'';
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration options that will be added verbatim at
the end of the nslcd configuration file (`nslcd.conf(5)`).
'';
} ;
rootpwmoddn = mkOption {
default = "";
example = "cn=admin,dc=example,dc=com";
type = types.str;
description = ''
The distinguished name to use to bind to the LDAP server
when the root user tries to modify a user's password.
'';
};
rootpwmodpwFile = mkOption {
default = "";
example = "/run/keys/nslcd.rootpwmodpw";
type = types.str;
description = ''
The path to a file containing the credentials with which to bind to
the LDAP server if the root user tries to change a user's password.
'';
};
};
bind = {
distinguishedName = mkOption {
default = "";
example = "cn=admin,dc=example,dc=com";
type = types.str;
description = ''
The distinguished name to bind to the LDAP server with. If this
is not specified, an anonymous bind will be done.
'';
};
passwordFile = mkOption {
default = "/etc/ldap/bind.password";
type = types.str;
description = ''
The path to a file containing the credentials to use when binding
to the LDAP server (if not binding anonymously).
'';
};
timeLimit = mkOption {
default = 30;
type = types.int;
description = ''
Specifies the time limit (in seconds) to use when connecting
to the directory server. This is distinct from the time limit
specified in {option}`users.ldap.timeLimit` and affects
the initial server connection only.
'';
};
policy = mkOption {
default = "hard_open";
type = types.enum [ "hard_open" "hard_init" "soft" ];
description = ''
Specifies the policy to use for reconnecting to an unavailable
LDAP server. The default is `hard_open`, which
reconnects if opening the connection to the directory server
failed. By contrast, `hard_init` reconnects if
initializing the connection failed. Initializing may not
actually contact the directory server, and it is possible that
a malformed configuration file will trigger reconnection. If
`soft` is specified, then
`nss_ldap` will return immediately on server
failure. All hard reconnect policies block with exponential
backoff before retrying.
'';
};
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration options that will be added verbatim at
the end of the ldap configuration file (`ldap.conf(5)`).
If {option}`users.ldap.daemon` is enabled, this
configuration will not be used. In that case, use
{option}`users.ldap.daemon.extraConfig` instead.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
environment.etc = lib.optionalAttrs (!cfg.daemon.enable) {
"ldap.conf" = ldapConfig;
};
system.nssModules = mkIf cfg.nsswitch (lib.singleton (
if cfg.daemon.enable then pkgs.nss_pam_ldapd else pkgs.nss_ldap
));
system.nssDatabases.group = lib.optional cfg.nsswitch "ldap";
system.nssDatabases.passwd = lib.optional cfg.nsswitch "ldap";
system.nssDatabases.shadow = lib.optional cfg.nsswitch "ldap";
users = mkIf cfg.daemon.enable {
groups.nslcd = {
gid = config.ids.gids.nslcd;
};
users.nslcd = {
uid = config.ids.uids.nslcd;
description = "nslcd user.";
group = "nslcd";
};
};
systemd.services = mkMerge [
(mkIf (!cfg.daemon.enable) {
ldap-password = {
wantedBy = [ "sysinit.target" ];
before = [ "sysinit.target" "shutdown.target" ];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
script = ''
if test -f "${cfg.bind.passwordFile}" ; then
umask 0077
conf="$(mktemp)"
printf 'bindpw %s\n' "$(cat ${cfg.bind.passwordFile})" |
cat ${ldapConfig.source} - >"$conf"
mv -fT "$conf" /etc/ldap.conf
fi
'';
};
})
(mkIf cfg.daemon.enable {
nslcd = {
wantedBy = [ "multi-user.target" ];
preStart = ''
umask 0077
conf="$(mktemp)"
{
cat ${nslcdConfig}
test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.passwordFile}' ||
printf 'bindpw %s\n' "$(cat '${cfg.bind.passwordFile}')"
test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpwFile}' ||
printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpwFile}')"
} >"$conf"
mv -fT "$conf" /run/nslcd/nslcd.conf
'';
restartTriggers = [
nslcdConfig
cfg.bind.passwordFile
cfg.daemon.rootpwmodpwFile
];
serviceConfig = {
ExecStart = "${nslcdWrapped}/bin/nslcd";
Type = "forking";
Restart = "always";
User = "nslcd";
Group = "nslcd";
RuntimeDirectory = [ "nslcd" ];
PIDFile = "/run/nslcd/nslcd.pid";
AmbientCapabilities = "CAP_SYS_RESOURCE";
};
};
})
];
};
imports =
[ (mkRenamedOptionModule [ "users" "ldap" "bind" "password"] [ "users" "ldap" "bind" "passwordFile"])
];
}