nixos-config/lib/fudo/ldap.nix

492 lines
15 KiB
Nix

{ 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 = with types; {
fudo = {
auth = {
ldap-server = {
enable = mkEnableOption "Fudo Authentication";
kerberos-host = mkOption {
type = str;
description = ''
The name of the host to use for Kerberos authentication.
'';
};
kerberos-keytab = mkOption {
type = str;
description = ''
The path to a keytab for the LDAP server, containing a principal for ldap/<hostname>.
'';
};
ssl-certificate = mkOption {
type = str;
description = ''
The path to the SSL certificate to use for the server.
'';
};
ssl-private-key = mkOption {
type = str;
description = ''
The path to the SSL key to use for the server.
'';
};
ssl-ca-certificate = mkOption {
type = with types; nullOr str;
description = ''
The path to the SSL CA cert used to sign the certificate.
'';
default = null;
};
organization = mkOption {
type = str;
description = ''
The name to use for the organization.
'';
};
base = mkOption {
type = str;
description = "The base dn of the LDAP server.";
example = "dc=fudo,dc=org";
};
rootpw-file = mkOption {
default = "";
type = str;
description = ''
The path to a file containing the root password for this database.
'';
};
listen-uris = mkOption {
type = 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 {
type = attrsOf (submodule ldapUserOpts);
example = {
tester = {
uid = 10099;
common-name = "Joe Blow";
hashed-password = "<insert password hash>";
};
};
description = ''
Users to be added to the Fudo LDAP database.
'';
default = { };
};
groups = mkOption {
default = { };
type = attrsOf (submodule ldapGroupOpts);
example = {
admin = {
gid = 1099;
members = [ "tester" ];
};
};
description = ''
Groups to be added to the Fudo LDAP database.
'';
};
system-users = mkOption {
default = { };
type = attrsOf (submodule ldapSystemUserOpts);
example = {
replicator = {
description = "System user for database sync";
hashed-password = "<insert password hash>";
};
};
description = ''
System users to be added to the Fudo LDAP database.
'';
};
};
};
};
};
config = mkIf cfg.enable {
environment = {
etc = {
"openldap/sasl2/slapd.conf" = {
mode = "0400";
user = config.services.openldap.user;
group = config.services.openldap.group;
text = ''
mech_list: gssapi external
keytab: ${cfg.kerberos-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;
database = "mdb";
settings = let
makeAccessLine = i: attrs: perm-map: let
perm-strings = mapAttrs (dn: perm: "by ${dn} ${perm}") perm-map;
perm-string = concatStringsSep " " perm-strings;
in "${i}to ${attrs} ${perm-string}";
makeAccess = access-map: let
pairs = mapAttrsToList (target: perm-map: [target perm-map]) access-map;
in imap0 (i: pair: makeAccessLine i pair[0] pair[1]) pairs;
in {
attrs = {
cn = "config";
objectClass = "olcGlobal";
olcPidFile = "/run/slapd/slapd.pid";
olcTLSCertificateFile = cfg.ssl-certificate;
olcTLSCertificateKeyFile = cfg.ssl-private-key;
olcTLSCACertificateFile = cfg.ssl-ca-certificate;
olcSaslSecProps = "noplain,noanonymous";
olcAuthzRegexp = let
authz-regex-entry = i: { regex, target}:
"{${i}}\"${rx}\" \"${target}\"";
in imap0 authz-regex-entry [
{
regex = "^uid=auth/([^.]+).fudo.org,cn=fudo.org,cn=gssapi,cn=auth$";
target = "cn=$1,ou=hosts,dc=fudo,dc=org";
}
{
regex = "^uid=[^,/]+/root,cn=fudo.org,cn=gssapi,cn=auth$";
target = "cn=admin,dc=fudo,dc=org";
}
{
regex = "^uid=([^,/]+),cn=fudo.org,cn=gssapi,cn=auth$";
target = "uid=$1,ou=members,dc=fudo,dc=org";
}
{
regex = "^uid=host/([^,/]+),cn=fudo.org,cn=gssapi,cn=auth$";
target = "cn=$1,ou=hosts,dc=fudo,dc=org";
}
{
regex = "^gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth$";
target = "cn=admin,dc=fudo,dc=org";
}
];
};
children = {
"olcDatabase{-1}frontend" = {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcFrontendConfig" ];
olcDatabase = "{-1}frontend";
olcAccess = makeAccess {
"*" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=admin,dc=fudo,dc=org" = "manage";
"*" = "none";
};
};
};
};
"olcDatabase{0}config" = {
attrs = {
objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{0}config";
olcAccess = [ "by * none" ];
};
};
"olcDatabase{1}mdb" = {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcSuffix = cfg.base;
# olcRootDN = "cn=admin,${cfg.base}";
# olcRootPW = FIXME; # NOTE: this should be hashed...
olcDbDirectory = cfg.database-directory;
olcDbIndex = [ "objectClass eq" "uid eq" ];
olcAccess = makeAccess {
"attrs=userPassword,shadowLastChange" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"dn.exact=cn=auth_reader,${cfg.base}" = "read";
"dn.exact=cn=replicator,${cfg.base}" = "read";
"self" = "write";
"*" = "auth";
};
"dn.base=cn=admin,ou=groups,${cfg.base}" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"users" = "read";
"*" = "none";
};
"dn.subtree=ou=groups,${cfg.base} attrs=memberUid" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"dn.regex=cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" = "write";
"group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"users" = "read";
"*" = "none";
};
"dn.subtree=ou=members,${cfg.base} attrs=cn,sn,homeDirectory,loginShell,gecos,description,homeDirectory,uidNumber,gidNumber" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"dn.exact=cn=user_db_reader,${cfg.base}" = "read";
"users" = "read";
"*" = "none";
};
"*" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=admin,ou=groups,${cfg.base}" = "write";
"group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "read";
"users" = "read";
"*" = "none";
};
};
};
};
};
};
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}
'';
};
};
}