nixos-config/lib/fudo/ldap.nix

461 lines
14 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.auth.ldap-server;
user-type = import ../types/user.nix { inherit lib; };
stringJoin = concatStringsSep;
getUserGidNumber = user: group-map: group-map.${user.primary-group}.gid;
attrOr = attrs: attr: value: if attrs ? ${attr} then attrs.${attr} else value;
ca-path = "${cfg.state-directory}/ca.pem";
build-ca-script = target: ca-cert: site-chain: let
user = config.services.openldap.user;
group = config.services.openldap.group;
in pkgs.writeShellScript "build-openldap-ca-script.sh" ''
cat ${site-chain} ${ca-cert} > ${target}
chmod 440 ${target}
chown ${user}:${group} ${target}
'';
mkHomeDir = username: user-opts:
if (user-opts.primary-group == "admin") then
"/home/${username}"
else
"/home/${user-opts.primary-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.ldap-hashed-passwd}
'';
systemUserLdif = base: name: opts: ''
dn: cn=${name},${base}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: ${name}
description: ${opts.description}
userPassword: ${opts.ldap-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-chain = mkOption {
type = str;
description = ''
The path to the SSL chain to to the certificate 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 = 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 user-type.userOpts);
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 user-type.groupOpts);
example = {
admin = {
gid = 1099;
members = [ "tester" ];
};
};
description = ''
Groups to be added to the Fudo LDAP database.
'';
};
system-users = mkOption {
default = { };
type = attrsOf (submodule user-type.systemUserOpts);
example = {
replicator = {
description = "System user for database sync";
ldap-hashed-password = "<insert password hash>";
};
};
description = "System users to be added to the Fudo LDAP database.";
};
state-directory = mkOption {
type = str;
description = "Path at which to store openldap database & state.";
};
systemd-target = mkOption {
type = str;
description = "Systemd target for running ldap server.";
default = "fudo-ldap-server.target";
};
required-services = mkOption {
type = listOf str;
description = "Systemd services on which the server depends.";
default = [ ];
};
};
};
};
};
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}
'';
};
};
};
networking.firewall = {
allowedTCPPorts = [ 389 636 ];
allowedUDPPorts = [ 389 ];
};
systemd = {
tmpfiles.rules = let
ca-dir = dirOf ca-path;
user = config.services.openldap.user;
group = config.services.openldap.group;
in [
"d ${ca-dir} 0700 ${user} ${group} - -"
];
services.openldap = {
partOf = [ cfg.systemd-target ];
requires = cfg.required-services;
environment.KRB5_KTNAME = cfg.kerberos-keytab;
preStart = mkBefore
"${build-ca-script ca-path
cfg.ssl-chain
cfg.ssl-ca-certificate}";
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 = concatStringsSep " " [
"~@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;
urlList = cfg.listen-uris;
settings = let
makePermEntry = dn: perm: "by ${dn} ${perm}";
makeAccessLine = target: perm-map: let
perm-entries = mapAttrsToList makePermEntry perm-map;
in "to ${target} ${concatStringsSep " " perm-entries}";
makeAccess = access-map: let
access-lines = mapAttrsToList makeAccessLine;
numbered-access-lines = imap0 (i: line: "{${toString i}}${line}");
in numbered-access-lines (access-lines access-map);
in {
attrs = {
cn = "config";
objectClass = "olcGlobal";
olcPidFile = "/run/slapd/slapd.pid";
olcTLSCertificateFile = cfg.ssl-certificate;
olcTLSCertificateKeyFile = cfg.ssl-private-key;
olcTLSCACertificateFile = ca-path;
olcSaslSecProps = "noplain,noanonymous";
olcAuthzRegexp = let
authz-regex-entry = i: { regex, target }:
"{${toString i}}\"${regex}\" \"${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 = {
"cn=schema" = {
includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
];
};
"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";
"*" = "none";
};
};
};
};
"olcDatabase={0}config" = {
attrs = {
objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{0}config";
olcAccess = makeAccess {
"*" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"*" = "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.state-directory}/database";
olcDbIndex = [ "objectClass eq" "uid eq" ];
olcAccess = makeAccess {
"attrs=userPassword,shadowLastChange" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"dn.exact=cn=auth_reader,${cfg.base}" = "read";
"dn.exact=cn=replicator,${cfg.base}" = "read";
"self" = "write";
"*" = "auth";
};
"dn=cn=admin,ou=groups,${cfg.base}" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"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.regex=cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${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=user_db_reader,${cfg.base}" = "read";
"users" = "read";
"*" = "none";
};
"*" = {
"dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage";
"users" = "read";
"*" = "none";
};
};
};
};
};
};
declarativeContents = {
"dc=fudo,dc=org" = ''
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}
'';
};
};
};
}