{ config, lib, pkgs, ... }: with lib; let cfg = config.fudo.auth.kdc; database-file = "${cfg.state-directory}/principals.db"; iprop-log = "${cfg.state-directory}/iprop.log"; acl-file = generate-acl-file cfg.acl; kdc-conf = generate-kdc-conf cfg.realm database-file cfg.master-key-file acl-file; get-domain-hosts = domain: attrNames (filterAttrs (host: hostOpts: hostOpts.domain == domain) config.fudo.hosts); get-host-principals = realm: hostname: let host = config.fudo.hosts.${hostname}; in map (service: "${service}/${hostname}.${toLower realm}@${realm}") host.kerberos-services; add-principal-str = kdc-conf: principal: "${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- add --random-key --use-defaults ${principal}"; add-hosts-principals = realm: kdc-conf: concatStringsSep "\n" (map (add-principal-str kdc-conf) (concatMap (get-host-principals realm) (get-domain-hosts (toLower realm)))); initialize-db = realm: user: group: kdc-conf: key-file: db-name: max-lifetime: max-renewal: primary-keytab: kadmin-keytab: kpasswd-keytab: local-hostname: pkgs.writeShellScript "initialize-kdc-db.sh" '' if [ ! -e ${key-file} ]; then ${pkgs.heimdalFull}/bin/kstash --key-file=${key-file} --random-key ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" ${realm} ${add-hosts-principals realm kdc-conf} ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- ext_keytab --keytab=${primary-keytab} */${local-hostname}@${realm} ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- ext_keytab --keytab=${kadmin-keytab} kadmin/admin@${realm} ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- ext_keytab --keytab=${kpasswd-keytab} kadmin/changepw@${realm} ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- ext_keytab --keytab=${kpasswd-keytab} kadmin/changepw@${realm} #${pkgs.coreutils}/bin/chown ${user}:${group} ${key-file} #${pkgs.coreutils}/bin/chown ${user}:${group} ${db-name} #${pkgs.coreutils}/bin/chown ${user}:${group} ${iprop-log} #${pkgs.coreutils}/bin/chown ${user}:${group} ${primary-keytab} #${pkgs.coreutils}/bin/chown ${user}:${group} ${kadmin-keytab} fi ''; generate-kdc-conf = realm: db-file: key-file: acl-file: pkgs.writeText "kdc.conf" '' [kdc] database = { dbname = sqlite:${db-file} realm = ${realm} mkey_file = ${key-file} acl_file = ${acl-file} log_file = ${iprop-log} } [realms] ${realm} = { enable-http = false } [logging] kdc = FILE:/var/kerberos/kerberos.log default = FILE:/var/kerberos/kerberos.log ''; aclEntry = { principal, ... }: { options = with types; { perms = let perms = [ "change-password" "add" "list" "delete" "modify" "get" "get-keys" "all" ]; in mkOption { type = listOf (enum perms); description = "List of permissions."; default = [ ]; }; target = mkOption { type = nullOr str; description = "Target principals."; default = null; example = "hosts/*@REALM.COM"; }; }; }; perms-to-permstring = perms: concatStringsSep "," perms; generate-acl-file = acl-entries: pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList (principal: opts: "${principal} ${perms-to-permstring opts.perms}${ optionalString (opts.target != null) " ${opts.target}" }") acl-entries)); kadmin-local = kdc-conf: kadmin-keytab: pkgs.writeShellScriptBin "kadmin.local" '' ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} --keytab=${kadmin-keytab} ''; in { options.fudo.auth.kdc = with types; { enable = mkEnableOption "Fudo KDC"; realm = mkOption { type = str; description = "The realm for which we are the acting KDC."; }; acl = mkOption { type = attrsOf (submodule aclEntry); description = "Mapping of pricipals to a list of permissions."; default = { "*/admin" = [ "all" ]; }; example = { "*/root" = [ "all" ]; "admin-user" = [ "add" "list" "modify" ]; }; }; bind-addresses = mkOption { type = listOf str; description = "A list of IP addresses on which to bind."; default = [ ]; }; user = mkOption { type = str; description = "User as which to run Heimdal servers."; default = "kerberos"; }; group = mkOption { type = str; description = "Group as which to run Heimdal servers."; default = "kerberos"; }; state-directory = mkOption { type = str; description = "Path at which to store kerberos database."; default = "/var/kerberos"; }; master-key-file = mkOption { type = str; description = "File containing the master key for the realm."; default = "/var/kerberos/master.key"; }; primary-keytab = mkOption { type = str; description = "Location of keytab for kadmind."; default = "/var/kerberos/host.keytab"; }; kadmin-keytab = mkOption { type = str; description = "Location of keytab for kadmind."; default = "/var/kerberos/kadmind.keytab"; }; kpasswdd-keytab = mkOption { type = str; description = "Location of keytab for kpasswdd."; default = "/var/kerberos/kpasswdd.keytab"; }; kdc-internal-port = mkOption { type = port; description = "Localhost port on which to listen for KDC traffic. Port 88 will be forwarded"; default = 4088; }; # k5login-directory = mkOption { # type = str; # description = # "Directory in which k5login files are stored for local users (equivalent to ~/.k5login)."; # default = "/var/kerberos/k5login"; # }; max-ticket-lifetime = mkOption { type = str; description = "Maximum lifetime of a single ticket in this realm."; default = "1d"; }; max-ticket-renewal = mkOption { type = str; description = "Maximum time a ticket may be renewed in this realm."; default = "7d"; }; }; config = mkIf cfg.enable { users = { users.${cfg.user} = { isSystemUser = true; home = "/var/kerberos"; group = cfg.group; }; groups.${cfg.group} = { members = [ cfg.user ]; }; }; krb5 = { libdefaults = { # Stick to ~/.k5login # k5login_directory = cfg.k5login-directory; ticket_lifetime = cfg.max-ticket-lifetime; renew_lifetime = cfg.max-ticket-renewal; }; realms = { ${cfg.realm} = { enable-http = false; }; }; extraConfig = '' default = FILE:/var/kerberos/kerberos.log ''; }; environment = { systemPackages = [ pkgs.heimdalFull (kadmin-local kdc-conf cfg.kadmin-keytab) ]; etc = { "krb5.keytab" = { user = "root"; group = "root"; mode = "0400"; source = cfg.primary-keytab; }; }; }; fudo.system = { ensure-directories = { "${cfg.state-directory}" = { user = cfg.user; group = cfg.group; perms = "0740"; }; }; services = { heimdal-kdc = let listen-addrs = concatStringsSep " " (map (addr: "--addresses=${addr}") cfg.bind-addresses); command = "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; in { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; description = "Heimdal Kerberos Key Distribution Center (ticket server)."; execStart = command; user = cfg.user; group = cfg.group; workingDirectory = cfg.state-directory; privateNetwork = false; addressFamilies = [ "AF_INET" "AF_INET6" ]; requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; }; heimdal-kdc-init = { requires = [ "heimdal-kdc.service" ]; wantedBy = [ "multi-user.target" ]; description = "Initialization script for Heimdal KDC."; type = "oneshot"; execStart = "${initialize-db cfg.realm cfg.user cfg.group kdc-conf cfg.master-key-file database-file cfg.max-ticket-lifetime cfg.max-ticket-renewal cfg.primary-keytab cfg.kadmin-keytab cfg.kpasswdd-keytab "${config.networking.hostName}.${toLower cfg.realm}"}"; user = cfg.user; group = cfg.group; protectSystem = "full"; addressFamilies = [ "AF_INET" "AF_INET6" ]; workingDirectory = cfg.state-directory; environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; }; }; }; # FIXME: is this even allowed to be a link? # systemd.tmpfiles.rules = mkIf (cfg.primary-keytab != "/etc/krb5.keytab") # [ "L /etc/krb5.keytab - - - - ${cfg.primary-keytab}" ]; services.xinetd = { enable = true; services = [ { name = "kerberos-adm"; user = cfg.user; server = "${pkgs.heimdalFull}/libexec/heimdal/kadmind"; protocol = "tcp"; serverArgs = "--config-file=${kdc-conf} --keytab=${cfg.kadmin-keytab}"; } { name = "kpasswd"; user = cfg.user; server = "${pkgs.heimdalFull}/libexec/heimdal/kpasswdd"; protocol = "udp"; serverArgs = "--config-file=${kdc-conf} --keytab=${cfg.kpasswdd-keytab}"; } ]; }; networking.firewall = { allowedTCPPorts = [ 88 749 ]; allowedUDPPorts = [ 88 464 ]; }; }; }