Should have a working KDC...

This commit is contained in:
Niten 2021-03-17 02:47:13 +00:00
parent 945312e94e
commit 098b55d047
7 changed files with 210 additions and 159 deletions

View File

@ -40,7 +40,7 @@
local-groups = [ "fudo" "selby" "admin" ]; local-groups = [ "fudo" "selby" "admin" ];
local-admins = [ "niten" ]; local-admins = [ "niten" ];
admin-email = "niten@fudo.org"; admin-email = "niten@fudo.org";
gssapi-realm = "FUDO.ORG"; gssapi-realm = "RUS.SELBY.CA";
}; };
"informis.land" = { "informis.land" = {

View File

@ -91,8 +91,8 @@ in {
realm = "RUS.SELBY.CA"; realm = "RUS.SELBY.CA";
bind-addresses = [ "10.0.0.1" "127.0.0.1" "[::1]" ]; bind-addresses = [ "10.0.0.1" "127.0.0.1" "[::1]" ];
acl = { acl = {
"niten" = { perms = [ "all" ]; }; "niten" = { perms = [ "add" "change-password" "list" ]; };
"*/root" = { perms = [ "password" "list" ]; }; "*/root" = { perms = [ "all" ]; };
}; };
}; };

View File

@ -1,13 +1,13 @@
{ config, lib, ... }: { config, lib, ... }:
with lib;
let local-domain = "rus.selby.ca"; let local-domain = "rus.selby.ca";
in { in {
default-host = "10.0.0.1"; default-host = "10.0.0.1";
mx = [ "mail.fudo.org" ]; mx = [ "mail.fudo.org" ];
gssapi-realm = "FUDO.ORG"; gssapi-realm = toUpper local-domain;
hosts = { hosts = {
clunk = { clunk = {
@ -61,11 +61,11 @@ in {
}]; }];
kerberos = [{ kerberos = [{
port = 88; port = 88;
host = "france.fudo.org"; host = "clunk.${local-domain}";
}]; }];
kerberos-adm = [{ kerberos-adm = [{
port = 88; port = 749;
host = "france.fudo.org"; host = "clunk.${local-domain}";
}]; }];
ssh = [{ ssh = [{
port = 22; port = 22;
@ -80,15 +80,15 @@ in {
}]; }];
kerberos = [{ kerberos = [{
port = 88; port = 88;
host = "france.fudo.org"; host = "clunk.${local-domain}";
}]; }];
kerboros-master = [{ kerboros-master = [{
port = 88; port = 88;
host = "france.fudo.org"; host = "clunk.${local-domain}";
}]; }];
kpasswd = [{ kpasswd = [{
port = 464; port = 464;
host = "france.fudo.org"; host = "clunk.${local-domain}";
}]; }];
}; };
}; };

View File

@ -25,15 +25,22 @@ in {
krb5 = { krb5 = {
enable = true; enable = true;
appdefaults = {
forwardable = true;
proxiable = true;
encrypt = true;
forward = true;
};
libdefaults = { libdefaults = {
allow_weak_crypto = false; allow_weak_crypto = false;
dns_lookup_kdc = true; dns_lookup_kdc = true;
dns_lookup_realm = true;
forwardable = true; forwardable = true;
ignore_acceptor_hostname = true;
noaddresses = true;
rdns = false;
proxiable = true; proxiable = true;
}; };
kerberos = pkgs.heimdalFull;
}; };
services = { services = {

View File

@ -85,6 +85,13 @@ let
enable-gui = mkEnableOption "Install desktop GUI software."; enable-gui = mkEnableOption "Install desktop GUI software.";
docker-server = mkEnableOption "Enable Docker on the current host."; 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" ];
};
}; };
}; };

View File

