1 { config, lib, pkgs, ... }:
3 cfg = config.services.openldap;
4 openldap = cfg.package;
5 configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
8 # Can't do types.either with multiple non-overlapping submodules, so define our own
9 singleLdapValueType = lib.mkOptionType rec {
11 # TODO: It would be nice to define a { secret = ...; } option, using
12 # systemd's LoadCredentials for secrets. That would remove the last
13 # barrier to using DynamicUser for openldap. This is blocked on
14 # systemd/systemd#19604
16 LDAP value - either a string, or an attrset containing
17 `path` or `base64` for included
18 values or base-64 encoded values respectively.
20 check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
21 merge = lib.mergeEqualOption;
23 # We don't coerce to lists of single values, as some values must be unique
24 in lib.types.either singleLdapValueType (lib.types.listOf singleLdapValueType);
29 attrs = lib.mkOption {
30 type = lib.types.attrsOf ldapValueType;
32 description = "Attributes of the parent entry.";
34 children = lib.mkOption {
35 # Hide the child attributes, to avoid infinite recursion in e.g. documentation
36 # Actual Nix evaluation is lazy, so this is not an issue there
38 hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
39 in lib.types.attrsOf (lib.types.submodule { options = hiddenOptions; });
41 description = "Child entries of the current entry, with recursively the same structure.";
42 example = lib.literalExpression ''
45 # The attribute used in the DN must be defined
46 attrs = { cn = "schema"; };
48 # This entry's DN is expanded to "cn=foo,cn=schema"
51 # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
57 includes = lib.mkOption {
58 type = lib.types.listOf lib.types.path;
61 LDIF files to include after the parent's attributes but before its children.
65 in lib.types.submodule { inherit options; };
67 valueToLdif = attr: values: let
68 listValues = if lib.isList values then values else lib.singleton values;
70 if lib.isAttrs value then
71 if lib.hasAttr "path" value
72 then "${attr}:< file://${value.path}"
73 else "${attr}:: ${value.base64}"
74 else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
77 attrsToLdif = dn: { attrs, children, includes, ... }: [''
79 ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
80 ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
81 lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
86 enable = lib.mkOption {
87 type = lib.types.bool;
89 description = "Whether to enable the ldap server.";
92 package = lib.mkPackageOption pkgs "openldap" {
94 This can be used to, for example, set an OpenLDAP package
95 with custom overrides to enable modules or other
100 user = lib.mkOption {
101 type = lib.types.str;
102 default = "openldap";
103 description = "User account under which slapd runs.";
106 group = lib.mkOption {
107 type = lib.types.str;
108 default = "openldap";
109 description = "Group account under which slapd runs.";
112 urlList = lib.mkOption {
113 type = lib.types.listOf lib.types.str;
114 default = [ "ldap:///" ];
115 description = "URL list slapd should listen on.";
116 example = [ "ldaps:///" ];
119 settings = lib.mkOption {
120 type = ldapAttrsType;
121 description = "Configuration for OpenLDAP, in OLC format";
122 example = lib.literalExpression ''
124 attrs.olcLogLevel = [ "stats" ];
126 "cn=schema".includes = [
127 "''${pkgs.openldap}/etc/schema/core.ldif"
128 "''${pkgs.openldap}/etc/schema/cosine.ldif"
129 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
131 "olcDatabase={-1}frontend" = {
133 objectClass = "olcDatabaseConfig";
134 olcDatabase = "{-1}frontend";
135 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
138 "olcDatabase={0}config" = {
140 objectClass = "olcDatabaseConfig";
141 olcDatabase = "{0}config";
142 olcAccess = [ "{0}to * by * none break" ];
145 "olcDatabase={1}mdb" = {
147 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
148 olcDatabase = "{1}mdb";
149 olcDbDirectory = "/var/lib/openldap/ldap";
156 olcSuffix = "dc=example,dc=com";
157 olcAccess = [ "{0}to * by * read break" ];
165 # This option overrides settings
166 configDir = lib.mkOption {
167 type = lib.types.nullOr lib.types.path;
170 Use this config directory instead of generating one from the
171 `settings` option. Overrides all NixOS settings.
173 example = "/var/lib/openldap/slapd.d";
176 mutableConfig = lib.mkOption {
177 type = lib.types.bool;
180 Whether to allow writable on-line configuration. If
181 `true`, the NixOS settings will only be used to
182 initialize the OpenLDAP configuration if it does not exist, and are
183 subsequently ignored.
187 declarativeContents = lib.mkOption {
188 type = with lib.types; attrsOf lines;
191 Declarative contents for the LDAP database, in LDIF format by suffix.
193 All data will be erased when starting the LDAP server. Modifications
194 to the database are not prevented, they are just dropped on the next
195 reboot of the server. Performance-wise the database and indexes are
196 rebuilt on each server startup, so this will slow down server startup,
197 especially with large databases.
199 Note that the root of the DB must be defined in
200 `services.openldap.settings` and the
201 `olcDbDirectory` must begin with
202 `"/var/lib/openldap"`.
204 example = lib.literalExpression ''
206 "dc=example,dc=org" = '''
207 dn= dn: dc=example,dc=org
211 dn: ou=users,dc=example,dc=org
212 objectClass = organizationalUnit
223 meta.maintainers = with lib.maintainers; [ kwohlfahrt ];
226 dbSettings = lib.mapAttrs' (name: { attrs, ... }: lib.nameValuePair attrs.olcSuffix attrs)
227 (lib.filterAttrs (name: { attrs, ... }: (lib.hasPrefix "olcDatabase=" name) && attrs ? olcSuffix) cfg.settings.children);
228 settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
229 writeConfig = pkgs.writeShellScript "openldap-config" ''
232 ${lib.optionalString (!cfg.mutableConfig) ''
233 chmod -R u+w ${configDir}
234 rm -rf ${configDir}/*
236 if [ ! -e "${configDir}/cn=config.ldif" ]; then
237 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
239 chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir}
242 contentsFiles = lib.mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents;
243 writeContents = pkgs.writeShellScript "openldap-load" ''
247 ${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3
249 in lib.mkIf cfg.enable {
251 assertion = (cfg.declarativeContents != {}) -> cfg.configDir == null;
253 Declarative DB contents (${lib.attrNames cfg.declarativeContents}) are not
254 supported with user-managed configuration.
257 assertion = (lib.getAttr dn dbSettings) ? "olcDbDirectory";
258 # olcDbDirectory is necessary to prepopulate database using `slapadd`.
260 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
261 `olcDbDirectory` configured.
263 }) (lib.attrNames cfg.declarativeContents)) ++ (lib.mapAttrsToList (dn: { olcDbDirectory ? null, ... }: {
264 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
265 # directories with `declarativeContents`.
266 assertion = (olcDbDirectory != null) ->
267 ((lib.hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/"));
269 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
270 `/var/lib/openldap/`.
273 environment.systemPackages = [ openldap ];
275 # Literal attributes must always be set
276 services.openldap.settings = {
278 objectClass = "olcGlobal";
281 children."cn=schema".attrs = {
283 objectClass = "olcSchemaConfig";
287 systemd.services.openldap = {
288 description = "OpenLDAP Server Daemon";
294 wantedBy = [ "multi-user.target" ];
295 wants = [ "network-online.target" ];
296 after = [ "network-online.target" ];
301 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
302 "+${pkgs.coreutils}/bin/chown $USER ${configDir}"
303 ] ++ (lib.optional (cfg.configDir == null) writeConfig)
304 ++ (lib.mapAttrsToList (dn: content: lib.escapeShellArgs [
305 writeContents dn (lib.getAttr dn dbSettings).olcDbDirectory content
307 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
308 ExecStart = lib.escapeShellArgs ([
309 "${openldap}/libexec/slapd" "-d" "0" "-F" configDir "-h" (lib.concatStringsSep " " cfg.urlList)
312 # Fixes an error where openldap attempts to notify from a thread
313 # outside the main process:
314 # Got notification message from PID 6378, but reception only permitted for main PID 6377
315 NotifyAccess = "all";
316 RuntimeDirectory = "openldap";
317 StateDirectory = ["openldap"]
318 ++ (map ({olcDbDirectory, ... }: lib.removePrefix "/var/lib/" olcDbDirectory) (lib.attrValues dbSettings));
319 StateDirectoryMode = "700";
320 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
321 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
325 users.users = lib.optionalAttrs (cfg.user == "openldap") {
332 users.groups = lib.optionalAttrs (cfg.group == "openldap") {