{ 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"]) ]; }