@ -4,92 +4,82 @@ with lib;
let let
cfg = config.fudo.auth.kdc; 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: get-domain-hosts = domain:
mapAttrsToList (host: hostOpts: "${host}.${domain}") attrNames
(filterAttrs (host: hostOpts: hostOpts.domain == domain) config.fudo.hosts); (filterAttrs (host: hostOpts: hostOpts.domain == domain) config.fudo.hosts);
add-host-principals = realm: db-name: host: '' get-host-principals = realm: hostname:
${pkgs.krb5}/bin/kadmin.local -d ${db-name} addprinc -randkey host/${host} -r ${realm} let host = config.fudo.hosts.${hostname};
${pkgs.krb5}/bin/kadmin.local -d ${db-name} addprinc -randkey ssh/${host} -r ${realm} 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" '' pkgs.writeShellScript "initialize-kdc-db.sh" ''
if [ ! -e ${db-name} ]; then if [ ! -e ${key-file} ]; then
KRB5_CONFIG=/etc/krb5.conf ${pkgs.heimdalFull}/bin/kstash --key-file=${key-file} --random-key
KRB5_KDC_PROFILE=${kdc-conf} ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" RUS.SELBY.CA
PWD=$(${pkgs.pwgen}/bin/pwgen 40 1) ${add-hosts-principals realm kdc-conf}
printf "$PWD\n$PWD\n$PWD\n" | ${pkgs.krb5}/bin/kdb5_util -r ${realm} -sf ${key-file} -d ${db-name} -m create -s ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- add --random-key --use-defaults kadmin/${local-hostname}@${realm}
${pkgs.coreutils}/bin/chown -R ${user}:${group} $(dirname ${db-name}) ${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 fi
''; '';
initialize-kadmin = realm: db-name: user: group: kadmin-keytab: host: generate-kdc-conf = realm: db-file: key-file: acl-file:
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:
pkgs.writeText "kdc.conf" '' pkgs.writeText "kdc.conf" ''
[kdcdefaults] [kdc]
kdc_listen = ${concatStringsSep "," kdc-listen-addrs} database = {
kdc_tcp_listen = ${concatStringsSep "," kdc-listen-addrs} dbname = sqlite:${db-file}
realm = ${realm}
[realm] mkey_file = ${key-file}
${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
acl_file = ${acl-file} acl_file = ${acl-file}
admin_keytab = ${kadmin-keytab} log_file = ${iprop-log}
key_stash_file = ${key-stash-file}
} }
[dbmodules] [realms]
${realm} = { ${realm} = {
database_name = ${database} enable-http = false
db_library = db2
} }
[logging] [logging]
kdc = SYSLOG
admin_server = SYSLOG
default = 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, ... }: { aclEntry = { principal, ... }: {
options = with types; { options = with types; {
perms = mkOption { perms = let
type = listOf (enum (attrNames perm-map)); perms = [
"change-password"
"add"
"list"
"delete"
"modify"
"get"
"get-keys"
"all"
];
in mkOption {
type = listOf (enum perms);
description = "List of permissions."; description = "List of permissions.";
default = [ ]; default = [ ];
}; };
@ -103,6 +93,8 @@ let
}; };
}; };
perms-to-permstring = perms: concatStringsSep "," perms;
generate-acl-file = acl-entries: generate-acl-file = acl-entries:
pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList
(principal: opts: (principal: opts:
@ -110,15 +102,10 @@ let
optionalString (opts.target != null) " ${opts.target}" optionalString (opts.target != null) " ${opts.target}"
}") acl-entries)); }") acl-entries));
acl-file = generate-acl-file cfg.acl; kadmin-local = kdc-conf: kadmin-keytab:
pkgs.writeShellScriptBin "kadmin.local" ''
kdc-listen-addrs = map (ip: "${ip}:88") cfg.bind-addresses; ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} --keytab=${kadmin-keytab}
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;
in { in {
@ -170,18 +157,50 @@ in {
default = "/var/kerberos/master.key"; default = "/var/kerberos/master.key";
}; };
primary-keytab = mkOption {
type = str;
description = "Location of keytab for kadmind.";
default = "/var/kerberos/host.keytab";
};
kadmin-keytab = mkOption { kadmin-keytab = mkOption {
type = str; type = str;
description = "Location of keytab for kadmind."; description = "Location of keytab for kadmind.";
default = "/var/kerberos/kadmind.keytab"; 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 { config = mkIf cfg.enable {
users = { users = {
users.${cfg.user} = { users.${cfg.user} = {
isSystemUser = true; isSystemUser = true;
home = "/var/heimdal"; home = "/var/kerberos";
group = cfg.group; group = cfg.group;
}; };
@ -189,26 +208,21 @@ in {
}; };
krb5 = { krb5 = {
libdefaults = { default_realm = mkDefault cfg.realm; }; libdefaults = {
realms.${cfg.realm} = { key_stash_file = cfg.master-key-file; }; k5login_directory = cfg.k5login-directory;
extraConfig = mkAfter '' ticket_lifetime = cfg.max-ticket-lifetime;
[dbmodules] renew_lifetime = cfg.max-ticket-renewal;
${cfg.realm} = { };
database_name = ${kerberos-database} realms = { ${cfg.realm} = { enable-http = false; }; };
} extraConfig = ''
default = SYSLOG
[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}
}
''; '';
}; };
environment = { systemPackages = [ pkgs.kerberos pkgs.krb5 ]; }; environment = {
systemPackages =
[ pkgs.heimdalFull (kadmin-local kdc-conf cfg.kadmin-keytab) ];
};
fudo.system = { fudo.system = {
ensure-directories = { ensure-directories = {
@ -217,64 +231,79 @@ in {
group = cfg.group; group = cfg.group;
perms = "0740"; perms = "0740";
}; };
"/run/mit-kdc" = { };
user = cfg.user;
group = cfg.group; internal-port-map = {
perms = "0744"; kdc = {
}; internal-port = cfg.kdc-internal-port;
"/run/mit-kadmin" = { external-port = 88;
user = cfg.user; protocols = [ "tcp" "udp" ];
group = cfg.group;
perms = "0744";
}; };
}; };
services = { services = {
mit-kdc = { heimdal-kdc = {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
type = "forking"; description =
description = "MIT Kerberos Key Distribution Center (ticket server)."; "Heimdal Kerberos Key Distribution Center (ticket server).";
execStart = execStart =
"${pkgs.krb5}/bin/krb5kdc -r ${cfg.realm} -d ${kerberos-database} -P /run/mit-kdc/mit-kdc.pid"; "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=${
readWritePaths = [ "/run/mit-kdc" ]; toString cfg.kdc-internal-port
environment = { } --addresses=127.0.0.1";
KRB5_CONFIG = "/etc/krb5.conf"; environment = { KRB5_CONFIG = "/etc/krb5.conf"; };
KRB5_KDC_PROFILE = "${kdc-conf}";
};
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
workingDirectory = cfg.state-directory; workingDirectory = cfg.state-directory;
preStart = "${initialize-db cfg.realm kdc-conf cfg.user cfg.group
cfg.master-key-file kerberos-database}";
privateNetwork = false; privateNetwork = false;
addressFamilies = [ "AF_INET" "AF_INET6" ]; addressFamilies = [ "AF_INET" "AF_INET6" ];
requiredCapabilities = [ "CAP_NET_BIND_SERVICE+ep" ];
}; };
mit-kadmin = { heimdal-kdc-init = {
wantedBy = [ "multi-user.target" ]; requires = [ "heimdal-kdc.service" ];
after = [ "network.target" ]; description = "Initialization script for Heimdal KDC.";
requires = [ "mit-kdc.service" ]; type = "oneshot";
description = "MIT Kerberos Remote Administration Server."; execStart = "${initialize-db cfg.realm cfg.user cfg.group kdc-conf
execStart = cfg.master-key-file database-file cfg.max-ticket-lifetime
"${pkgs.krb5}/bin/kadmind -r ${cfg.realm} -P /run/mit-kadmin/mit-kadmin.pid"; cfg.max-ticket-renewal cfg.primary-keytab cfg.kadmin-keytab
readWritePaths = [ "/run/mit-kadmin" ]; "${config.networking.hostName}.${toLower cfg.realm}"}";
environment = {
KRB5_CONFIG = "/etc/krb5.conf";
KRB5_KDC_PROFILE = "${kdc-conf}";
};
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
workingDirectory = cfg.state-directory; workingDirectory = cfg.state-directory;
privateNetwork = false; environment = { KRB5_CONFIG = "/etc/krb5.conf"; };
# 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" ];
}; };
}; };
}; };
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 ];
};
}; };
} }

