diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/apps/openldap/default.nix | 197 | ||||
-rw-r--r-- | modules/apps/openldap/instance.nix | 133 |
2 files changed, 330 insertions, 0 deletions
diff --git a/modules/apps/openldap/default.nix b/modules/apps/openldap/default.nix new file mode 100644 index 0000000..060e85a --- /dev/null +++ b/modules/apps/openldap/default.nix @@ -0,0 +1,197 @@ +{ config, lib, pkgs, ... }: + +let + + inherit (builtins) + isList isString replaceStrings ; + + inherit (lib) + concatMapStringsSep concatStringsSep filter filterAttrs flatten foldAttrs + foldl hasPrefix imap mapAttrsToList mkOption ; + + inherit (lib.types) + attrsOf path str submodule ; + + explicit = filterAttrs (n: v: n != "_module" && v != null); + instances = explicit config.nixsap.apps.openldap; + users = mapAttrsToList (_: i: i.user) instances; + + keyrings = + let + keyFiles = i: filter (hasPrefix config.nixsap.deployment.keyStore) i.apply; + ik = mapAttrsToList (_: i: { + "${i.user}" = [i."cn=config".olcTLSCertificateKeyFile] ++ keyFiles i; + } ) instances; + in foldAttrs (l: r: l ++ r) [] ik; + + mkService = name: cfg: + let + + uid = config.users.users.${cfg.user}.uid; + gid = config.users.groups.${cfg.user}.gid; + + show = n: v: + if isList v then map (s: "${n}: ${toString s}") v + else "${n}: ${toString v}"; + + olc = let olcXX = filterAttrs (n: _: hasPrefix "olc" n) (explicit cfg."cn=config"); + in flatten (mapAttrsToList show olcXX); + + configInit = pkgs.writeText "openldap-${name}-cn=config.ldif" '' + dn: cn=config + objectClass: olcGlobal + cn: config + ${concatStringsSep "\n" olc} + + dn: cn=schema,cn=config + objectClass: olcSchemaConfig + cn: schema + + # XXX: access rule of this DB are appended to each other DB, except {0}config (???). + dn: olcDatabase={-1}frontend,cn=config + objectClass: olcDatabaseConfig + objectClass: olcFrontendConfig + olcDatabase: {-1}frontend + olcAccess: {0}to * by dn.exact=gidNumber=${toString gid}+uidNumber=${toString uid},cn=peercred,cn=external,cn=auth manage by * break + + dn: olcDatabase={0}config,cn=config + objectClass: olcDatabaseConfig + olcDatabase: {0}config + olcReadOnly: TRUE + olcAccess: {0}to * by dn.exact=gidNumber=${toString gid}+uidNumber=${toString uid},cn=peercred,cn=external,cn=auth read by * break + olcAccess: {1}to * by * none + + ${cfg."cn=config".ldif} + ''; + + slapdDir = "${cfg.home}/slapd.d"; + + socket = replaceStrings + [ " " "/" ] + [ "+" "%2F" ] + "${cfg.home}/${name}.sock"; + + # XXX: OpenLDAP starts as root for privileged ports. Capabilities set by systemd aren't reliable + # XXX: See for example https://github.com/systemd/systemd/issues/5000 + start = pkgs.writeBashScriptBin "openldap-${name}" '' + set -euo pipefail + + rm -rf -- '${slapdDir}' + mkdir -p '${slapdDir}' + + # XXX: All `olcDbDirectory` must exist before slapd or slapadd run + # XXX: until http://www.openldap.org/lists/openldap-devel/200904/msg00015.html + while IFS= read -r d + do + if [[ "$d" == '${cfg.home}/'* ]]; then + if [ ! -d "$d" ]; then + mkdir -p -- "$d" + chmod -R u=rwX,g=rX,o= -- "$d" + chown -R '${cfg.user}:${cfg.user}' -- "$d" + fi + else + echo "Path '$d' is not under '${cfg.home}'" >&2 + exit 1 + fi + done < <(${pkgs.gnused}/bin/sed -rn '/^olcDbDirectory:/ s!^olcDbDirectory: *(.+) *$!\1!p' '${configInit}') + + echo '>>>>> importing ${configInit} ...' + ${cfg.package}/bin/slapadd -n 0 -v -F '${slapdDir}' \ + ${concatMapStringsSep " " (o: "-d ${o}") cfg.debugLevel} \ + -l ${configInit} + chmod -R u=rX,g=rX,o= '${slapdDir}' + chown -R '${cfg.user}:${cfg.user}' '${slapdDir}' + echo '<<<<< imported ${configInit}' + + exec ${cfg.package}/libexec/slapd \ + -n 'openldap-${name}' \ + -u '${cfg.user}' \ + -g '${cfg.user}' \ + -h 'ldapi://${socket} ${cfg.urlList}' \ + ${concatMapStringsSep " " (o: "-d ${o}") cfg.debugLevel} \ + -F '${slapdDir}' + ''; + + + ldifs = imap (i: f: + if hasPrefix "/" f then "'${f}'" + else pkgs.writeText "openldap-${name}.${toString i}.ldif" f + ) cfg.apply; + + + apply = pkgs.writeBashScriptBin "openldap-${name}-apply" '' + set -euo pipefail + + while ! ${cfg.package}/bin/ldapsearch \ + -LLL -Y EXTERNAL \ + -H 'ldapi://${socket}' \ + -b 'olcDatabase={0}config,cn=config' dn \ + | ${pkgs.gnugrep}/bin/grep -qF 'olcDatabase={0}config,cn=config' + do + sleep 1s + done + + exec ${pkgs.ldapply}/bin/ldapply \ + -H 'ldapi://${socket}' \ + ${toString ldifs} + ''; + + in { + "openldap-${name}" = { + description = "OpenLDAP server (${name})"; + wantedBy = [ "multi-user.target" ]; + after = [ "keys.target" "network.target" "local-fs.target" ]; + preStart = '' + mkdir -p -- '${cfg.home}' + find '${cfg.home}' -not -type l \( \ + -not -user '${cfg.user}' \ + -or -not -group '${cfg.user}' \ + -or \( -type d -not -perm -u=wrx,g=rx \) \ + -or \( -type f -not -perm -u=rw,g=r \) \ + \) \ + -exec chown -c -- '${cfg.user}:${cfg.user}' {} + \ + -exec chmod -c -- u=rwX,g=rX,o= {} + + ''; + + unitConfig = { + # XXX OpenLDAP can be running long before fail: + StartLimitBurst = 3; + StartLimitIntervalSec = 300; + }; + + serviceConfig = { + ExecStart = "${start}/bin/openldap-${name}"; + Restart = "always"; + TimeoutSec = 60; + }; + }; + + "openldap-${name}-apply" = { + description = "OpenLDAP server (${name}) data update"; + wantedBy = [ "multi-user.target" ]; + after = [ "openldap-${name}.service" ]; + serviceConfig = { + ExecStart = "${apply}/bin/openldap-${name}-apply"; + Type = "oneshot"; + RemainAfterExit = true; + TimeoutSec = 60; + User = cfg.user; + }; + }; + }; + +in { + + options.nixsap.apps.openldap = mkOption { + description = "OpenLDAP instances"; + default = {}; + type = attrsOf (submodule (import ./instance.nix pkgs)); + }; + + config = { + nixsap.deployment.keyrings = keyrings; + nixsap.system.users.daemons = users; + systemd.services = foldl (a: b: a//b) {} (mapAttrsToList mkService instances); + }; + +} diff --git a/modules/apps/openldap/instance.nix b/modules/apps/openldap/instance.nix new file mode 100644 index 0000000..5ab5e0a --- /dev/null +++ b/modules/apps/openldap/instance.nix @@ -0,0 +1,133 @@ +pkgs: +{ config, lib, name, ... }: + +let + + inherit (lib) + mkOption mkOrder ; + + inherit (lib.types) + either enum int lines listOf nullOr package path str ; + + optional = t: mkOption { type = nullOr t; default = null; }; + default = d: t: mkOption { type = t; default = d; }; + +in { + options = { + + user = mkOption { + description = "User to run as"; + type = str; + default = "openldap-${name}"; + }; + + package = mkOption { + description = "OpenLDAP package"; + type = package; + default = pkgs.openldap; + }; + + home = mkOption { + description = '' + OpenLDAP home directory, where all the databases are stored, + including `cn=config`. + ''; + type = path; + default = "/openldap/${name}"; + }; + + debugLevel = mkOption { + description = "What to log"; + type = listOf (enum [ + "acl" "any" "args" "ber" "config" "conns" "filter" "none" + "packets" "parse" "pcache" "shell" "stats" "stats2" "sync" + "trace" + ]); + default = [ "acl" "ber" "config" "conns" ]; + }; + + urlList = mkOption { + description = '' + Passed as is for the -h option to slapd. Note that one more url + ldapi:// will be passed anyway for internal maintenance. + ''; + type = str; + default = "ldap://127.0.0.1"; + example = "ldapi://%2Ftmp%2Fldapi ldaps:///"; + }; + + "cn=config" = { + olcConnMaxPending = optional int; + olcConnMaxPendingAuth = optional int; + olcIdleTimeout = optional int; + olcReferral = default [] (listOf str); + olcTLSCACertificateFile = optional path; + olcTLSCACertificatePath = optional path; + olcTLSCRLCheck = optional (enum ["none" "peer" "all"]); + olcTLSCRLFile = optional path; + olcTLSCertificateFile = optional path; + olcTLSCertificateKeyFile = optional path; + olcTLSCipherSuite = optional str; + olcTLSDHParamFile = optional path; + olcTLSRandFile = optional path; + olcTLSVerifyClient = optional (enum ["never" "allow" "try"]); + olcThreads = optional int; + olcWriteTimeout = optional int; + + ldif = mkOption { + description = '' + OpenLDAP configuration in LDIF format. This is fed to the slapadd + utility before slapd is started and completely replaces any existing + slapd configuration (`cn=config`). You may include schema files + here, add databases, load modules. Any `olcDbDirectory` mentioned + here will be automatically created iff it is under home directory. + To configure `cn=config` itself use dedicated options. + ''; + type = lines; + example = '' + dn: olcDatabase={1}mdb,cn=config + objectClass: olcDatabaseConfig + objectClass: olcMdbConfig + olcAccess: {0}to attrs=userPassword + by anonymous auth + by * break + olcAccess: {1}to dn.subtree="dc=example,dc=com" + by dn="cn=admin,dc=example,dc=com" write + by * break + olcDatabase: {1}mdb + olcDbCheckpoint: 512 30 + olcDbDirectory: $\{apps.openldap.foo.home\}/example.com + olcDbIndex: cn eq + olcDbMaxSize: 1073741824 + olcSuffix: dc=example,dc=com + ''; + }; + }; + + apply = mkOption { + description = '' + LDIF files to apply. This data is idempotently applied by the + `ldapply` tool. Useful for initial configuration. These files are + processed in order, after slapd is started and ready. Important + note: if you want to apply to a specific tree/object, make sure to + append 'by * break' to any access rule targeting this tree/object. + Otherwise internal maintenance script will not be able to operate. + For example, 'olcAccess: to dn.subtree="dc=example,dc=com" by + dn="cn=admin,dc=example,dc=com" write by * break'. This is because + the default rule is 'by * none stop'. + ''; + type = listOf (either str path); + default = []; + example = [ "/foo/addusers.ldif" "/run/keys/set_passwords.ldif" ]; + }; + }; + + config = { + "cn=config".ldif = mkOrder 0 '' + include: file://${config.package}/etc/openldap/schema/core.ldif + include: file://${config.package}/etc/openldap/schema/cosine.ldif + include: file://${config.package}/etc/openldap/schema/inetorgperson.ldif + ''; + }; +} + |