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 "authentication against an LDAP server";
63
64 loginPam = mkOption {
65 type = types.bool;
66 default = true;
67 description = "Whether to include authentication against LDAP in login PAM.";
68 };
69
70 nsswitch = mkOption {
71 type = types.bool;
72 default = true;
73 description = "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 = "The URL of the LDAP server.";
80 };
81
82 base = mkOption {
83 type = types.str;
84 example = "dc=example,dc=org";
85 description = "The distinguished name of the search base.";
86 };
87
88 useTLS = mkOption {
89 type = types.bool;
90 default = false;
91 description = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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.nssModules = mkIf cfg.nsswitch (singleton (
230 if cfg.daemon.enable then nss_pam_ldapd else nss_ldap
231 ));
232
233 system.nssDatabases.group = optional cfg.nsswitch "ldap";
234 system.nssDatabases.passwd = optional cfg.nsswitch "ldap";
235 system.nssDatabases.shadow = optional cfg.nsswitch "ldap";
236
237 users = mkIf cfg.daemon.enable {
238 groups.nslcd = {
239 gid = config.ids.gids.nslcd;
240 };
241
242 users.nslcd = {
243 uid = config.ids.uids.nslcd;
244 description = "nslcd user.";
245 group = "nslcd";
246 };
247 };
248
249 systemd.services = mkMerge [
250 (mkIf (!cfg.daemon.enable) {
251 ldap-password = {
252 wantedBy = [ "sysinit.target" ];
253 before = [ "sysinit.target" "shutdown.target" ];
254 conflicts = [ "shutdown.target" ];
255 unitConfig.DefaultDependencies = false;
256 serviceConfig.Type = "oneshot";
257 serviceConfig.RemainAfterExit = true;
258 script = ''
259 if test -f "${cfg.bind.passwordFile}" ; then
260 umask 0077
261 conf="$(mktemp)"
262 printf 'bindpw %s\n' "$(cat ${cfg.bind.passwordFile})" |
263 cat ${ldapConfig.source} - >"$conf"
264 mv -fT "$conf" /etc/ldap.conf
265 fi
266 '';
267 };
268 })
269
270 (mkIf cfg.daemon.enable {
271 nslcd = {
272 wantedBy = [ "multi-user.target" ];
273
274 preStart = ''
275 umask 0077
276 conf="$(mktemp)"
277 {
278 cat ${nslcdConfig}
279 test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.passwordFile}' ||
280 printf 'bindpw %s\n' "$(cat '${cfg.bind.passwordFile}')"
281 test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpwFile}' ||
282 printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpwFile}')"
283 } >"$conf"
284 mv -fT "$conf" /run/nslcd/nslcd.conf
285 '';
286
287 restartTriggers = [
288 nslcdConfig
289 cfg.bind.passwordFile
290 cfg.daemon.rootpwmodpwFile
291 ];
292
293 serviceConfig = {
294 ExecStart = "${nslcdWrapped}/bin/nslcd";
295 Type = "forking";
296 Restart = "always";
297 User = "nslcd";
298 Group = "nslcd";
299 RuntimeDirectory = [ "nslcd" ];
300 PIDFile = "/run/nslcd/nslcd.pid";
301 AmbientCapabilities = "CAP_SYS_RESOURCE";
302 };
303 };
304 })
305 ];
306
307 };
308
309 imports =
310 [ (mkRenamedOptionModule [ "users" "ldap" "bind" "password"] [ "users" "ldap" "bind" "passwordFile"])
311 ];
312}