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