{ config, lib, pkgs, ... }: with lib; let cfg = config.fudo.auth.ldap-server; ldapSystemUserOpts = { name, ... }: { options = { description = mkOption { type = types.str; description = '' The description of this system user. ''; }; hashed-password = mkOption { type = types.str; description = '' The password for this user, hashed with ldappasswd. ''; default = ""; }; }; }; ldapGroupOpts = { name, ... }: { options = { gid = mkOption { type = types.int; description = '' The GID number of this group. ''; }; description = mkOption { type = types.str; description = '' The description of this group. ''; }; members = mkOption { type = with types; listOf str; default = [ ]; description = '' A list of usernames representing the members of this group. ''; }; }; }; ldapUserOpts = { name, ... }: { options = { uid = mkOption { type = types.int; description = '' The UID number of this user. ''; }; common-name = mkOption { type = types.str; description = '' The given name of this user. ''; }; group = mkOption { type = types.str; description = '' The name of the user's primary group. ''; }; login-shell = mkOption { type = types.str; default = "/bin/bash"; description = '' The user's preferred shell. Default is /bin/bash. ''; }; description = mkOption { type = types.str; default = "Fudo Member"; description = '' The description of this user. ''; }; hashed-password = mkOption { type = types.str; description = '' The password for this user, hashed with ldappasswd. ''; default = ""; }; }; }; stringJoin = joiner: attrList: if (length attrList) == 0 then "" else foldr (lAttr: rAttr: "${lAttr}${joiner}${rAttr}") (last attrList) (init attrList); getUserGidNumber = user: group-map: group-map.${user.group}.gid; attrOr = attrs: attr: value: if attrs ? ${attr} then attrs.${attr} else value; mkHomeDir = username: user-opts: if (user-opts.group == "admin") then "/home/${username}" else "/home/${user-opts.group}/${username}"; userLdif = base: name: group-map: opts: '' dn: uid=${name},ou=members,${base} uid: ${name} objectClass: account objectClass: shadowAccount objectClass: posixAccount cn: ${opts.common-name} uidNumber: ${toString (opts.uid)} gidNumber: ${toString (getUserGidNumber opts group-map)} homeDirectory: ${mkHomeDir name opts} description: ${opts.description} shadowLastChange: 12230 shadowMax: 99999 shadowWarning: 7 userPassword: ${opts.hashed-password} ''; systemUserLdif = base: name: opts: '' dn: cn=${name},${base} objectClass: organizationalRole objectClass: simpleSecurityObject cn: ${name} description: ${opts.description} userPassword: ${opts.hashed-password} ''; toMemberList = userList: stringJoin "\n" (map (username: "memberUid: ${username}") userList); groupLdif = base: name: opts: '' dn: cn=${name},ou=groups,${base} objectClass: posixGroup cn: ${name} gidNumber: ${toString (opts.gid)} description: ${opts.description} ${toMemberList opts.members} ''; systemUsersLdif = base: user-map: stringJoin "\n" (mapAttrsToList (name: opts: systemUserLdif base name opts) user-map); groupsLdif = base: group-map: stringJoin "\n" (mapAttrsToList (name: opts: groupLdif base name opts) group-map); usersLdif = base: group-map: user-map: stringJoin "\n" (mapAttrsToList (name: opts: userLdif base name group-map opts) user-map); in { options = { fudo = { auth = { ldap-server = { enable = mkEnableOption "Fudo Authentication"; kerberos-host = mkOption { type = types.str; description = '' The name of the host to use for Kerberos authentication. ''; }; kerberos-keytab = mkOption { type = types.str; description = '' The path to a keytab for the LDAP server, containing a principal for ldap/. ''; }; sslCert = mkOption { type = types.str; description = '' The path to the SSL certificate to use for the server. ''; }; sslKey = mkOption { type = types.str; description = '' The path to the SSL key to use for the server. ''; }; sslCACert = mkOption { type = with types; nullOr str; description = '' The path to the SSL CA cert used to sign the certificate. ''; default = null; }; organization = mkOption { type = types.str; description = '' The name to use for the organization. ''; }; base = mkOption { type = types.str; description = '' The base dn of the LDAP server (eg. "dc=fudo,dc=org"). ''; }; rootpw-file = mkOption { default = ""; type = types.str; description = '' The path to a file containing the root password for this database. ''; }; listen-uris = mkOption { default = [ ]; type = with types; listOf str; description = '' A list of URIs on which the ldap server should listen. ''; example = [ "ldap://auth.fudo.org" "ldaps://auth.fudo.org" ]; }; users = mkOption { default = { }; type = with types; attrsOf (submodule ldapUserOpts); example = { tester = { uid = 10099; common-name = "Joe Blow"; hashed-password = ""; }; }; description = '' Users to be added to the Fudo LDAP database. ''; }; groups = mkOption { default = { }; type = with types; attrsOf (submodule ldapGroupOpts); example = { admin = { gid = 1099; members = [ "tester" ]; }; }; description = '' Groups to be added to the Fudo LDAP database. ''; }; system-users = mkOption { default = { }; type = with types; attrsOf (submodule ldapSystemUserOpts); example = { replicator = { description = "System user for database sync"; hashed-password = ""; }; }; description = '' System users to be added to the Fudo LDAP database. ''; }; }; }; }; }; config = mkIf cfg.enable { environment = { etc = { "openldap/sasl2/slapd.conf" = { mode = "0400"; user = "openldap"; group = "openldap"; # FIXME: take arguments! text = '' mech_list: gssapi external keytab: /etc/ldap/ldap.keytab ''; }; }; }; systemd.services.openldap = { environment = { KRB5_KTNAME = cfg.kerberos-keytab; }; # FIXME: THIS IS ALL UNPROVEN serviceConfig = { PrivateDevices = true; PrivateTmp = true; PrivateMounts = true; ProtectControlGroups = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectSystem = true; ProtectHostname = true; ProtectHome = true; ProtectClock = true; ProtectKernelLogs = true; KeyringMode = "private"; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; Restart = "on-failure"; LockPersonality = true; RestrictRealtime = true; MemoryDenyWriteExecute = true; SystemCallFilter = "~@clock @debug @module @mount @raw-io @reboot @swap @privileged @resources @cpu-emulation @obsolete"; UMask = "7007"; InaccessiblePaths = [ "/home" "/root" ]; LimitNOFILE = 49152; PermissionsStartOnly = true; }; }; services.openldap = { enable = true; suffix = cfg.base; rootdn = "cn=admin,${cfg.base}"; rootpwFile = "${cfg.rootpw-file}"; urlList = cfg.listen-uris; extraConfig = '' TLSCertificateFile ${cfg.sslCert} TLSCertificateKeyFile ${cfg.sslKey} ${optionalString (cfg.sslCACert != null) "TLSCACertificateFile ${cfg.sslCACert}"} authz-regexp "^uid=auth/([^.]+)\.fudo\.org,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org" authz-regexp "^uid=[^,/]+/root,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=admin,dc=fudo,dc=org" authz-regexp "^uid=([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "uid=$1,ou=members,dc=fudo,dc=org" authz-regexp "^uid=host/([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org" authz-regexp "^gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth$" "cn=admin,dc=fudo,dc=org" ''; extraDatabaseConfig = '' # access to dn=base="" # by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage # by * read access to attrs=userPassword,shadowLastChange by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by group.exact="cn=admin,ou=members,${cfg.base}" write by dn.exact="cn=auth_reader,${cfg.base}" read by dn.exact="cn=replicator,${cfg.base}" read by self write by * auth access to dn.exact="cn=admin,ou=groups,${cfg.base}" by dn.exact="cn=admin,${cfg.base}" write by users read by * none access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write by group.exact="cn=admin,ou=groups,${cfg.base}" write by users read by * none access to dn.subtree="ou=members,${cfg.base}" attrs=cn,sn,homeDirectory,loginShell,gecos,description,homeDirectory,uidNumber,gidNumber by group.exact="cn=admin,ou=groups,${cfg.base}" write by dn.exact="cn=user_db_reader,${cfg.base}" read by users read by * none access to dn.exact="cn=admin,ou=groups,${cfg.base}" by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by users read by * none access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write by group.exact="cn=admin,ou=groups,${cfg.base}" write by users read by * none access to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by users read by * none index objectClass,uid eq ''; declarativeContents = '' dn: ${cfg.base} objectClass: top objectClass: dcObject objectClass: organization o: ${cfg.organization} dn: ou=groups,${cfg.base} objectClass: organizationalUnit description: ${cfg.organization} groups dn: ou=members,${cfg.base} objectClass: organizationalUnit description: ${cfg.organization} members dn: cn=admin,${cfg.base} objectClass: organizationalRole cn: admin description: "Admin User" ${systemUsersLdif cfg.base cfg.system-users} ${groupsLdif cfg.base cfg.groups} ${usersLdif cfg.base cfg.groups cfg.users} ''; }; }; }