View File

@ -260,12 +260,12 @@ let
"A list of paths which should be inaccessible to the service."; "A list of paths which should be inaccessible to the service.";
default = [ "/home" "/root" ]; default = [ "/home" "/root" ];
}; };
noExecPaths = mkOption { # noExecPaths = mkOption {
type = listOf str; # type = listOf str;
description = # description =
"A list of paths where the service will not be allowed to run executables."; # "A list of paths where the service will not be allowed to run executables.";
default = [ "/home" "/root" "/tmp" "/var" ]; # default = [ "/home" "/root" "/tmp" "/var" ];
}; # };
readOnlyPaths = mkOption { readOnlyPaths = mkOption {
type = listOf str; type = listOf str;
description = description =
@ -390,11 +390,11 @@ let
type = port; type = port;
description = "External port on which to listen for traffic."; description = "External port on which to listen for traffic.";
}; };
protocol = mkOption { protocols = mkOption {
type = nullOr str; type = listOf str;
description = description =
"Protocol for which to forward ports. Default is tcp & udp."; "Protocols for which to forward ports. Default is tcp-only.";
default = null; default = [ "tcp" ];
}; };
}; };
}; };
@ -464,15 +464,22 @@ in {
# opts.external-port) cfg.internal-port-map); # opts.external-port) cfg.internal-port-map);
# }; # };
services.xinetd = { services.xinetd = mkIf ((length (attrNames cfg.internal-port-map)) > 0) {
enable = true; enable = true;
services = mapAttrsToList (name: opts: { services = let
name = name; 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; unlisted = true;
port = opts.external-port; port = opts.external-port;
server = "${pkgs.coreutils}/bin/false"; server = "${pkgs.coreutils}/bin/false";
extraConfig = "redirect = localhost ${toString opts.internal-port}"; extraConfig = "redirect = localhost ${toString opts.internal-port}";
}) cfg.internal-port-map; protocol = opts.protocol;
}) svcs-protocols;
}; };
systemd.timers = mapAttrs (name: opts: { systemd.timers = mapAttrs (name: opts: {
@ -559,7 +566,8 @@ in {
ReadWritePaths = opts.readWritePaths; ReadWritePaths = opts.readWritePaths;
ReadOnlyPaths = opts.readOnlyPaths; ReadOnlyPaths = opts.readOnlyPaths;
InaccessiblePaths = opts.inaccessiblePaths; InaccessiblePaths = opts.inaccessiblePaths;
NoExecPaths = opts.noExecPaths; # Apparently not supported yet?
# NoExecPaths = opts.noExecPaths;
ExecPaths = opts.execPaths; ExecPaths = opts.execPaths;
}; };
}) config.fudo.system.services; }) config.fudo.system.services;