1{ config, lib, pkgs, ... }:
2
3with pkgs;
4with lib;
5
6let
7
8 cfg = config.users.ldap;
9
10 # Careful: OpenLDAP seems to be very picky about the indentation of
11 # this file. Directives HAVE to start in the first column!
12 ldapConfig = {
13 target = "ldap.conf";
14 source = writeText "ldap.conf" ''
15 uri ${config.users.ldap.server}
16 base ${config.users.ldap.base}
17 timelimit ${toString config.users.ldap.timeLimit}
18 bind_timelimit ${toString config.users.ldap.bind.timeLimit}
19 bind_policy ${config.users.ldap.bind.policy}
20 ${optionalString config.users.ldap.useTLS ''
21 ssl start_tls
22 ''}
23 ${optionalString (config.users.ldap.bind.distinguishedName != "") ''
24 binddn ${config.users.ldap.bind.distinguishedName}
25 ''}
26 ${optionalString (cfg.extraConfig != "") cfg.extraConfig }
27 '';
28 };
29
30 nslcdConfig = writeText "nslcd.conf" ''
31 uri ${cfg.server}
32 base ${cfg.base}
33 timelimit ${toString cfg.timeLimit}
34 bind_timelimit ${toString cfg.bind.timeLimit}
35 ${optionalString (cfg.bind.distinguishedName != "")
36 "binddn ${cfg.bind.distinguishedName}" }
37 ${optionalString (cfg.daemon.rootpwmoddn != "")
38 "rootpwmoddn ${cfg.daemon.rootpwmoddn}" }
39 ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig }
40 '';
41
42 # nslcd normally reads configuration from /etc/nslcd.conf.
43 # this file might contain secrets. We append those at runtime,
44 # so redirect its location to something more temporary.
45 nslcdWrapped = runCommand "nslcd-wrapped" { nativeBuildInputs = [ makeWrapper ]; } ''
46 mkdir -p $out/bin
47 makeWrapper ${nss_pam_ldapd}/sbin/nslcd $out/bin/nslcd \
48 --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so" \
49 --set NIX_REDIRECTS "/etc/nslcd.conf=/run/nslcd/nslcd.conf"
50 '';
51
52in
53
54{
55
56 ###### interface
57
58 options = {
59
60 users.ldap = {
61
62 enable = mkEnableOption (lib.mdDoc "authentication against an LDAP server");
63
64 loginPam = mkOption {
65 type = types.bool;
66 default = true;
67 description = lib.mdDoc "Whether to include authentication against LDAP in login PAM.";
68 };
69
70 nsswitch = mkOption {
71 type = types.bool;
72 default = true;
73 description = lib.mdDoc "Whether to include lookup against LDAP in NSS.";
74 };
75
76 server = mkOption {
77 type = types.str;
78 example = "ldap://ldap.example.org/";
79 description = lib.mdDoc "The URL of the LDAP server.";
80 };
81
82 base = mkOption {
83 type = types.str;
84 example = "dc=example,dc=org";
85 description = lib.mdDoc "The distinguished name of the search base.";
86 };
87
88 useTLS = mkOption {
89 type = types.bool;
90 default = false;
91 description = lib.mdDoc ''
92 If enabled, use TLS (encryption) over an LDAP (port 389)
93 connection. The alternative is to specify an LDAPS server (port
94 636) in {option}`users.ldap.server` or to forego
95 security.
96 '';
97 };
98
99 timeLimit = mkOption {
100 default = 0;
101 type = types.int;
102 description = lib.mdDoc ''
103 Specifies the time limit (in seconds) to use when performing
104 searches. A value of zero (0), which is the default, is to
105 wait indefinitely for searches to be completed.
106 '';
107 };
108
109 daemon = {
110 enable = mkOption {
111 type = types.bool;
112 default = false;
113 description = lib.mdDoc ''
114 Whether to let the nslcd daemon (nss-pam-ldapd) handle the
115 LDAP lookups for NSS and PAM. This can improve performance,
116 and if you need to bind to the LDAP server with a password,
117 it increases security, since only the nslcd user needs to
118 have access to the bindpw file, not everyone that uses NSS
119 and/or PAM. If this option is enabled, a local nscd user is
120 created automatically, and the nslcd service is started
121 automatically when the network get up.
122 '';
123 };
124
125 extraConfig = mkOption {
126 default = "";
127 type = types.lines;
128 description = lib.mdDoc ''
129 Extra configuration options that will be added verbatim at
130 the end of the nslcd configuration file (`nslcd.conf(5)`).
131 '' ;
132 } ;
133
134 rootpwmoddn = mkOption {
135 default = "";
136 example = "cn=admin,dc=example,dc=com";
137 type = types.str;
138 description = lib.mdDoc ''
139 The distinguished name to use to bind to the LDAP server
140 when the root user tries to modify a user's password.
141 '';
142 };
143
144 rootpwmodpwFile = mkOption {
145 default = "";
146 example = "/run/keys/nslcd.rootpwmodpw";
147 type = types.str;
148 description = lib.mdDoc ''
149 The path to a file containing the credentials with which to bind to
150 the LDAP server if the root user tries to change a user's password.
151 '';
152 };
153 };
154
155 bind = {
156 distinguishedName = mkOption {
157 default = "";
158 example = "cn=admin,dc=example,dc=com";
159 type = types.str;
160 description = lib.mdDoc ''
161 The distinguished name to bind to the LDAP server with. If this
162 is not specified, an anonymous bind will be done.
163 '';
164 };
165
166 passwordFile = mkOption {
167 default = "/etc/ldap/bind.password";
168 type = types.str;
169 description = lib.mdDoc ''
170 The path to a file containing the credentials to use when binding
171 to the LDAP server (if not binding anonymously).
172 '';
173 };
174
175 timeLimit = mkOption {
176 default = 30;
177 type = types.int;
178 description = lib.mdDoc ''
179 Specifies the time limit (in seconds) to use when connecting
180 to the directory server. This is distinct from the time limit
181 specified in {option}`users.ldap.timeLimit` and affects
182 the initial server connection only.
183 '';
184 };
185
186 policy = mkOption {
187 default = "hard_open";
188 type = types.enum [ "hard_open" "hard_init" "soft" ];
189 description = lib.mdDoc ''
190 Specifies the policy to use for reconnecting to an unavailable
191 LDAP server. The default is `hard_open`, which
192 reconnects if opening the connection to the directory server
193 failed. By contrast, `hard_init` reconnects if
194 initializing the connection failed. Initializing may not
195 actually contact the directory server, and it is possible that
196 a malformed configuration file will trigger reconnection. If
197 `soft` is specified, then
198 `nss_ldap` will return immediately on server
199 failure. All hard reconnect policies block with exponential
200 backoff before retrying.
201 '';
202 };
203 };
204
205 extraConfig = mkOption {
206 default = "";
207 type = types.lines;
208 description = lib.mdDoc ''
209 Extra configuration options that will be added verbatim at
210 the end of the ldap configuration file (`ldap.conf(5)`).
211 If {option}`users.ldap.daemon` is enabled, this
212 configuration will not be used. In that case, use
213 {option}`users.ldap.daemon.extraConfig` instead.
214 '' ;
215 };
216
217 };
218
219 };
220
221 ###### implementation
222
223 config = mkIf cfg.enable {
224
225 environment.etc = optionalAttrs (!cfg.daemon.enable) {
226 "ldap.conf" = ldapConfig;
227 };
228
229 system.activationScripts = mkIf (!cfg.daemon.enable) {
230 ldap = stringAfter [ "etc" "groups" "users" ] ''
231 if test -f "${cfg.bind.passwordFile}" ; then
232 umask 0077
233 conf="$(mktemp)"
234 printf 'bindpw %s\n' "$(cat ${cfg.bind.passwordFile})" |
235 cat ${ldapConfig.source} - >"$conf"
236 mv -fT "$conf" /etc/ldap.conf
237 fi
238 '';
239 };
240
241 system.nssModules = mkIf cfg.nsswitch (singleton (
242 if cfg.daemon.enable then nss_pam_ldapd else nss_ldap
243 ));
244
245 system.nssDatabases.group = optional cfg.nsswitch "ldap";
246 system.nssDatabases.passwd = optional cfg.nsswitch "ldap";
247 system.nssDatabases.shadow = optional cfg.nsswitch "ldap";
248
249 users = mkIf cfg.daemon.enable {
250 groups.nslcd = {
251 gid = config.ids.gids.nslcd;
252 };
253
254 users.nslcd = {
255 uid = config.ids.uids.nslcd;
256 description = "nslcd user.";
257 group = "nslcd";
258 };
259 };
260
261 systemd.services = mkIf cfg.daemon.enable {
262 nslcd = {
263 wantedBy = [ "multi-user.target" ];
264
265 preStart = ''
266 umask 0077
267 conf="$(mktemp)"
268 {
269 cat ${nslcdConfig}
270 test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.passwordFile}' ||
271 printf 'bindpw %s\n' "$(cat '${cfg.bind.passwordFile}')"
272 test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpwFile}' ||
273 printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpwFile}')"
274 } >"$conf"
275 mv -fT "$conf" /run/nslcd/nslcd.conf
276 '';
277
278 restartTriggers = [
279 nslcdConfig
280 cfg.bind.passwordFile
281 cfg.daemon.rootpwmodpwFile
282 ];
283
284 serviceConfig = {
285 ExecStart = "${nslcdWrapped}/bin/nslcd";
286 Type = "forking";
287 Restart = "always";
288 User = "nslcd";
289 Group = "nslcd";
290 RuntimeDirectory = [ "nslcd" ];
291 PIDFile = "/run/nslcd/nslcd.pid";
292 AmbientCapabilities = "CAP_SYS_RESOURCE";
293 };
294 };
295
296 };
297
298 };
299
300 imports =
301 [ (mkRenamedOptionModule [ "users" "ldap" "bind" "password"] [ "users" "ldap" "bind" "passwordFile"])
302 ];
303}