diff --git a/config/domains.nix b/config/domains.nix index 3308d16..51a06c0 100644 --- a/config/domains.nix +++ b/config/domains.nix @@ -40,7 +40,7 @@ local-groups = [ "fudo" "selby" "admin" ]; local-admins = [ "niten" ]; admin-email = "niten@fudo.org"; - gssapi-realm = "FUDO.ORG"; + gssapi-realm = "RUS.SELBY.CA"; }; "informis.land" = { diff --git a/config/hosts/clunk.nix b/config/hosts/clunk.nix index 1e1d12a..73d4f87 100644 --- a/config/hosts/clunk.nix +++ b/config/hosts/clunk.nix @@ -91,8 +91,8 @@ in { realm = "RUS.SELBY.CA"; bind-addresses = [ "10.0.0.1" "127.0.0.1" "[::1]" ]; acl = { - "niten" = { perms = [ "all" ]; }; - "*/root" = { perms = [ "password" "list" ]; }; + "niten" = { perms = [ "add" "change-password" "list" ]; }; + "*/root" = { perms = [ "all" ]; }; }; }; diff --git a/config/networks/rus.selby.ca.nix b/config/networks/rus.selby.ca.nix index 2cd6ba1..3b31147 100644 --- a/config/networks/rus.selby.ca.nix +++ b/config/networks/rus.selby.ca.nix @@ -1,13 +1,13 @@ { config, lib, ... }: +with lib; let local-domain = "rus.selby.ca"; - in { default-host = "10.0.0.1"; mx = [ "mail.fudo.org" ]; - gssapi-realm = "FUDO.ORG"; + gssapi-realm = toUpper local-domain; hosts = { clunk = { @@ -61,11 +61,11 @@ in { }]; kerberos = [{ port = 88; - host = "france.fudo.org"; + host = "clunk.${local-domain}"; }]; kerberos-adm = [{ - port = 88; - host = "france.fudo.org"; + port = 749; + host = "clunk.${local-domain}"; }]; ssh = [{ port = 22; @@ -80,15 +80,15 @@ in { }]; kerberos = [{ port = 88; - host = "france.fudo.org"; + host = "clunk.${local-domain}"; }]; kerboros-master = [{ port = 88; - host = "france.fudo.org"; + host = "clunk.${local-domain}"; }]; kpasswd = [{ port = 464; - host = "france.fudo.org"; + host = "clunk.${local-domain}"; }]; }; }; diff --git a/config/profiles/common.nix b/config/profiles/common.nix index 9c02ad6..9a3dc80 100644 --- a/config/profiles/common.nix +++ b/config/profiles/common.nix @@ -25,15 +25,22 @@ in { krb5 = { enable = true; + appdefaults = { + forwardable = true; + proxiable = true; + encrypt = true; + forward = true; + }; + libdefaults = { allow_weak_crypto = false; dns_lookup_kdc = true; + dns_lookup_realm = true; forwardable = true; - ignore_acceptor_hostname = true; - noaddresses = true; - rdns = false; proxiable = true; }; + + kerberos = pkgs.heimdalFull; }; services = { diff --git a/lib/fudo/hosts.nix b/lib/fudo/hosts.nix index ef1f73d..3b6d544 100644 --- a/lib/fudo/hosts.nix +++ b/lib/fudo/hosts.nix @@ -85,6 +85,13 @@ let enable-gui = mkEnableOption "Install desktop GUI software."; docker-server = mkEnableOption "Enable Docker on the current host."; + + kerberos-services = mkOption { + type = listOf str; + description = + "List of services which should exist for this host, if it belongs to a realm."; + default = [ "ssh" "host" ]; + }; }; }; diff --git a/lib/fudo/kdc.nix b/lib/fudo/kdc.nix index b27bf57..a4a4ef4 100644 --- a/lib/fudo/kdc.nix +++ b/lib/fudo/kdc.nix @@ -4,92 +4,82 @@ with lib; let cfg = config.fudo.auth.kdc; - kerberos-database = "${cfg.state-directory}/kerberos.db"; + 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: - mapAttrsToList (host: hostOpts: "${host}.${domain}") + attrNames (filterAttrs (host: hostOpts: hostOpts.domain == domain) config.fudo.hosts); - add-host-principals = realm: db-name: host: '' - ${pkgs.krb5}/bin/kadmin.local -d ${db-name} addprinc -randkey host/${host} -r ${realm} - ${pkgs.krb5}/bin/kadmin.local -d ${db-name} addprinc -randkey ssh/${host} -r ${realm} - ''; + get-host-principals = realm: hostname: + let host = config.fudo.hosts.${hostname}; + in map (service: "${service}/${hostname}.${toLower realm}@${realm}") + host.kerberos-services; - initialize-db = realm: kdc-conf: user: group: key-file: db-name: + 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: local-hostname: pkgs.writeShellScript "initialize-kdc-db.sh" '' - if [ ! -e ${db-name} ]; then - KRB5_CONFIG=/etc/krb5.conf - KRB5_KDC_PROFILE=${kdc-conf} - PWD=$(${pkgs.pwgen}/bin/pwgen 40 1) - printf "$PWD\n$PWD\n$PWD\n" | ${pkgs.krb5}/bin/kdb5_util -r ${realm} -sf ${key-file} -d ${db-name} -m create -s - ${pkgs.coreutils}/bin/chown -R ${user}:${group} $(dirname ${db-name}) + 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}" RUS.SELBY.CA + ${add-hosts-principals realm kdc-conf} + ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- add --random-key --use-defaults kadmin/${local-hostname}@${realm} + ${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/${local-hostname}@${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 ''; - initialize-kadmin = realm: db-name: user: group: kadmin-keytab: host: - let - domain = toLower realm; - hosts = get-domain-hosts domain; - in pkgs.writeShellScript "initialize-kadmin.sh" '' - if [ ! -e ${kadmin-keytab} ]; then - # ${pkgs.krb5}/bin/kadmin.local -d ${db-name} addprinc -randkey kadmin/${host}.${domain} - # ${pkgs.krb5}/bin/kadmin.local -d ${db-name} ktadd -k ${kadmin-keytab} kadmin/${host}.${domain} - # TODO: extract kadmin keytab - # ${ - concatStringsSep "\n" (map (add-host-principals realm db-name) hosts) - } - fi - ''; - - generate-kdc-conf = - realm: database: kdc-listen-addrs: kadmin-listen-addrs: kpasswd-listen-addrs: acl-file: kadmin-keytab: key-stash-file: + generate-kdc-conf = realm: db-file: key-file: acl-file: pkgs.writeText "kdc.conf" '' - [kdcdefaults] - kdc_listen = ${concatStringsSep "," kdc-listen-addrs} - kdc_tcp_listen = ${concatStringsSep "," kdc-listen-addrs} - - [realm] - ${realm} = { - kadmind_listen = ${concatStringsSep "," kadmin-listen-addrs} - kpasswd_listen = ${concatStringsSep "," kpasswd-listen-addrs} - max_life = 24h 0m 0s - max_renewable_life = 14d 0h 0m 0s + [kdc] + database = { + dbname = sqlite:${db-file} + realm = ${realm} + mkey_file = ${key-file} acl_file = ${acl-file} - admin_keytab = ${kadmin-keytab} - key_stash_file = ${key-stash-file} + log_file = ${iprop-log} } - [dbmodules] + [realms] ${realm} = { - database_name = ${database} - db_library = db2 + enable-http = false } [logging] - kdc = SYSLOG - admin_server = SYSLOG default = SYSLOG ''; - perm-map = { - "add" = "a"; - "all" = "x"; - "password" = "c"; - "delete" = "d"; - "inquire" = "i"; - "list" = "l"; - "modify" = "m"; - "propagate" = "p"; - "set-key" = "s"; - }; - - perms-to-permstring = perms: - concatStringsSep "" (map (perm: perm-map.${perm}) perms); - aclEntry = { principal, ... }: { options = with types; { - perms = mkOption { - type = listOf (enum (attrNames perm-map)); + perms = let + perms = [ + "change-password" + "add" + "list" + "delete" + "modify" + "get" + "get-keys" + "all" + ]; + in mkOption { + type = listOf (enum perms); description = "List of permissions."; default = [ ]; }; @@ -103,6 +93,8 @@ let }; }; + perms-to-permstring = perms: concatStringsSep "," perms; + generate-acl-file = acl-entries: pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList (principal: opts: @@ -110,15 +102,10 @@ let optionalString (opts.target != null) " ${opts.target}" }") acl-entries)); - acl-file = generate-acl-file cfg.acl; - - kdc-listen-addrs = map (ip: "${ip}:88") cfg.bind-addresses; - kadmin-listen-addrs = map (ip: "${ip}:749") cfg.bind-addresses; - kpasswd-listen-addrs = map (ip: "${ip}:464") cfg.bind-addresses; - - kdc-conf = generate-kdc-conf cfg.realm kerberos-database kdc-listen-addrs - kadmin-listen-addrs kpasswd-listen-addrs acl-file cfg.kadmin-keytab - cfg.master-key-file; + kadmin-local = kdc-conf: kadmin-keytab: + pkgs.writeShellScriptBin "kadmin.local" '' + ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} --keytab=${kadmin-keytab} + ''; in { @@ -170,18 +157,50 @@ in { 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"; }; + + 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/heimdal"; + home = "/var/kerberos"; group = cfg.group; }; @@ -189,26 +208,21 @@ in { }; krb5 = { - libdefaults = { default_realm = mkDefault cfg.realm; }; - realms.${cfg.realm} = { key_stash_file = cfg.master-key-file; }; - extraConfig = mkAfter '' - [dbmodules] - ${cfg.realm} = { - database_name = ${kerberos-database} - } - - [realm] - ${cfg.realm} = { - kadmind_listen = ${concatStringsSep "," kadmin-listen-addrs} - kpasswd_listen = ${concatStringsSep "," kpasswd-listen-addrs} - acl_file = ${acl-file} - admin_keytab = ${cfg.kadmin-keytab} - key_stash_file = ${cfg.master-key-file} - } + libdefaults = { + 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 = SYSLOG ''; }; - environment = { systemPackages = [ pkgs.kerberos pkgs.krb5 ]; }; + environment = { + systemPackages = + [ pkgs.heimdalFull (kadmin-local kdc-conf cfg.kadmin-keytab) ]; + }; fudo.system = { ensure-directories = { @@ -217,64 +231,79 @@ in { group = cfg.group; perms = "0740"; }; - "/run/mit-kdc" = { - user = cfg.user; - group = cfg.group; - perms = "0744"; - }; - "/run/mit-kadmin" = { - user = cfg.user; - group = cfg.group; - perms = "0744"; + }; + + internal-port-map = { + kdc = { + internal-port = cfg.kdc-internal-port; + external-port = 88; + protocols = [ "tcp" "udp" ]; }; }; services = { - mit-kdc = { + heimdal-kdc = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - type = "forking"; - description = "MIT Kerberos Key Distribution Center (ticket server)."; + description = + "Heimdal Kerberos Key Distribution Center (ticket server)."; execStart = - "${pkgs.krb5}/bin/krb5kdc -r ${cfg.realm} -d ${kerberos-database} -P /run/mit-kdc/mit-kdc.pid"; - readWritePaths = [ "/run/mit-kdc" ]; - environment = { - KRB5_CONFIG = "/etc/krb5.conf"; - KRB5_KDC_PROFILE = "${kdc-conf}"; - }; + "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=${ + toString cfg.kdc-internal-port + } --addresses=127.0.0.1"; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; user = cfg.user; group = cfg.group; workingDirectory = cfg.state-directory; - preStart = "${initialize-db cfg.realm kdc-conf cfg.user cfg.group - cfg.master-key-file kerberos-database}"; privateNetwork = false; addressFamilies = [ "AF_INET" "AF_INET6" ]; - requiredCapabilities = [ "CAP_NET_BIND_SERVICE+ep" ]; }; - mit-kadmin = { - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - requires = [ "mit-kdc.service" ]; - description = "MIT Kerberos Remote Administration Server."; - execStart = - "${pkgs.krb5}/bin/kadmind -r ${cfg.realm} -P /run/mit-kadmin/mit-kadmin.pid"; - readWritePaths = [ "/run/mit-kadmin" ]; - environment = { - KRB5_CONFIG = "/etc/krb5.conf"; - KRB5_KDC_PROFILE = "${kdc-conf}"; - }; + heimdal-kdc-init = { + requires = [ "heimdal-kdc.service" ]; + 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 + "${config.networking.hostName}.${toLower cfg.realm}"}"; user = cfg.user; group = cfg.group; workingDirectory = cfg.state-directory; - privateNetwork = false; - # postStart = - # "${initialize-kadmin cfg.realm kerberos-database cfg.user cfg.group - # cfg.kadmin-keytab config.networking.hostName}"; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; }; }; }; + + 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.kadmin-keytab}"; + } + ]; + }; + + networking.firewall = { + allowedTCPPorts = [ 88 749 ]; + allowedUDPPorts = [ 88 464 ]; + }; }; } diff --git a/lib/fudo/system.nix b/lib/fudo/system.nix index 025a515..3acf379 100644 --- a/lib/fudo/system.nix +++ b/lib/fudo/system.nix @@ -260,12 +260,12 @@ let "A list of paths which should be inaccessible to the service."; default = [ "/home" "/root" ]; }; - noExecPaths = mkOption { - type = listOf str; - description = - "A list of paths where the service will not be allowed to run executables."; - default = [ "/home" "/root" "/tmp" "/var" ]; - }; + # noExecPaths = mkOption { + # type = listOf str; + # description = + # "A list of paths where the service will not be allowed to run executables."; + # default = [ "/home" "/root" "/tmp" "/var" ]; + # }; readOnlyPaths = mkOption { type = listOf str; description = @@ -390,11 +390,11 @@ let type = port; description = "External port on which to listen for traffic."; }; - protocol = mkOption { - type = nullOr str; + protocols = mkOption { + type = listOf str; description = - "Protocol for which to forward ports. Default is tcp & udp."; - default = null; + "Protocols for which to forward ports. Default is tcp-only."; + default = [ "tcp" ]; }; }; }; @@ -464,15 +464,22 @@ in { # opts.external-port) cfg.internal-port-map); # }; - services.xinetd = { + services.xinetd = mkIf ((length (attrNames cfg.internal-port-map)) > 0) { enable = true; - services = mapAttrsToList (name: opts: { - name = name; + services = let + svcs = mapAttrsToList (name: opts: opts // { name = name; }) + cfg.internal-port-map; + svcs-protocols = concatMap + (svc: map (protocol: svc // { protocol = protocol; }) svc.protocols) + svcs; + in map (opts: { + name = opts.name; unlisted = true; port = opts.external-port; server = "${pkgs.coreutils}/bin/false"; extraConfig = "redirect = localhost ${toString opts.internal-port}"; - }) cfg.internal-port-map; + protocol = opts.protocol; + }) svcs-protocols; }; systemd.timers = mapAttrs (name: opts: { @@ -559,7 +566,8 @@ in { ReadWritePaths = opts.readWritePaths; ReadOnlyPaths = opts.readOnlyPaths; InaccessiblePaths = opts.inaccessiblePaths; - NoExecPaths = opts.noExecPaths; + # Apparently not supported yet? + # NoExecPaths = opts.noExecPaths; ExecPaths = opts.execPaths; }; }) config.fudo.system.services;