diff --git a/config/domains.nix b/config/domains.nix index d46a497..e9deac6 100644 --- a/config/domains.nix +++ b/config/domains.nix @@ -62,20 +62,25 @@ local-admins = [ "niten" ]; admin-email = "viator@informis.land"; gssapi-realm = "INFORMIS.LAND"; + kerberos-master = "procul"; + kerberos-slaves = [ "legatus" ]; + primary-nameserver = "procul"; }; - eur.fudo.org = { + "eur.fudo.org" = { local-networks = [ "208.81.1.128/28" "208.81.3.112/28" "91.229.23.204/31" ]; - local-users = [ "niten"]; + local-users = [ "niten" "reaper" ]; local-groups = [ "admin" ]; - local-admins = [ "niten" ]; + local-admins = [ "niten" "reaper" ]; admin-email = "nitenn@fudo.org"; gssapi-realm = "FUDO.ORG"; + # kerberos-master = "legatus"; + primary-nameserver = "legatus"; }; }; } diff --git a/config/hardware/legatus.nix b/config/hardware/legatus.nix index 05d393a..6272fc3 100644 --- a/config/hardware/legatus.nix +++ b/config/hardware/legatus.nix @@ -67,8 +67,8 @@ with lib; { interfaces = { extif0 = { - # output of: echo legatus-extif0|md5sum|sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/' - macAddress = pkgs.lib.fudo.network.generate-mac-address "legatus" "extif0"; + macAddress = + pkgs.lib.fudo.network.generate-mac-address "legatus" "extif0"; }; }; }; diff --git a/config/hardware/nutboy3.nix b/config/hardware/nutboy3.nix new file mode 100644 index 0000000..f7e134e --- /dev/null +++ b/config/hardware/nutboy3.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, ... }: + +with lib; { + boot = { + initrd = { + availableKernelModules = [ + FIXME + ]; + kernelModules = [ "dm-snapshot" ]; + }; + kernelModules = [ FIXME ]; + extraModulePackages = [ ]; + loader.grub = { + enable = true; + version = 2; + device = "/dev/sda"; + }; + + supportedFilesystems = [ "btrfs" ]; + }; + + fileSystems = { + "/" = { + device = "root-tmpfs"; + fsType = "tmpfs"; + options = [ "mode=755" "noexec" ]; + }; + + "/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "ext4"; + options = [ "noexec" "noatime" "nodiratime" ]; + }; + + "/nix" = { + device = "/dev/disk/by-label/data"; + fsType = "btrfs"; + options = [ "subvol=@nix" "compress=zstd" "noatime" "nodiratime" ]; + }; + + "/var/log" = { + device = "/dev/disk/by-label/data"; + fsType = "btrfs"; + options = [ "subvol=@logs" "compress=zstd" "noatime" "nodiratime" "noexec" ]; + neededForBoot = true; + }; + + "/state" = { + device = "/dev/disk/by-label/data"; + fsType = "btrfs"; + options = [ "subvol=@state" "compress=zstd" "noatime" "nodiratime" "noexec" ]; + }; + }; + + swapDevices = [{ device = "/dev/disk/by-label/swap"; }]; + + networking = { + macvlans = { + extif0 = { + interface = "eno1"; + mode = "bridge"; + }; + }; + + useDHCP = false; + + interfaces = { + extif0 = { + macAddress = + pkgs.lib.fudo.network.generate-mac-address config.instance.hostname "extif0"; + }; + }; + }; +} diff --git a/config/host-config/legatus.nix b/config/host-config/legatus.nix index 95afa7d..50f29a5 100644 --- a/config/host-config/legatus.nix +++ b/config/host-config/legatus.nix @@ -8,7 +8,6 @@ let domain = config.fudo.domains.${domain-name}; site-name = config.fudo.hosts.${hostname}.site; site = config.fudo.sites.${site-name}; - host-fqdn = "${hostname}.${domain-name}"; local-packages = with pkgs; [ ldns.examples ]; @@ -56,80 +55,67 @@ in { # }; # }; - fudo = { + fudo = { hosts.legatus.external-interfaces = [ "extif0" ]; + secrets.host-secrets.legatus = let + files = config.fudo.secrets.files; + in { + # postgres-keytab = { + # source-file = files.service-keytabs.procul.postgres; + # target-file = "/srv/postgres/secure/postgres.keytab"; + # user = "root"; + # }; - # secrets.host-secrets.procul = let - # files = config.fudo.secrets.files; - # in { - # postgres-keytab = { - # source-file = files.service-keytabs.procul.postgres; - # target-file = "/srv/postgres/secure/postgres.keytab"; - # user = "root"; - # }; + # gitea-database-password = { + # source-file = files.service-passwords.procul.gitea-database; + # target-file = "/srv/gitea/secure/database.passwd"; + # user = config.fudo.git.user; + # }; - # gitea-database-password = { - # source-file = files.service-passwords.procul.gitea-database; - # target-file = "/srv/gitea/secure/database.passwd"; - # user = config.fudo.git.user; - # }; - # }; + heimdal-master-key = { + source-file = files.realm-master-keys."FUDO.ORG"; + target-file = "/run/heimdal/master-key"; + user = config.fudo.auth.kdc.user; + }; - # client.dns = { - # enable = true; - # ipv4 = true; - # ipv6 = true; - # user = "fudo-client"; - # external-interface = "extif0"; - # }; + ipropd-keytab = { + source-file = files.service-keytabs.legatus.ipropd; + target-file = "/run/heimdal/ipropd.keytab"; + user = config.fudo.auth.kdc.user; + }; + }; - # auth.kdc = { - # enable = true; - # realm = "INFORMIS.LAND"; - # bind-addresses = [ host-ipv4 "127.0.0.1" ]; - # acl = { - # "niten" = { perms = [ "add" "change-password" "list" ]; }; - # "*/root" = { perms = [ "all" ]; }; - # }; - # }; + client.dns = { + ipv4 = true; + ipv6 = true; + user = "fudo-client"; + external-interface = "extif0"; + }; - # secure-dns-proxy = { - # enable = true; - # upstream-dns = - # [ "https://1.1.1.1/dns-query" "https://1.0.0.1/dns-query" ]; - # bootstrap-dns = "1.1.1.1"; - # listen-ips = [ "127.0.0.1" ]; - # listen-port = 53; - # allowed-networks = [ "1.1.1.1/32" "1.0.0.1/32" "localhost" "link-local" ]; - # }; + auth.kdc = { + enable = true; + realm = "FUDO.ORG"; + bind-addresses = [ host-ipv4 "127.0.0.1" ]; + master-key-file = + secrets.heimdal-master-key.target-file; + state-directory = "/state/kerberos"; + slave-config = { + master-host = "france"; + ipropd-keytab = secrets.ipropd-keytab.target-file; + }; + }; - # dns = { - # enable = true; - # identity = "procul.informis.land"; - # nameservers = { - # ns1 = { - # ipv4-address = host-ipv4; - # description = "Primary Informis Nameserver"; - # }; - # ns2 = { - # ipv4-address = host-ipv4; - # description = "Secondary Informis Nameserver"; - # }; - # }; + secure-dns-proxy = { + enable = true; + upstream-dns = + [ "https://1.1.1.1/dns-query" "https://1.0.0.1/dns-query" ]; + bootstrap-dns = "1.1.1.1"; + listen-ips = [ "127.0.0.1" ]; + listen-port = 53; + allowed-networks = [ "1.1.1.1/32" "1.0.0.1/32" "localhost" "link-local" ]; + }; - # listen-ips = [ host-ipv4 ]; - - # domains = { - # "informis.land" = { - # dnssec = true; - # default-host = host-ipv4; - # gssapi-realm = "INFORMIS.LAND"; - # mx = [ "smtp.informis.land" ]; - # network-definition = config.fudo.networks."informis.land"; - # dmarc-report-address = "dmarc-report@informis.land"; - # }; - # }; - # }; + dns.state-directory = "/state/nsd"; # mail-server = { # enable = true; @@ -218,16 +204,5 @@ in { # listen-port = 2222; # }; # }; - - # acme = { - # enable = true; - # admin-address = "admin@${domain-name}"; - # hostnames = [ - # "informis.land" - # "imap.informis.land" - # "smtp.informis.land" - # "gemini.informis.land" - # ]; - # }; }; } diff --git a/config/host-config/limina.nix b/config/host-config/limina.nix index 7a2bae2..55fec23 100644 --- a/config/host-config/limina.nix +++ b/config/host-config/limina.nix @@ -60,13 +60,7 @@ in { network-definition = config.fudo.networks.${domain-name}; }; - client.dns = { - enable = true; - external-interface = "enp1s0"; - ## This is now set by hosts.nix - # password-file = - # config.fudo.secrets.host-secrets.limina.backplane-client-passwd.target-file; - }; + client.dns.external-interface = "enp1s0"; garbage-collector = { enable = true; diff --git a/config/host-config/nutboy3.nix b/config/host-config/nutboy3.nix new file mode 100644 index 0000000..270bcd9 --- /dev/null +++ b/config/host-config/nutboy3.nix @@ -0,0 +1,196 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = "nutboy3"; + host-ipv4 = "199.87.154.175"; + domain-name = config.fudo.hosts.${hostname}.domain; + domain = config.fudo.domains.${domain-name}; + site-name = config.fudo.hosts.${hostname}.site; + site = config.fudo.sites.${site-name}; + + local-packages = with pkgs; [ ldns.examples ]; + + secrets = config.fudo.secrets.host-secrets.${hostname}; + +in { + networking = { + enableIPv6 = true; + + nameservers = [ "1.1.1.1" ]; + defaultGateway = { + address = site.gateway-v4; + interface = "extif0"; + }; + + interfaces.extif0.ipv4.addresses = [{ + address = host-ipv4; + prefixLength = 31; + }]; + }; + + systemd.tmpfiles.rules = [ + "L /etc/adjtime - - - - /state/etc/adjtime" + ]; + + environment.systemPackages = local-packages; + + # networking.firewall.allowedTCPPorts = [ 80 443 ]; + + # informis.cl-gemini = { + # enable = true; + + # hostname = "gemini.informis.land"; + # server-ip = host-ipv4; + # document-root = "/srv/gemini/root"; + # textfiles-archive = "${pkgs.textfiles}"; + # slynk-port = 4005; + + # feeds = { + # viator = { + # title = "viator's phlog"; + # path = "/home/viator/gemini-public/feed/"; + # url = "gemini://informis.land/user/viator/feed/"; + # }; + # }; + # }; + + fudo = { + hosts.${hostname}.external-interfaces = [ "extif0" ]; + secrets.host-secrets.nutboy3 = let + files = config.fudo.secrets.files; + in { + # heimdal-master-key = { + # source-file = files.realm-master-keys."FUDO.ORG"; + # target-file = "/run/heimdal/master-key"; + # user = config.fudo.auth.kdc.user; + # }; + + # ipropd-keytab = { + # source-file = files.service-keytabs.legatus.ipropd; + # target-file = "/run/heimdal/ipropd.keytab"; + # user = config.fudo.auth.kdc.user; + # }; + }; + + client.dns = { + ipv4 = true; + ipv6 = true; + user = "fudo-client"; + external-interface = "extif0"; + }; + + # auth.kdc = { + # enable = true; + # realm = "FUDO.ORG"; + # bind-addresses = [ host-ipv4 "127.0.0.1" ]; + # master-key-file = + # secrets.heimdal-master-key.target-file; + # state-directory = "/state/kerberos"; + # slave-config = { + # master-host = "france"; + # ipropd-keytab = secrets.ipropd-keytab.target-file; + # }; + # }; + + # secure-dns-proxy = { + # enable = true; + # upstream-dns = + # [ "https://1.1.1.1/dns-query" "https://1.0.0.1/dns-query" ]; + # bootstrap-dns = "1.1.1.1"; + # listen-ips = [ "127.0.0.1" ]; + # listen-port = 53; + # allowed-networks = [ "1.1.1.1/32" "1.0.0.1/32" "localhost" "link-local" ]; + # }; + + # dns.state-directory = "/state/nsd"; + + # mail-server = { + # enable = true; + # debug = true; + + # domain = domain-name; + # mail-hostname = "${host-fqdn}"; + # monitoring = false; + # mail-user = "mailuser"; + # mail-user-id = 525; + # mail-group = "mailgroup"; + # clamav.enable = true; + # dkim.signing = true; + + # dovecot = { + # ssl-certificate = acme-certificate "imap.${domain-name}"; + # ssl-private-key = acme-private-key "imap.${domain-name}"; + # }; + + # postfix = { + # ssl-certificate = acme-certificate "smtp.${domain-name}"; + # ssl-private-key = acme-private-key "smtp.${domain-name}"; + # }; + + # # This should NOT include the primary domain + # local-domains = [ host-fqdn "smtp.${domain-name}" ]; + + # mail-directory = "/srv/mailserver/mail"; + # state-directory = "/srv/mailserver/state"; + + # trusted-networks = [ "172.86.179.16/29" "127.0.0.0/16" ]; + + # alias-users = { + # root = [ "niten" ]; + # postmaster = [ "niten" ]; + # hostmaster = [ "niten" ]; + # webmaster = [ "niten" ]; + # system = [ "niten" ]; + # admin = [ "niten" ]; + # dmarc-report = [ "niten" ]; + # }; + # }; + + # postgresql = { + # enable = true; + # ssl-certificate = (acme-certificate host-fqdn); + # ssl-private-key = (acme-private-key host-fqdn); + # keytab = secrets.postgres-keytab.target-file; + # local-networks = local-networks; + + # users = { + # gituser = { + # password-file = + # secrets.gitea-database-password.target-file; + # databases = { + # git = { + # access = "CONNECT"; + # entity-access = { + # "ALL TABLES IN SCHEMA public" = "SELECT,INSERT,UPDATE,DELETE"; + # "ALL SEQUENCES IN SCHEMA public" = "SELECT, UPDATE"; + # }; + # }; + # }; + # }; + # }; + + # databases = { git = { users = [ "niten" ]; }; }; + # }; + + # git = { + # enable = true; + # hostname = "git.informis.land"; + # site-name = "informis git"; + # user = "gituser"; + # repository-dir = /srv/git/repo; + # state-dir = /srv/git/state; + # database = { + # user = "gituser"; + # password-file = + # secrets.gitea-database-password.target-file; + # hostname = "127.0.0.1"; + # name = "git"; + # }; + # ssh = { + # listen-ip = host-ipv4; + # listen-port = 2222; + # }; + # }; + }; +} diff --git a/config/host-config/procul.nix b/config/host-config/procul.nix index e1a7f76..7dec9f5 100644 --- a/config/host-config/procul.nix +++ b/config/host-config/procul.nix @@ -101,25 +101,22 @@ in { target-file = "/srv/gitea/secure/database.passwd"; user = config.fudo.git.user; }; + + heimdal-master-key = { + source-file = files.realm-master-keys."INFORMIS.LAND"; + target-file = "/run/heimdal/master-key"; + user = config.fudo.auth.kdc.user; + }; }; client.dns = { - enable = true; ipv4 = true; ipv6 = true; user = "fudo-client"; external-interface = "extif0"; }; - auth.kdc = { - enable = true; - realm = "INFORMIS.LAND"; - bind-addresses = [ host-ipv4 "127.0.0.1" ]; - acl = { - "niten" = { perms = [ "add" "change-password" "list" ]; }; - "*/root" = { perms = [ "all" ]; }; - }; - }; + auth.kdc.master-key-file = secrets.heimdal-master-key.target-file; secure-dns-proxy = { enable = true; @@ -131,34 +128,6 @@ in { allowed-networks = [ "1.1.1.1/32" "1.0.0.1/32" "localhost" "link-local" ]; }; - dns = { - enable = true; - identity = "procul.informis.land"; - nameservers = { - ns1 = { - ipv4-address = host-ipv4; - description = "Primary Informis Nameserver"; - }; - ns2 = { - ipv4-address = host-ipv4; - description = "Secondary Informis Nameserver"; - }; - }; - - listen-ips = [ host-ipv4 ]; - - domains = { - "informis.land" = { - dnssec = true; - default-host = host-ipv4; - gssapi-realm = "INFORMIS.LAND"; - mx = [ "smtp.informis.land" ]; - network-definition = config.fudo.networks."informis.land"; - dmarc-report-address = "dmarc-report@informis.land"; - }; - }; - }; - mail-server = { enable = true; debug = true; @@ -232,8 +201,8 @@ in { hostname = "git.informis.land"; site-name = "informis git"; user = "gituser"; - repository-dir = /srv/git/repo; - state-dir = /srv/git/state; + repository-dir = "/srv/git/repo"; + state-dir = "/srv/git/state"; database = { user = "gituser"; password-file = diff --git a/config/host-config/socrates.nix b/config/host-config/socrates.nix index a6893d2..d8a3428 100644 --- a/config/host-config/socrates.nix +++ b/config/host-config/socrates.nix @@ -49,11 +49,6 @@ in { system.stateVersion = "21.05"; - security.sudo.extraConfig = '' - # Due to tmpfs home, it'll always lecture otherwise - Defaults lecture = never - ''; - services = { openssh = { hostKeys = [ diff --git a/config/hosts/legatus.nix b/config/hosts/legatus.nix index e116500..5e1e277 100644 --- a/config/hosts/legatus.nix +++ b/config/hosts/legatus.nix @@ -1,5 +1,5 @@ { - description = "informis.land server."; + description = "eur.fudo.org server."; rp = "niten"; admin-email = "niten@fudo.org"; domain = "eur.fudo.org"; @@ -11,7 +11,7 @@ nixos-system = true; machine-id = "749bbf411088411b8784b76bb44bd617"; master-key = { - public-key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqUnzf8bfPyoJX6XjFqD6v5MZQnV8STP0152VS3uwM7"; + public-key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGsTkxsVViISxZYtqwNs6DEK2XgyBUPhqio4XPQbMKNo"; key-path = "/state/master-key/ed25519_key"; }; # initrd-network = { diff --git a/config/hosts/nutboy3.nix b/config/hosts/nutboy3.nix new file mode 100644 index 0000000..3b88440 --- /dev/null +++ b/config/hosts/nutboy3.nix @@ -0,0 +1,24 @@ +{ + description = "fudo.org server."; + rp = "reaper"; + admin-email = "reaper@fudo.org"; + domain = "fudo.org"; + site = "pioneer"; + profile = "server"; + enable-gui = false; + arch = "x86_64-linux"; + nixos-system = true; + machine-id = "d608fb62dc1e493a9a0ebf173ab255b2"; + master-key = { + public-key = FIXME; + key-path = "/state/master-key/ed25519_key"; + }; + # initrd-network = { + # ip = "172.86.179.18"; + # interface = "enp0s25"; + # keypair = { + # public-key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIgvl/pxPGN5XuUFsEywHV/PJMI+wPHA6NKTtE8SZC04"; + # private-key-file = "/state/ssh/initrd/ssh_ed25519_key"; + # }; + # }; +} diff --git a/config/networks/informis.land.nix b/config/networks/informis.land.nix index ab7f02e..9279649 100644 --- a/config/networks/informis.land.nix +++ b/config/networks/informis.land.nix @@ -10,10 +10,6 @@ srv-records = { tcp = { - domain = [{ - host = "ns1.informis.land"; - port = 53; - }]; ssh = [{ host = "procul.informis.land"; port = 22; @@ -22,14 +18,6 @@ host = "procul.informis.land"; port = 587; }]; - kerberos = [{ - host = "procul.informis.land"; - port = 88; - }]; - kerberos-adm = [{ - host = "procul.informis.land"; - port = 749; - }]; imaps = [{ host = "procul.informis.land"; port = 993; @@ -49,25 +37,6 @@ port = 443; }]; }; - - udp = { - domain = [{ - host = "ns1.informis.land"; - port = 53; - }]; - kerberos = [{ - host = "procul.informis.land"; - port = 88; - }]; - kerberos-master = [{ - host = "procul.informis.land"; - port = 88; - }]; - kpasswd = [{ - host = "procul.informis.land"; - port = 464; - }]; - }; }; hosts = { diff --git a/config/profile-config/common.nix b/config/profile-config/common.nix index 6b042f3..e198f0e 100644 --- a/config/profile-config/common.nix +++ b/config/profile-config/common.nix @@ -8,6 +8,7 @@ let cryptsetup git heimdal + mosh openssh_gssapi tldr vim diff --git a/config/profile-config/host/kerberos.nix b/config/profile-config/host/kerberos.nix index 634ddb6..2faa345 100644 --- a/config/profile-config/host/kerberos.nix +++ b/config/profile-config/host/kerberos.nix @@ -7,15 +7,21 @@ let try-attr = attr: set: if (hasAttr attr set) then set.${attr} else null; in { - config = mkIf has-secret-files { - fudo.secrets.host-secrets.${hostname} = let - keytab-file = try-attr hostname config.fudo.secrets.files.host-keytabs; - in mkIf (keytab-file != null) { - host-keytab = { - source-file = keytab-file; - target-file = "/etc/krb5.keytab"; - user = "root"; - }; + config = mkIf has-secret-files (let + keytab-file = try-attr hostname config.fudo.secrets.files.host-keytabs; + in { + environment.etc."krb5.keytab" = mkIf (keytab-file != null) { + source = + config.fudo.secrets.host-secrets.${hostname}.host-keytab.target-file; + user = "root"; + group = "root"; + mode = "0400"; }; - }; + + fudo.secrets.host-secrets.${hostname}.host-keytab = mkIf (keytab-file != null) { + source-file = keytab-file; + target-file = "/run/kerberos/krb5.keytab"; + user = "root"; + }; + }); } diff --git a/config/profile-config/host/ssh.nix b/config/profile-config/host/ssh.nix index 0163690..05f31a7 100644 --- a/config/profile-config/host/ssh.nix +++ b/config/profile-config/host/ssh.nix @@ -37,7 +37,7 @@ in { (map (keypair: nameValuePair "host-${keypair.key-type}-private-key" { source-file = keypair.private-key; - target-file = "/var/run/ssh/private/host-${keypair.key-type}-private-key"; + target-file = "/run/openssh/private/host-${keypair.key-type}-private-key"; user = "root"; }) host-keypairs); @@ -52,8 +52,11 @@ in { }) config.fudo.secrets.files.host-ssh-keypairs); }; - services.openssh.hostKeys = map (keypair: { - path = "/var/run/ssh/private/host-${keypair.key-type}-private-key"; + services.openssh.hostKeys = let + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + in map (keypair: { + path = + host-secrets."host-${keypair.key-type}-private-key".target-file; type = keypair.key-type; }) host-keypairs; }); diff --git a/config/site-config/nuttyclub.nix b/config/site-config/nuttyclub.nix new file mode 100644 index 0000000..865d469 --- /dev/null +++ b/config/site-config/nuttyclub.nix @@ -0,0 +1,5 @@ +{ config, lib, pkgs, ... }: + +{ + +} diff --git a/config/site-config/seattle.nix b/config/site-config/seattle.nix index 9d7b7fb..9172e21 100644 --- a/config/site-config/seattle.nix +++ b/config/site-config/seattle.nix @@ -38,21 +38,23 @@ in { options = [ "comment=systemd.automount" ]; }; + # "proto=tcp" + # NOTE: these are pointing directly to nostromo so the krb lookup works "/net/documents" = { device = "nostromo.sea.fudo.org:/export/documents"; fsType = "nfs4"; - options = [ "comment=systemd.automount" "sec=krb5p" "proto=tcp" ]; + options = [ "comment=systemd.automount" "sec=krb5p" ]; }; "/net/downloads" = { device = "nostromo.sea.fudo.org:/export/downloads"; fsType = "nfs4"; - options = [ "comment=systemd.automount" "sec=krb5i" "proto=tcp" ]; + options = [ "comment=systemd.automount" "sec=krb5i" ]; }; "/net/projects" = { device = "nostromo.sea.fudo.org:/export/projects"; fsType = "nfs4"; - options = [ "comment=systemd.automount" "sec=krb5p" "proto=tcp" ]; + options = [ "comment=systemd.automount" "sec=krb5p" ]; }; }; diff --git a/config/sites.nix b/config/sites.nix index 9423a8a..ab516c4 100644 --- a/config/sites.nix +++ b/config/sites.nix @@ -38,6 +38,18 @@ mail-server = "mail.fudo.org"; }; + nuttyclub = { + gateway-v4 = "199.87.154.174"; + network = "199.87.154.174/31"; + nameservers = [ "1.1.1.1" ]; + timezone = "America/Winnipeg"; + deploy-pubkeys = [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDPwh522lvafTJYA0X2uFdP7Ws+Um1f8gZsARK1Y5nMzf6ZcWBF1jplTOKUVSOl4isMWni0Tu0TnX4zqCcgocWUVbwIwXSIRYqdiCPvVOH+/Ibc97n1/dYxk5JPMtbrsEw6/gWZxVg0qwe0J3dQWldEMiDY7iWhlrmIr7YL+Y3PUd7DOwp3PbfWfNyzTfE1kXcz5YvTeN+txFhbbXT0oS2R2wtc1vYXFZ/KbNstjqd+i8jszAq3ZkbbwL3aNR0RO4n8+GoIILGw8Ya4eP7D6+mYk608IhAoxpGyMrUch2TC2uvOK3rd/rw1hsTxf4AKjAZbrfd/FJaYru9ZeoLjD4bRGMdVp56F1m7pLvRiWRK62pV2Q/fjx+4KjHUrgyPd601eUIP0ayS/Rfuq8ijLpBJgO5/Y/6mFus/kjZIfRR9dXfLM67IMpyEzEITYrc/R2sedWf+YHxSh6eguAZ/kLzioar1nHLR7Wzgeu0tgWkD78WQGjpXGoefAz3xHeBg3Et0=" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGVez4of30f+j0cWKj5kYCKeFjyNsYvG9UbOMxF5hImD2lP5MSbFBv31gFgHjx3yCG4zQRZlpuyU5uWo0qIwe9N84/LcZcB9WrWKZXDmuof7zPFy0J+Hj+LVLDQI/mVXHNwkMhBMHpPrdwA05EYDAYCYklWT4cSByu10pHtST+olF8i+A+UQgUzgNZzdJVeiYZv6MBDTYsJWptGeDUkl2B0Es3gtbGYcCCfnyS3RC7DIXlDo3NBbAr7WaHY2MBbT+R/+jicn9E3IY3NCM5jENxqmvHy9MDsxEEYgFNm7IDwq4V1VRUWy277YsvRbmEaHb+osOA5u1VNN4z3UftOZcSZgR5C/vR71cENXoPt1YQpCzu7i38ojtvL+tDVEKT7sIovrQw8q1sszNlW2nXh8RSPiIq5TMnrV73MP0egKcr9n3tfxwi1BIkLjvfom/02BkTK9R9v+VMNhYU1YwROhORCiMIgoxUGiUvtH8u38JGr7E0hhMoAjCE5k80WPUivl0=" + ]; + mail-server = "mail.fudo.org"; + }; + russell = { gateway-v4 = "10.0.0.1"; nameservers = [ "10.0.0.1" ]; @@ -61,7 +73,7 @@ }; worldstream = { - gateway-v4 = "91.229.23.204"; + gateway-v4 = "91.229.23.1"; network = "91.229.23.0/24"; nameservers = [ "1.1.1.1" "2606:4700:4700::1111" ]; timezone = "Europe/Amsterdam"; diff --git a/lib/default.nix b/lib/default.nix index c555faf..62d5a20 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -34,6 +34,7 @@ with lib; { ./fudo/netinfo-email.nix ./fudo/networks.nix ./fudo/node-exporter.nix + ./fudo/nsd.nix ./fudo/password.nix ./fudo/postgres.nix ./fudo/prometheus.nix diff --git a/lib/fudo/client/dns.nix b/lib/fudo/client/dns.nix index f692ebe..c81292e 100644 --- a/lib/fudo/client/dns.nix +++ b/lib/fudo/client/dns.nix @@ -11,8 +11,6 @@ let in { options.fudo.client.dns = { - enable = mkEnableOption "Enable Fudo DynDNS Client."; - ipv4 = mkOption { type = types.bool; default = true; @@ -71,7 +69,7 @@ in { }; }; - config = mkIf cfg.enable { + config = { users.users = { "${cfg.user}" = { diff --git a/lib/fudo/dns.nix b/lib/fudo/dns.nix index 39c8bb9..fcee95b 100644 --- a/lib/fudo/dns.nix +++ b/lib/fudo/dns.nix @@ -116,6 +116,12 @@ in { description = "A list of IPs on which to listen for DNS queries."; example = [ "1.2.3.4" ]; }; + + state-directory = mkOption { + type = str; + description = "Path at which to store nameserver state, including DNSSEC keys."; + default = "/var/lib/nsd"; + }; }; config = mkIf cfg.enable { @@ -124,48 +130,49 @@ in { allowedUDPPorts = [ 53 ]; }; - services.nsd = { + fudo.nsd = { enable = true; identity = cfg.identity; interfaces = cfg.listen-ips; + stateDir = cfg.state-directory; zones = mapAttrs' (dom: dom-cfg: let net-cfg = dom-cfg.network-definition; in nameValuePair "${dom}." { - dnssec = dom-cfg.dnssec; + dnssec = dom-cfg.dnssec; - data = '' - $ORIGIN ${dom}. - $TTL 12h + data = '' + $ORIGIN ${dom}. + $TTL 12h - @ IN SOA ns1.${dom}. hostmaster.${dom}. ( - ${toString config.instance.build-timestamp} - 30m - 2m - 3w - 5m) + @ IN SOA ns1.${dom}. hostmaster.${dom}. ( + ${toString config.instance.build-timestamp} + 30m + 2m + 3w + 5m) - ${optionalString (dom-cfg.default-host != null) + ${optionalString (dom-cfg.default-host != null) "@ IN A ${dom-cfg.default-host}"} - ${mxRecords dom-cfg.mx} + ${mxRecords dom-cfg.mx} - $TTL 6h + $TTL 6h - ${optionalString (dom-cfg.gssapi-realm != null) + ${optionalString (dom-cfg.gssapi-realm != null) ''_kerberos IN TXT "${dom-cfg.gssapi-realm}"''} - ${nsRecords dom cfg.nameservers} - ${join-lines (mapAttrsToList hostRecords cfg.nameservers)} + ${nsRecords dom cfg.nameservers} + ${join-lines (mapAttrsToList hostRecords cfg.nameservers)} - ${dmarcRecord dom-cfg.dmarc-report-address} + ${dmarcRecord dom-cfg.dmarc-report-address} - ${join-lines + ${join-lines (mapAttrsToList makeSrvProtocolRecords net-cfg.srv-records)} - ${join-lines (mapAttrsToList hostRecords net-cfg.hosts)} - ${join-lines (mapAttrsToList cnameRecord net-cfg.aliases)} - ${join-lines net-cfg.verbatim-dns-records} - ''; - }) cfg.domains; + ${join-lines (mapAttrsToList hostRecords net-cfg.hosts)} + ${join-lines (mapAttrsToList cnameRecord net-cfg.aliases)} + ${join-lines net-cfg.verbatim-dns-records} + ''; + }) cfg.domains; }; }; } diff --git a/lib/fudo/domain/dns.nix b/lib/fudo/domain/dns.nix new file mode 100644 index 0000000..bf84435 --- /dev/null +++ b/lib/fudo/domain/dns.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + domain = config.instance.local-domain; + cfg = config.fudo.domains.${domain}; + + served-domain = cfg.primary-nameserver != null; + + is-primary = hostname == cfg.primary-nameserver; + + create-srv-record = port: hostname: { + port = port; + host = hostname; + }; + +in { + config = { + fudo.dns = mkIf is-primary (let + primary-ip = pkgs.lib.fudo.network.host-ipv4 config hostname; + all-ips = pkgs.lib.fudo.network.host-ips config hostname; + in { + enable = true; + identity = "${hostname}.${domain}"; + nameservers = { + ns1 = { + ipv4-address = primary-ip; + description = "Primary ${domain} nameserver"; + }; + }; + + # Deliberately leaving out localhost so the primary nameserver + # can use a custom recursor + listen-ips = all-ips; + + domains = { + ${domain} = { + dnssec = true; + default-host = primary-ip; + gssapi-realm = cfg.gssapi-realm; + mx = optional (cfg.primary-mailserver != null) + cfg.primary-mailserver; + # TODO: there's no guarantee this exists... + dmarc-report-address = "dmarc-report@${domain}"; + + network-definition = let + network = config.fudo.networks.${domain}; + in network // { + srv-records = { + tcp = { + domain = [{ + host = "ns1.${domain}"; + port = 53; + }]; + }; + udp = { + domain = [{ + host = "ns1.${domain}"; + port = 53; + }]; + }; + }; + }; + }; + }; + }); + }; +} diff --git a/lib/fudo/domain/kerberos.nix b/lib/fudo/domain/kerberos.nix new file mode 100644 index 0000000..f104c0a --- /dev/null +++ b/lib/fudo/domain/kerberos.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + domain = config.instance.local-domain; + cfg = config.fudo.domains.${domain}; + +in { + config = let + hostname = config.instance.hostname; + is-master = hostname == cfg.kerberos-master; + is-slave = elem hostname cfg.kerberos-slaves; + + kerberized-domain = cfg.kerberos-master != null; + + in { + fudo = { + auth.kdc = mkIf (is-master || is-slave) { + enable = true; + realm = cfg.gssapi-realm; + # TODO: Also bind to ::1? + bind-addresses = + (pkgs.lib.fudo.network.host-ips config hostname) ++ + [ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"); + master-config = mkIf is-master { + acl = let + admin-entries = genAttrs cfg.local-admins + (admin: { + perms = [ "add" "change-password" "list" ]; + }); + in admin-entries // { + "*/root" = { perms = [ "all" ]; }; + }; + }; + slave-config = mkIf is-slave { + master-host = cfg.kerberos-master; + # You gotta provide the keytab yourself, sorry... + }; + }; + + dns.domains.${domain} = { + network-definition = mkIf kerberized-domain { + srv-records = let + get-fqdn = hostname: + "${hostname}.${config.fudo.hosts.${hostname}.domain}"; + + create-srv-record = port: hostname: { + port = port; + host = hostname; + }; + + all-servers = map get-fqdn + ([cfg.kerberos-master] ++ cfg.kerberos-slaves); + + master-servers = + map get-fqdn [cfg.kerberos-master]; + + in { + tcp = { + kerberos = map (create-srv-record 88) all-servers; + kerberos-adm = map (create-srv-record 749) master-servers; + }; + udp = { + kerberos = map (create-srv-record 88) all-servers; + kerberos-master = map (create-srv-record 88) master-servers; + kpasswd = map (create-srv-record 464) master-servers; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/fudo/domains.nix b/lib/fudo/domains.nix index 5575f53..5b6202b 100644 --- a/lib/fudo/domains.nix +++ b/lib/fudo/domains.nix @@ -2,50 +2,80 @@ with lib; let - domainOpts = { domain, ... }: { - options = { + hostname = config.instance.hostname; + domain = config.instance.local-domain; + + domainOpts = { name, ... }: let + domain = name; + in { + options = with types; { domain = mkOption { - type = types.str; + type = str; description = "Domain name."; default = domain; }; local-networks = mkOption { - type = with types; listOf str; + type = listOf str; description = "A list of networks to be considered trusted on this network."; default = [ ]; }; local-users = mkOption { - type = with types; listOf str; + type = listOf str; description = "A list of users who should have local (i.e. login) access to _all_ hosts in this domain."; default = [ ]; }; local-admins = mkOption { - type = with types; listOf str; + type = listOf str; description = "A list of users who should have admin access to _all_ hosts in this domain."; default = [ ]; }; local-groups = mkOption { - type = with types; listOf str; + type = listOf str; description = "List of groups which should exist within this domain."; default = [ ]; }; admin-email = mkOption { - type = types.str; + type = str; description = "Email for the administrator of this domain."; - default = "admin@fudo.org"; + default = "admin@${domain}"; }; gssapi-realm = mkOption { - type = with types; nullOr str; + type = str; description = "GSSAPI (i.e. Kerberos) realm of this domain."; + default = toUpper domain; + }; + + kerberos-master = mkOption { + type = nullOr str; + description = "Hostname of the Kerberos master server for the domain, if applicable."; + default = null; + }; + + kerberos-slaves = mkOption { + type = listOf str; + description = "List of hosts acting as Kerberos slaves for the domain."; + default = []; + }; + + primary-nameserver = mkOption { + type = nullOr str; + description = "Hostname of the primary nameserver for this domain."; + default = null; + }; + + primary-mailserver = mkOption { + type = nullOr str; + description = "Hostname of the primary mail server for this domain."; + default = null; }; }; }; @@ -56,4 +86,9 @@ in { description = "Domain configurations for all domains known to the system."; default = { }; }; + + imports = [ + ./domain/kerberos.nix + ./domain/dns.nix + ]; } diff --git a/lib/fudo/git.nix b/lib/fudo/git.nix index 7d904c4..4988645 100644 --- a/lib/fudo/git.nix +++ b/lib/fudo/git.nix @@ -64,15 +64,15 @@ in { }; repository-dir = mkOption { - type = path; + type = str; description = "Path at which to store repositories."; - example = /srv/git/repo; + example = "/srv/git/repo"; }; state-dir = mkOption { - type = path; + type = str; description = "Path at which to store server state."; - example = /srv/git/state; + example = "/srv/git/state"; }; user = mkOption { @@ -103,6 +103,15 @@ in { networking.firewall.allowedTCPPorts = mkIf (cfg.ssh != null) [ cfg.ssh.listen-port ]; + environment.systemPackages = with pkgs; let + gitea-admin = writeShellScriptBin "gitea-admin" '' + TMP=$(mktemp -d /tmp/gitea-XXXXXXXX) + ${gitea}/bin/gitea --custom-path ${cfg.state-dir}/custom --config ${cfg.state-dir}/custom/conf/app.ini --work-path $TMP $@ + ''; + in [ + gitea-admin + ]; + services = { gitea = { enable = true; @@ -118,8 +127,8 @@ in { domain = cfg.hostname; httpAddress = "127.0.0.1"; httpPort = cfg.local-port; - repositoryRoot = toString cfg.repository-dir; - stateDir = toString cfg.state-dir; + repositoryRoot = cfg.repository-dir; + stateDir = cfg.state-dir; rootUrl = "https://${cfg.hostname}/"; user = mkIf (cfg.user != null) cfg.user; ssh = { diff --git a/lib/fudo/hosts.nix b/lib/fudo/hosts.nix index 2c3a018..3c169f8 100644 --- a/lib/fudo/hosts.nix +++ b/lib/fudo/hosts.nix @@ -36,6 +36,11 @@ in { has-build-keys = (length host-cfg.build-pubkeys) > 0; in { + security.sudo.extraConfig = '' + # I get it, I get it + Defaults lecture = never + ''; + networking = { hostName = config.instance.hostname; domain = domain-name; @@ -65,11 +70,11 @@ in { config.fudo.system.hostfile-entries; in mkForce { text = '' - 127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost - 127.0.0.2 ${hostname} localhost - ::1 ${hostname}.${domain-name} ${hostname} localhost - ${concatStringsSep "\n" host-entries} - ''; + 127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost + 127.0.0.2 ${hostname} localhost + ::1 ${hostname}.${domain-name} ${hostname} localhost + ${concatStringsSep "\n" host-entries} + ''; user = "root"; group = "root"; mode = "0444"; diff --git a/lib/fudo/kdc.nix b/lib/fudo/kdc.nix index 6b24130..a195093 100644 --- a/lib/fudo/kdc.nix +++ b/lib/fudo/kdc.nix @@ -1,59 +1,125 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, ... } @ toplevel: 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; + hostname = config.instance.hostname; - get-domain-hosts = domain: - attrNames - (filterAttrs (host: hostOpts: hostOpts.domain == domain) config.fudo.hosts); + state-directory = toplevel.config.fudo.auth.kdc.state-directory; - get-host-principals = realm: hostname: - let host = config.fudo.hosts.${hostname}; - in map (service: "${service}/${hostname}.${toLower realm}@${realm}") - host.kerberos-services; + database-file = "${state-directory}/principals.db"; + iprop-log = "${state-directory}/iprop.log"; - add-principal-str = kdc-conf: principal: - "${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} -- add --random-key --use-defaults ${principal}"; + master-server = cfg.master-config != null; + slave-server = cfg.slave-config != null; - add-hosts-principals = realm: kdc-conf: - concatStringsSep "\n" (map (add-principal-str kdc-conf) - (concatMap (get-host-principals realm) - (get-domain-hosts (toLower realm)))); + get-fqdn = hostname: + "${hostname}.${config.fudo.hosts.${hostname}.domain}"; + + kdc-conf = generate-kdc-conf { + realm = cfg.realm; + db-file = database-file; + key-file = cfg.master-key-file; + acl-data = if master-server then cfg.master-config.acl else null; + }; 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 - ''; + { realm, user, group, kdc-conf, key-file, db-name, max-lifetime, max-renewal, + primary-keytab, kadmin-keytab, kpasswd-keytab, ipropd-keytab, local-hostname }: let - generate-kdc-conf = realm: db-file: key-file: acl-file: + kadmin-cmd = "kadmin -l -c ${kdc-conf} --"; + + get-domain-hosts = domain: let + host-in-subdomain = host: hostOpts: + (builtins.match "(.+[.])?${domain}$" hostOpts.domain) != null; + in attrNames (filterAttrs host-in-subdomain config.fudo.hosts); + + get-host-principals = realm: hostname: let + host = config.fudo.hosts.${hostname}; + in map (service: "${service}/${hostname}.${host.domain}@${realm}") + host.kerberos-services; + + add-principal-str = principal: + "${kadmin-cmd} add --random-key --use-defaults ${principal}"; + + test-existence = principal: + "[[ $( ${kadmin-cmd} get ${principal} ) ]]"; + + exists-or-add = principal: '' + if ${test-existence principal}; then + echo "skipping ${principal}, already exists" + else + ${add-principal-str principal} + fi + ''; + + ensure-host-principals = realm: + concatStringsSep "\n" + (map exists-or-add + (concatMap (get-host-principals realm) + (get-domain-hosts (toLower realm)))); + + slave-hostnames = map get-fqdn cfg.master-config.slave-hosts; + + ensure-iprop-principals = concatStringsSep "\n" + (map (host: exists-or-add "iprop/${host}@${realm}") + [ local-hostname ] ++ slave-hostnames); + + copy-slave-principals-file = let + slave-principals = map + (host: "iprop/${hostname}@${cfg.realm}") + slave-hostnames; + slave-principals-file = pkgs.writeText "heimdal-slave-principals" + (concatStringsSep "\n" slave-principals); + in optionalString (slave-principals-file != null) '' + cp ${slave-principals-file} ${state-directory}/slaves + # Since it's copied from /nix/store, this is by default read-only, + # which causes updates to fail. + chmod u+w ${state-directory}/slaves + ''; + + in pkgs.writeShellScript "initialize-kdc-db.sh" '' + TMP=$(mktemp -d -t kdc-XXXXXXXX) + if [ ! -e ${database-file} ]; then + ## CHANGING HOW THIS WORKS + ## Now we expect the key to be provided + # kstash --key-file=${key-file} --random-key + ${kadmin-cmd} init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" ${realm} + fi + + ${ensure-host-principals realm} + + ${ensure-iprop-principals} + + echo "*** BEGIN EXTRACTING KEYTABS" + echo "*** You can probably ignore the 'principal does not exist' errors that follow," + echo "*** they're just testing for principal existence before creating those that" + echo "*** don't already exist" + + ${kadmin-cmd} ext_keytab --keytab=$TMP/primary.keytab */${local-hostname}@${realm} + mv $TMP/primary.keytab ${primary-keytab} + ${kadmin-cmd} ext_keytab --keytab=$TMP/kadmin.keytab kadmin/admin@${realm} + mv $TMP/kadmin.keytab ${kadmin-keytab} + ${kadmin-cmd} ext_keytab --keytab=$TMP/kpasswd.keytab kadmin/changepw@${realm} + mv $TMP/kpasswd.keytab ${kpasswd-keytab} + ${kadmin-cmd} ext_keytab --keytab=$TMP/ipropd.keytab iprop/${local-hostname}@${realm} + mv $TMP/ipropd.keytab ${ipropd-keytab} + + echo "*** END EXTRACTING KEYTABS" + + ${copy-slave-principals-file} + ''; + + generate-kdc-conf = { realm, db-file, key-file, acl-data }: pkgs.writeText "kdc.conf" '' [kdc] database = { dbname = sqlite:${db-file} realm = ${realm} mkey_file = ${key-file} - acl_file = ${acl-file} + ${optionalString (acl-data != null) + "acl_file = ${generate-acl-file acl-data}"} log_file = ${iprop-log} } @@ -63,8 +129,8 @@ let } [logging] - kdc = FILE:${cfg.state-directory}/kerberos.log - default = FILE:${cfg.state-directory}/kerberos.log + kdc = FILE:${state-directory}/kerberos.log + default = FILE:${state-directory}/kerberos.log ''; aclEntry = { principal, ... }: { @@ -95,25 +161,84 @@ let }; }; - perms-to-permstring = perms: concatStringsSep "," perms; - - generate-acl-file = acl-entries: + generate-acl-file = acl-entries: let + perms-to-permstring = perms: concatStringsSep "," perms; + in pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList (principal: opts: "${principal} ${perms-to-permstring opts.perms}${ - optionalString (opts.target != null) " ${opts.target}" - }") acl-entries)); + optionalString (opts.target != null) " ${opts.target}" }") + acl-entries)); - kadmin-local = kdc-conf: kadmin-keytab: + kadmin-local = kdc-conf: pkgs.writeShellScriptBin "kadmin.local" '' - ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} --keytab=${kadmin-keytab} + ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} $@ ''; + masterOpts = { ... }: { + options = with types; { + 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" ]; + }; + }; + + kadmin-keytab = mkOption { + type = str; + description = "Location at which to store keytab for kadmind."; + default = "${state-directory}/kadmind.keytab"; + }; + + kpasswdd-keytab = mkOption { + type = str; + description = "Location at which to store keytab for kpasswdd."; + default = "${state-directory}/kpasswdd.keytab"; + }; + + ipropd-keytab = mkOption { + type = str; + description = "Location at which to store keytab for ipropd master."; + default = "${state-directory}/ipropd.keytab"; + }; + + slave-hosts = mkOption { + type = listOf str; + description = '' + A list of host to which the database should be propagated. + + Must exist in the Fudo Host database. + ''; + default = [ ]; + }; + }; + }; + + slaveOpts = { ... }: { + options = with types; { + master-host = mkOption { + type = str; + description = '' + Host from which to recieve database updates. + + Must exist in the Fudo Host database. + ''; + }; + + ipropd-keytab = mkOption { + type = str; + description = "Location at which to find keytab for ipropd slave."; + default = "${state-directory}/ipropd.keytab"; + }; + }; + }; + in { - options.fudo.auth.kdc = with types; let - default-state-dir = "/var/kerberos"; - in { + options.fudo.auth.kdc = with types; { enable = mkEnableOption "Fudo KDC"; realm = mkOption { @@ -121,16 +246,6 @@ in { 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."; @@ -152,38 +267,34 @@ in { state-directory = mkOption { type = str; description = "Path at which to store kerberos database."; - default = default-state-dir; + default = "/var/lib/kerberos"; }; master-key-file = mkOption { type = str; - description = "File containing the master key for the realm."; - default = "${default-state-dir}/master.key"; + description = '' + File containing the master key for the realm. + + Must be provided! + ''; }; primary-keytab = mkOption { type = str; - description = "Location of keytab for kadmind."; - default = "${default-state-dir}/host.keytab"; + description = "Location of host master keytab."; + default = "${state-directory}/host.keytab"; }; - kadmin-keytab = mkOption { - type = str; - description = "Location of keytab for kadmind."; - default = "${default-state-dir}/kadmind.keytab"; + master-config = mkOption { + type = nullOr (submodule masterOpts); + description = "Configuration for the master KDC server."; + default = null; }; - kpasswdd-keytab = mkOption { - type = str; - description = "Location of keytab for kpasswdd."; - default = "${default-state-dir}/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; + slave-config = mkOption { + type = nullOr (submodule slaveOpts); + description = "Configuration for slave KDC servers."; + default = null; }; max-ticket-lifetime = mkOption { @@ -200,10 +311,24 @@ in { }; config = mkIf cfg.enable { + + assertions = [ + { + assertion = master-server || slave-server; + message = + "For the KDC to be enabled, a master OR slave config must be provided."; + } + { + assertion = !(master-server && slave-server); + message = + "Only one of master-config and slave-config may be provided."; + } + ]; + users = { users.${cfg.user} = { isSystemUser = true; - home = cfg.state-directory; + home = state-directory; group = cfg.group; }; @@ -217,81 +342,166 @@ in { ticket_lifetime = cfg.max-ticket-lifetime; renew_lifetime = cfg.max-ticket-renewal; }; - realms = { ${cfg.realm} = { enable-http = false; }; }; + # Sorry, port 80 isn't available! + realms.${cfg.realm}.enable-http = false; extraConfig = '' - default = FILE:${cfg.state-directory}/kerberos.log + default = FILE:${state-directory}/kerberos.log ''; }; environment = { - systemPackages = - [ pkgs.heimdalFull (kadmin-local kdc-conf cfg.kadmin-keytab) ]; + systemPackages = [ pkgs.heimdalFull (kadmin-local kdc-conf) ]; - etc = { - "krb5.keytab" = { - user = "root"; - group = "root"; - mode = "0400"; - source = cfg.primary-keytab; - }; - }; + ## This shouldn't be necessary...every host gets a krb5.keytab + # etc = { + # "krb5.keytab" = { + # user = "root"; + # group = "root"; + # mode = "0400"; + # source = cfg.primary-keytab; + # }; + # }; }; fudo.system = { ensure-directories = { - "${cfg.state-directory}" = { + "${state-directory}" = { user = cfg.user; group = cfg.group; perms = "0740"; }; }; - services = { + services = if master-server then { + heimdal-kdc = let listen-addrs = concatStringsSep " " (map (addr: "--addresses=${addr}") cfg.bind-addresses); + in { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = + "Heimdal Kerberos Key Distribution Center (ticket server)."; + execStart = "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; + user = cfg.user; + group = cfg.group; + workingDirectory = state-directory; + privateNetwork = false; + addressFamilies = [ "AF_INET" "AF_INET6" ]; + requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; + }; + + heimdal-kadmin = { + wantedBy = [ "multi-user.target" ]; + requires = [ "heimdal-kdc.service" ]; + description = + "Heimdal Kerberos Remote Administration Server."; + # Doesn't have any way to specify IPs to listen on...sigh + execStart = "${pkgs.heimdalFull}/libexec/heimdal/kadmin -c ${kdc-conf} -k ${cfg.master-key-file} -r ${cfg.realm} --ports=749"; + user = cfg.user; + group = cfg.group; + workingDirectory = state-directory; + privateNetwork = false; + addressFamilies = [ "AF_INET" "AF_INET6" ]; + requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; + }; + + heimdal-kdc-init = let + init-cmd = initialize-db { + realm = cfg.realm; + user = cfg.user; + group = cfg.group; + kdc-conf = kdc-conf; + key-file = cfg.master-key-file; + db-name = database-file; + max-lifetime = cfg.max-ticket-lifetime; + max-renewal = cfg.max-ticket-renewal; + primary-keytab = cfg.primary-keytab; + kadmin-keytab = cfg.master-config.kadmin-keytab; + kpasswd-keytab = cfg.master-config.kpasswdd-keytab; + ipropd-keytab = cfg.master-config.ipropd-keytab; + local-hostname = + "${config.instance.hostname}.${config.instance.local-domain}"; + }; + in { + requires = [ "heimdal-kdc.service" ]; + wantedBy = [ "multi-user.target" ]; + description = "Initialization script for Heimdal KDC."; + type = "oneshot"; + execStart = "${init-cmd}"; + user = cfg.user; + group = cfg.group; + path = with pkgs; [ heimdalFull ]; + protectSystem = "full"; + addressFamilies = [ "AF_INET" "AF_INET6" ]; + workingDirectory = state-directory; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; + }; + + heimdal-ipropd-master = mkIf (length cfg.master-config.slave-hosts > 0) { + requires = [ "heimdal-kdc.service" ]; + wantedBy = [ "multi-user.target" ]; + description = "Propagate changes to the master KDC DB to all slaves."; + path = with pkgs; [ heimdalFull ]; + execStart = "${pkgs.heimdalFull}/libexec/heimdal/ipropd-master -c ${kdc-conf} -k ${cfg.master.ipropd-keytab}"; + user = cfg.user; + group = cfg.group; + workingDirectory = state-directory; + privateNetwork = false; + addressFamilies = [ "AF_INET" "AF_INET6" ]; + environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; + }; + + } else { + + heimdal-kdc-slave = 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)."; + "Heimdal Slave Kerberos Key Distribution Center (ticket server)."; execStart = command; user = cfg.user; group = cfg.group; - workingDirectory = cfg.state-directory; + workingDirectory = 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}"}"; + heimdal-ipropd-slave = { + #wantedBy = [ "multi-user.target" ]; + description = "Receive changes propagated from the KDC master server."; + path = with pkgs; [ heimdalFull ]; + execStart = concatStringsSep " " [ + "${pkgs.heimdalFull}/libexec/heimdal/ipropd-slave" + "--config-file=${kdc-conf}" + "--keytab=${cfg.slave-config.ipropd-keytab}" + "--realm=${cfg.realm}" + "--hostname=${get-fqdn hostname}" + "--port=2121" + "--verbose" + (get-fqdn cfg.slave-config.master-host) + ]; user = cfg.user; group = cfg.group; - protectSystem = "full"; + workingDirectory = state-directory; + privateNetwork = false; addressFamilies = [ "AF_INET" "AF_INET6" ]; - workingDirectory = cfg.state-directory; + requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 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 = { + services.xinetd = mkIf master-server { enable = true; services = [ @@ -301,7 +511,7 @@ in { server = "${pkgs.heimdalFull}/libexec/heimdal/kadmind"; protocol = "tcp"; serverArgs = - "--config-file=${kdc-conf} --keytab=${cfg.kadmin-keytab}"; + "--config-file=${kdc-conf} --keytab=${cfg.master-config.kadmin-keytab}"; } { name = "kpasswd"; @@ -309,14 +519,20 @@ in { server = "${pkgs.heimdalFull}/libexec/heimdal/kpasswdd"; protocol = "udp"; serverArgs = - "--config-file=${kdc-conf} --keytab=${cfg.kpasswdd-keytab}"; + "--config-file=${kdc-conf} --keytab=${cfg.master-config.kpasswdd-keytab}"; } ]; }; - networking.firewall = { - allowedTCPPorts = [ 88 749 ]; - allowedUDPPorts = [ 88 464 ]; + networking = { + firewall = { + allowedTCPPorts = [ 88 ] ++ + (optionals master-server [ 749 ]) ++ + (optionals slave-server [ 2121 ]); + allowedUDPPorts = [ 88 ] ++ + (optionals master-server [ 464 ]) ++ + (optionals slave-server [ 2121 ]); + }; }; }; } diff --git a/lib/fudo/mail.nix b/lib/fudo/mail.nix index 70410cd..e31d948 100644 --- a/lib/fudo/mail.nix +++ b/lib/fudo/mail.nix @@ -198,7 +198,8 @@ in { config = mkIf cfg.enable { systemd.tmpfiles.rules = [ - "d ${cfg.mail-directory} 770 ${cfg.mail-user} ${cfg.mail-group} - -" + "d ${cfg.mail-directory} 775 ${cfg.mail-user} ${cfg.mail-group} - -" + "d ${cfg.state-directory} 775 root ${cfg.mail-group} - -" ]; networking.firewall = { diff --git a/lib/fudo/nsd.nix b/lib/fudo/nsd.nix new file mode 100644 index 0000000..bb200e4 --- /dev/null +++ b/lib/fudo/nsd.nix @@ -0,0 +1,978 @@ +### NOTE: +## This is a copy of the upstream version, which allows for overriding the state directory + +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.fudo.nsd; + + username = "nsd"; + stateDir = cfg.stateDir; + pidFile = stateDir + "/var/nsd.pid"; + + # build nsd with the options needed for the given config + nsdPkg = pkgs.nsd.override { + bind8Stats = cfg.bind8Stats; + ipv6 = cfg.ipv6; + ratelimit = cfg.ratelimit.enable; + rootServer = cfg.rootServer; + zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0; + }; + + mkZoneFileName = name: if name == "." then "root" else name; + + # replaces include: directives for keys with fake keys for nsd-checkconf + injectFakeKeys = keys: concatStrings + (mapAttrsToList + (keyName: keyOptions: '' + fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")" + sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf + '') + keys); + + nsdEnv = pkgs.buildEnv { + name = "nsd-env"; + + paths = [ configFile ] + ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs; + + postBuild = '' + echo "checking zone files" + cd $out/zones + for zoneFile in *; do + echo "|- checking zone '$out/zones/$zoneFile'" + ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || { + if grep -q \\\\\\$ "$zoneFile"; then + echo zone "$zoneFile" contains escaped dollar signs \\\$ + echo Escaping them is not needed any more. Please make sure \ + to unescape them where they prefix a variable name. + fi + exit 1 + } + done + echo "checking configuration file" + # Save original config file including key references... + cp $out/nsd.conf{,.orig} + # ...inject mock keys into config + ${injectFakeKeys cfg.keys} + # ...do the checkconf + ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf + # ... and restore original config file. + mv $out/nsd.conf{.orig,} + ''; + }; + + writeZoneData = name: text: pkgs.writeTextFile { + name = "nsd-zone-${mkZoneFileName name}"; + inherit text; + destination = "/zones/${mkZoneFileName name}"; + }; + + + # options are ordered alphanumerically by the nixos option name + configFile = pkgs.writeTextDir "nsd.conf" '' + server: + chroot: "${stateDir}" + username: ${username} + # The directory for zonefile: files. The daemon chdirs here. + zonesdir: "${stateDir}" + # the list of dynamically added zones. + database: "${stateDir}/var/nsd.db" + pidfile: "${pidFile}" + xfrdfile: "${stateDir}/var/xfrd.state" + xfrdir: "${stateDir}/tmp" + zonelistfile: "${stateDir}/var/zone.list" + # interfaces + ${forEach " ip-address: " cfg.interfaces} + ip-freebind: ${yesOrNo cfg.ipFreebind} + hide-version: ${yesOrNo cfg.hideVersion} + identity: "${cfg.identity}" + ip-transparent: ${yesOrNo cfg.ipTransparent} + do-ip4: ${yesOrNo cfg.ipv4} + ipv4-edns-size: ${toString cfg.ipv4EDNSSize} + do-ip6: ${yesOrNo cfg.ipv6} + ipv6-edns-size: ${toString cfg.ipv6EDNSSize} + log-time-ascii: ${yesOrNo cfg.logTimeAscii} + ${maybeString "nsid: " cfg.nsid} + port: ${toString cfg.port} + reuseport: ${yesOrNo cfg.reuseport} + round-robin: ${yesOrNo cfg.roundRobin} + server-count: ${toString cfg.serverCount} + ${maybeToString "statistics: " cfg.statistics} + tcp-count: ${toString cfg.tcpCount} + tcp-query-count: ${toString cfg.tcpQueryCount} + tcp-timeout: ${toString cfg.tcpTimeout} + verbosity: ${toString cfg.verbosity} + ${maybeString "version: " cfg.version} + xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout} + zonefiles-check: ${yesOrNo cfg.zonefilesCheck} + ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength} + ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength} + rrl-ratelimit: ${toString cfg.ratelimit.ratelimit} + ${maybeString "rrl-slip: " cfg.ratelimit.slip} + rrl-size: ${toString cfg.ratelimit.size} + rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit} + ${keyConfigFile} + remote-control: + control-enable: ${yesOrNo cfg.remoteControl.enable} + control-key-file: "${cfg.remoteControl.controlKeyFile}" + control-cert-file: "${cfg.remoteControl.controlCertFile}" + ${forEach " control-interface: " cfg.remoteControl.interfaces} + control-port: ${toString cfg.remoteControl.port} + server-key-file: "${cfg.remoteControl.serverKeyFile}" + server-cert-file: "${cfg.remoteControl.serverCertFile}" + ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)} + ${cfg.extraConfig} + ''; + + yesOrNo = b: if b then "yes" else "no"; + maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"''; + maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}''; + forEach = pre: l: concatMapStrings (x: pre + x + "\n") l; + + + keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: '' + key: + name: "${keyName}" + algorithm: "${keyOptions.algorithm}" + include: "${stateDir}/private/${keyName}" + '') cfg.keys); + + copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: '' + secret=$(cat "${keyOptions.keyFile}") + dest="${stateDir}/private/${keyName}" + echo " secret: \"$secret\"" > "$dest" + chown ${username}:${username} "$dest" + chmod 0400 "$dest" + '') cfg.keys); + + + # options are ordered alphanumerically by the nixos option name + zoneConfigFile = name: zone: '' + zone: + name: "${name}" + zonefile: "${stateDir}/zones/${mkZoneFileName name}" + ${maybeString "outgoing-interface: " zone.outgoingInterface} + ${forEach " rrl-whitelist: " zone.rrlWhitelist} + ${maybeString "zonestats: " zone.zoneStats} + ${maybeToString "max-refresh-time: " zone.maxRefreshSecs} + ${maybeToString "min-refresh-time: " zone.minRefreshSecs} + ${maybeToString "max-retry-time: " zone.maxRetrySecs} + ${maybeToString "min-retry-time: " zone.minRetrySecs} + allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback} + ${forEach " allow-notify: " zone.allowNotify} + ${forEach " request-xfr: " zone.requestXFR} + ${forEach " notify: " zone.notify} + notify-retry: ${toString zone.notifyRetry} + ${forEach " provide-xfr: " zone.provideXFR} + ''; + + zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; }; + + zoneConfigs' = parent: name: zone: + if !(zone ? children) || zone.children == null || zone.children == { } + # leaf -> actual zone + then listToAttrs [ (nameValuePair name (parent // zone)) ] + + # fork -> pattern + else zipAttrsWith (name: head) ( + mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child) + zone.children + ); + + # fighting infinite recursion + zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true; + zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false; + zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false; + zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false; + zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false; + zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false; + zoneOptions6 = zoneOptionsRaw // childConfig null false; + + childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; }; + + # options are ordered alphanumerically + zoneOptionsRaw = types.submodule { + options = { + + allowAXFRFallback = mkOption { + type = types.bool; + default = true; + description = '' + If NSD as secondary server should be allowed to AXFR if the primary + server does not allow IXFR. + ''; + }; + + allowNotify = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name" + "10.0.3.4&255.255.0.0 BLOCKED" + ]; + description = '' + Listed primary servers are allowed to notify this secondary server. + + either a plain IPv4/IPv6 address or range. Valid patters for ranges: + * 10.0.0.0/24 # via subnet size + * 10.0.0.0&255.255.255.0 # via subnet mask + * 10.0.0.1-10.0.0.254 # via range + A optional port number could be added with a '@': + * 2001:1234::1@1234 + + * will use the specified TSIG key + * NOKEY no TSIG signature is required + * BLOCKED notifies from non-listed or blocked IPs will be ignored + * ]]> + ''; + }; + + children = mkOption { + default = {}; + description = '' + Children zones inherit all options of their parents. Attributes + defined in a child will overwrite the ones of its parent. Only + leaf zones will be actually served. This way it's possible to + define maybe zones which share most attributes without + duplicating everything. This mechanism replaces nsd's patterns + in a save and functional way. + ''; + }; + + data = mkOption { + type = types.lines; + default = ""; + example = ""; + description = '' + The actual zone data. This is the content of your zone file. + Use imports or pkgs.lib.readFile if you don't want this data in your config file. + ''; + }; + + dnssec = mkEnableOption "DNSSEC"; + + dnssecPolicy = { + algorithm = mkOption { + type = types.str; + default = "RSASHA256"; + description = "Which algorithm to use for DNSSEC"; + }; + keyttl = mkOption { + type = types.str; + default = "1h"; + description = "TTL for dnssec records"; + }; + coverage = mkOption { + type = types.str; + default = "1y"; + description = '' + The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time. + ''; + }; + zsk = mkOption { + type = keyPolicy; + default = { keySize = 2048; + prePublish = "1w"; + postPublish = "1w"; + rollPeriod = "1mo"; + }; + description = "Key policy for zone signing keys"; + }; + ksk = mkOption { + type = keyPolicy; + default = { keySize = 4096; + prePublish = "1mo"; + postPublish = "1mo"; + rollPeriod = "0"; + }; + description = "Key policy for key signing keys"; + }; + }; + + maxRefreshSecs = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Limit refresh time for secondary zones. This is the timer which + checks to see if the zone has to be refetched when it expires. + Normally the value from the SOA record is used, but this option + restricts that value. + ''; + }; + + minRefreshSecs = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Limit refresh time for secondary zones. + ''; + }; + + maxRetrySecs = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Limit retry time for secondary zones. This is the timeout after + a failed fetch attempt for the zone. Normally the value from + the SOA record is used, but this option restricts that value. + ''; + }; + + minRetrySecs = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Limit retry time for secondary zones. + ''; + }; + + + notify = mkOption { + type = types.listOf types.str; + default = []; + example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ]; + description = '' + This primary server will notify all given secondary servers about + zone changes. + + a plain IPv4/IPv6 address with on optional port number (ip@port) + + * sign notifies with the specified key + * NOKEY don't sign notifies + ]]> + ''; + }; + + notifyRetry = mkOption { + type = types.int; + default = 5; + description = '' + Specifies the number of retries for failed notifies. Set this along with notify. + ''; + }; + + outgoingInterface = mkOption { + type = types.nullOr types.str; + default = null; + example = "2000::1@1234"; + description = '' + This address will be used for zone-transfere requests if configured + as a secondary server or notifications in case of a primary server. + Supply either a plain IPv4 or IPv6 address with an optional port + number (ip@port). + ''; + }; + + provideXFR = mkOption { + type = types.listOf types.str; + default = []; + example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ]; + description = '' + Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED + address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40 + ''; + }; + + requestXFR = mkOption { + type = types.listOf types.str; + default = []; + example = []; + description = '' + Format: [AXFR|UDP] <ip-address> <key-name | NOKEY> + ''; + }; + + rrlWhitelist = mkOption { + type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]); + default = []; + description = '' + Whitelists the given rrl-types. + ''; + }; + + zoneStats = mkOption { + type = types.nullOr types.str; + default = null; + example = "%s"; + description = '' + When set to something distinct to null NSD is able to collect + statistics per zone. All statistics of this zone(s) will be added + to the group specified by this given name. Use "%s" to use the zones + name as the group. The groups are output from nsd-control stats + and stats_noreset. + ''; + }; + }; + }; + + keyPolicy = types.submodule { + options = { + keySize = mkOption { + type = types.int; + description = "Key size in bits"; + }; + prePublish = mkOption { + type = types.str; + description = "How long in advance to publish new keys"; + }; + postPublish = mkOption { + type = types.str; + description = "How long after deactivation to keep a key in the zone"; + }; + rollPeriod = mkOption { + type = types.str; + description = "How frequently to change keys"; + }; + }; + }; + + dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs); + + dnssec = dnssecZones != {}; + + dnssecTools = pkgs.bind.override { enablePython = true; }; + + signZones = optionalString dnssec '' + mkdir -p ${stateDir}/dnssec + chown ${username}:${username} ${stateDir}/dnssec + chmod 0600 ${stateDir}/dnssec + ${concatStrings (mapAttrsToList signZone dnssecZones)} + ''; + signZone = name: zone: '' + ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name} + ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name} + ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name} + ''; + policyFile = name: policy: pkgs.writeText "${name}.policy" '' + zone ${name} { + algorithm ${policy.algorithm}; + key-size zsk ${toString policy.zsk.keySize}; + key-size ksk ${toString policy.ksk.keySize}; + keyttl ${policy.keyttl}; + pre-publish zsk ${policy.zsk.prePublish}; + pre-publish ksk ${policy.ksk.prePublish}; + post-publish zsk ${policy.zsk.postPublish}; + post-publish ksk ${policy.ksk.postPublish}; + roll-period zsk ${policy.zsk.rollPeriod}; + roll-period ksk ${policy.ksk.rollPeriod}; + coverage ${policy.coverage}; + }; + ''; +in +{ + # options are ordered alphanumerically + options.fudo.nsd = { + + enable = mkEnableOption "NSD authoritative DNS server"; + + bind8Stats = mkEnableOption "BIND8 like statistics"; + + dnssecInterval = mkOption { + type = types.str; + default = "1h"; + description = '' + How often to check whether dnssec key rollover is required + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra nsd config. + ''; + }; + + hideVersion = mkOption { + type = types.bool; + default = true; + description = '' + Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries. + ''; + }; + + identity = mkOption { + type = types.str; + default = "unidentified server"; + description = '' + Identify the server (CH TXT ID.SERVER entry). + ''; + }; + + interfaces = mkOption { + type = types.listOf types.str; + default = [ "127.0.0.0" "::1" ]; + description = '' + What addresses the server should listen to. + ''; + }; + + ipFreebind = mkOption { + type = types.bool; + default = false; + description = '' + Whether to bind to nonlocal addresses and interfaces that are down. + Similar to ip-transparent. + ''; + }; + + ipTransparent = mkOption { + type = types.bool; + default = false; + description = '' + Allow binding to non local addresses. + ''; + }; + + ipv4 = mkOption { + type = types.bool; + default = true; + description = '' + Whether to listen on IPv4 connections. + ''; + }; + + ipv4EDNSSize = mkOption { + type = types.int; + default = 4096; + description = '' + Preferred EDNS buffer size for IPv4. + ''; + }; + + ipv6 = mkOption { + type = types.bool; + default = true; + description = '' + Whether to listen on IPv6 connections. + ''; + }; + + ipv6EDNSSize = mkOption { + type = types.int; + default = 4096; + description = '' + Preferred EDNS buffer size for IPv6. + ''; + }; + + logTimeAscii = mkOption { + type = types.bool; + default = true; + description = '' + Log time in ascii, if false then in unix epoch seconds. + ''; + }; + + nsid = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + NSID identity (hex string, or "ascii_somestring"). + ''; + }; + + port = mkOption { + type = types.int; + default = 53; + description = '' + Port the service should bind do. + ''; + }; + + reuseport = mkOption { + type = types.bool; + default = pkgs.stdenv.isLinux; + description = '' + Whether to enable SO_REUSEPORT on all used sockets. This lets multiple + processes bind to the same port. This speeds up operation especially + if the server count is greater than one and makes fast restarts less + prone to fail + ''; + }; + + rootServer = mkOption { + type = types.bool; + default = false; + description = '' + Whether this server will be a root server (a DNS root server, you + usually don't want that). + ''; + }; + + roundRobin = mkEnableOption "round robin rotation of records"; + + serverCount = mkOption { + type = types.int; + default = 1; + description = '' + Number of NSD servers to fork. Put the number of CPUs to use here. + ''; + }; + + + stateDir = mkOption { + type = types.str; + description = "Directory at which to store NSD state data."; + default = "/var/lib/nsd"; + }; + + statistics = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Statistics are produced every number of seconds. Prints to log. + If null no statistics are logged. + ''; + }; + + tcpCount = mkOption { + type = types.int; + default = 100; + description = '' + Maximum number of concurrent TCP connections per server. + ''; + }; + + tcpQueryCount = mkOption { + type = types.int; + default = 0; + description = '' + Maximum number of queries served on a single TCP connection. + 0 means no maximum. + ''; + }; + + tcpTimeout = mkOption { + type = types.int; + default = 120; + description = '' + TCP timeout in seconds. + ''; + }; + + verbosity = mkOption { + type = types.int; + default = 0; + description = '' + Verbosity level. + ''; + }; + + version = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The version string replied for CH TXT version.server and version.bind + queries. Will use the compiled package version on null. + See hideVersion for enabling/disabling this responses. + ''; + }; + + xfrdReloadTimeout = mkOption { + type = types.int; + default = 1; + description = '' + Number of seconds between reloads triggered by xfrd. + ''; + }; + + zonefilesCheck = mkOption { + type = types.bool; + default = true; + description = '' + Whether to check mtime of all zone files on start and sighup. + ''; + }; + + + keys = mkOption { + type = types.attrsOf (types.submodule { + options = { + + algorithm = mkOption { + type = types.str; + default = "hmac-sha256"; + description = '' + Authentication algorithm for this key. + ''; + }; + + keyFile = mkOption { + type = types.path; + description = '' + Path to the file which contains the actual base64 encoded + key. The key will be copied into "${stateDir}/private" before + NSD starts. The copied file is only accessibly by the NSD + user. + ''; + }; + + }; + }); + default = {}; + example = literalExample '' + { "tsig.example.org" = { + algorithm = "hmac-md5"; + keyFile = "/path/to/my/key"; + }; + } + ''; + description = '' + Define your TSIG keys here. + ''; + }; + + + ratelimit = { + + enable = mkEnableOption "ratelimit capabilities"; + + ipv4PrefixLength = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + IPv4 prefix length. Addresses are grouped by netblock. + ''; + }; + + ipv6PrefixLength = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + IPv6 prefix length. Addresses are grouped by netblock. + ''; + }; + + ratelimit = mkOption { + type = types.int; + default = 200; + description = '' + Max qps allowed from any query source. + 0 means unlimited. With an verbosity of 2 blocked and + unblocked subnets will be logged. + ''; + }; + + slip = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Number of packets that get discarded before replying a SLIP response. + 0 disables SLIP responses. 1 will make every response a SLIP response. + ''; + }; + + size = mkOption { + type = types.int; + default = 1000000; + description = '' + Size of the hashtable. More buckets use more memory but lower + the chance of hash hash collisions. + ''; + }; + + whitelistRatelimit = mkOption { + type = types.int; + default = 2000; + description = '' + Max qps allowed from whitelisted sources. + 0 means unlimited. Set the rrl-whitelist option for specific + queries to apply this limit instead of the default to them. + ''; + }; + + }; + + + remoteControl = { + + enable = mkEnableOption "remote control via nsd-control"; + + controlCertFile = mkOption { + type = types.path; + default = "/etc/nsd/nsd_control.pem"; + description = '' + Path to the client certificate signed with the server certificate. + This file is used by nsd-control and generated by nsd-control-setup. + ''; + }; + + controlKeyFile = mkOption { + type = types.path; + default = "/etc/nsd/nsd_control.key"; + description = '' + Path to the client private key, which is used by nsd-control + but not by the server. This file is generated by nsd-control-setup. + ''; + }; + + interfaces = mkOption { + type = types.listOf types.str; + default = [ "127.0.0.1" "::1" ]; + description = '' + Which interfaces NSD should bind to for remote control. + ''; + }; + + port = mkOption { + type = types.int; + default = 8952; + description = '' + Port number for remote control operations (uses TLS over TCP). + ''; + }; + + serverCertFile = mkOption { + type = types.path; + default = "/etc/nsd/nsd_server.pem"; + description = '' + Path to the server self signed certificate, which is used by the server + but and by nsd-control. This file is generated by nsd-control-setup. + ''; + }; + + serverKeyFile = mkOption { + type = types.path; + default = "/etc/nsd/nsd_server.key"; + description = '' + Path to the server private key, which is used by the server + but not by nsd-control. This file is generated by nsd-control-setup. + ''; + }; + + }; + + zones = mkOption { + type = types.attrsOf zoneOptions; + default = {}; + example = literalExample '' + { "serverGroup1" = { + provideXFR = [ "10.1.2.3 NOKEY" ]; + children = { + "example.com." = { + data = ''' + $ORIGIN example.com. + $TTL 86400 + @ IN SOA a.ns.example.com. admin.example.com. ( + ... + '''; + }; + "example.org." = { + data = ''' + $ORIGIN example.org. + $TTL 86400 + @ IN SOA a.ns.example.com. admin.example.com. ( + ... + '''; + }; + }; + }; + "example.net." = { + provideXFR = [ "10.3.2.1 NOKEY" ]; + data = ''' + ... + '''; + }; + } + ''; + description = '' + Define your zones here. Zones can cascade other zones and therefore + inherit settings from parent zones. Look at the definition of + children to learn about inheritance and child zones. + The given example will define 3 zones (example.(com|org|net).). Both + example.com. and example.org. inherit their configuration from + serverGroup1. + ''; + }; + }; + + config = mkIf cfg.enable { + + assertions = singleton { + assertion = zoneConfigs ? "." -> cfg.rootServer; + message = "You have a root zone configured. If this is really what you " + + "want, please enable 'services.nsd.rootServer'."; + }; + + environment = { + systemPackages = [ nsdPkg ]; + etc."nsd/nsd.conf".source = "${configFile}/nsd.conf"; + }; + + users.groups.${username}.gid = config.ids.gids.nsd; + + users.users.${username} = { + description = "NSD service user"; + home = stateDir; + createHome = true; + uid = config.ids.uids.nsd; + group = username; + }; + + systemd.services.nsd = { + description = "NSD authoritative only domain name service"; + + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + startLimitBurst = 4; + startLimitIntervalSec = 5 * 60; # 5 mins + serviceConfig = { + ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf"; + StandardError = "null"; + PIDFile = pidFile; + Restart = "always"; + RestartSec = "4s"; + }; + + preStart = '' + rm -Rf "${stateDir}/private/" + rm -Rf "${stateDir}/tmp/" + mkdir -m 0700 -p "${stateDir}/private" + mkdir -m 0700 -p "${stateDir}/tmp" + mkdir -m 0700 -p "${stateDir}/var" + cat > "${stateDir}/don't touch anything in here" << EOF + Everything in this directory except NSD's state in var and dnssec + is automatically generated and will be purged and redeployed by + the nsd.service pre-start script. + EOF + chown ${username}:${username} -R "${stateDir}/private" + chown ${username}:${username} -R "${stateDir}/tmp" + chown ${username}:${username} -R "${stateDir}/var" + rm -rf "${stateDir}/zones" + cp -rL "${nsdEnv}/zones" "${stateDir}/zones" + ${copyKeys} + ''; + }; + + systemd.timers.nsd-dnssec = mkIf dnssec { + description = "Automatic DNSSEC key rollover"; + + wantedBy = [ "nsd.service" ]; + + timerConfig = { + OnActiveSec = cfg.dnssecInterval; + OnUnitActiveSec = cfg.dnssecInterval; + }; + }; + + systemd.services.nsd-dnssec = mkIf dnssec { + description = "DNSSEC key rollover"; + + wantedBy = [ "nsd.service" ]; + before = [ "nsd.service" ]; + + script = signZones; + + postStop = '' + /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service + ''; + }; + + }; +} diff --git a/lib/fudo/secrets.nix b/lib/fudo/secrets.nix index d75effd..344371a 100644 --- a/lib/fudo/secrets.nix +++ b/lib/fudo/secrets.nix @@ -16,32 +16,28 @@ let decrypt-script = { secret-name, source-file, target-host, target-file , host-master-key, user, group, permissions }: - pkgs.writeShellScript - "decrypt-fudo-secret-${target-host}-${secret-name}.sh" '' - rm -rf ${target-file} - age -d -i ${host-master-key.key-path} -o ${target-file} ${ - encrypt-on-disk { - inherit secret-name source-file target-host; - target-pubkey = host-master-key.public-key; - } - } + pkgs.writeShellScript "decrypt-fudo-secret-${target-host}-${secret-name}.sh" '' + rm -f ${target-file} + touch ${target-file} chown ${user}:${group} ${target-file} chmod ${permissions} ${target-file} + # NOTE: silly hack because sometimes age leaves a blank line + # Only include lines with at least one non-space character + SRC=$(mktemp fudo-secret-${target-host}-${secret-name}.XXXXXXXX) + cat ${encrypt-on-disk { + inherit secret-name source-file target-host; + target-pubkey = host-master-key.public-key; + }} | grep "[^ ]" > $SRC + age -d -i ${host-master-key.key-path} -o ${target-file} $SRC + rm -f $SRC ''; secret-service = target-host: secret-name: - { source-file, target-file, user, group, permissions }: { + { source-file, target-file, user, group, permissions, ... }: { description = "decrypt secret ${secret-name} for ${target-host}."; wantedBy = [ "default.target" ]; serviceConfig = { Type = "oneshot"; - ExecStartPre = pkgs.writeShellScript - "prepare-${target-host}-${secret-name}-secret-dir.sh" '' - TARGET_DIR=$(dirname ${target-file}) - if [[ ! -d "$TARGET_DIR" ]]; then - mkdir -p "$TARGET_DIR" - fi - ''; ExecStart = let host-master-key = config.fudo.hosts.${target-host}.master-key; in decrypt-script { @@ -82,6 +78,12 @@ let description = "Permissions to set on the target file."; default = "0400"; }; + + metadata = mkOption { + type = attrsOf anything; + description = "Arbitrary metadata associated with this secret."; + default = {}; + }; }; }; @@ -166,17 +168,33 @@ in { systemd = let hostname = config.instance.hostname; + host-secrets = if (hasAttr hostname cfg.host-secrets) then cfg.host-secrets.${hostname} else { }; + host-secret-services = mapAttrs' (secret: secretOpts: (nameValuePair "fudo-secret-${hostname}-${secret}" (secret-service hostname secret secretOpts))) host-secrets; + trace-all = obj: builtins.trace obj obj; + + host-secret-paths = mapAttrsToList + (secret: secretOpts: + let perms = if secretOpts.group != "nobody" then "550" else "500"; + in "d ${dirOf secretOpts.target-file} ${perms} ${secretOpts.user} ${secretOpts.group} - -") + host-secrets; + + build-secret-paths = + map (path: "d '${path}' - root ${cfg.secret-group} - -") + cfg.secret-paths; + in { + tmpfiles.rules = host-secret-paths ++ build-secret-paths; + services = host-secret-services // { - fudo-secrets-watcher = { + fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) { wantedBy = [ "default.target" ]; description = "Ensure access for group ${cfg.secret-group} to fudo secret paths."; @@ -190,7 +208,7 @@ in { }; }; - paths.fudo-secrets-watcher = mkIf ((length cfg.secret-paths) > 0) { + paths.fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) { wantedBy = [ "default.target" ]; description = "Watch fudo secret paths, and correct perms on changes."; pathConfig = { @@ -198,9 +216,6 @@ in { Unit = "fudo-secrets-watcher.service"; }; }; - - tmpfiles.rules = map (path: "d '${path}' - root ${cfg.secret-group} - -") - cfg.secret-paths; }; }; } diff --git a/lib/fudo/users.nix b/lib/fudo/users.nix index 4ce924b..c95b6ed 100644 --- a/lib/fudo/users.nix +++ b/lib/fudo/users.nix @@ -11,34 +11,6 @@ let let user-list = attrNames users; in filter (username: list-includes user-list username) group-members; - ensure-group-directory = group: dir: '' - if [[ -d ${dir} ]]; then - GROUP="$(stat --format '%G' "${dir}")" - if [[ "$GROUP" = "${group}" ]]; then - echo "${dir} exists and belongs to ${group}" - exit 0 - else - echo "setting ownership of ${dir} to ${group}" - chgrp ${group} ${dir} - chmod g+rx ${dir} - fi - elif [[ ! -e ${dir} ]]; then - echo "creating ${dir} and setting ownership to ${group}" - mkdir ${dir} - chgrp ${group} ${dir} - chmod g+rx ${dir} - elif [[ -e ${dir} && ! -d ${dir} ]]; then - echo "unable to create directory ${dir}, object exists" - exit 2 - else - echo "unknown error creating ${dir}" - exit 3 - fi - ''; - - ensure-group-dirs-script = group: dirs: - concatStringsSep "\n" (map (ensure-group-directory group) dirs); - hostname = config.instance.hostname; host-cfg = config.fudo.hosts.${hostname}; @@ -145,17 +117,10 @@ in { }; # Group home directories have to exist, otherwise users can't log in - systemd.services = let - ensure-group-directories = group: - nameValuePair "ensure-group-directories-${group}" { - script = ensure-group-dirs-script group [ "/home/${group}" ]; - wantedBy = [ "multi-user.target" ]; - requires = [ "local-fs.target" ]; - after = [ "remote-fs.target" ]; - }; + systemd.tmpfiles.rules = let groups-with-members = attrNames (filterAttrs (group: groupOpts: (length groupOpts.members) > 0) sys.local-groups); - in listToAttrs (map ensure-group-directories groups-with-members); + in map (group: "d /home/${group} 550 root ${group} - -") groups-with-members; }; } diff --git a/lib/network.nix b/lib/network.nix index 412c08d..944b722 100644 --- a/lib/network.nix +++ b/lib/network.nix @@ -10,8 +10,44 @@ let ''; }; + # dropUntil = pred: lst: let + # drop-until-helper = pred: lst: + # if (length lst) == 0 then [] else + # if (pred (head lst)) then lst else (drop-until-helper pred (tail lst)); + # in drop-until-helper pred lst; + + # dropWhile = pred: dropUntil (el: !(pred el)); + + # is-whitespace = str: (builtins.match "^[[:space:]]*$" str) != null; + + # stripWhitespace = str: let + # lines = builtins.split "\n" str; + # lines-front-stripped = dropWhile is-whitespace lines; + # lines-rear-stripped = lib.reverseList + # (dropWhile is-whitespace + # (lib.reverseList lines-front-stripped)); + # in concatStringsSep "\n" lines-rear-stripped; + + host-ipv4 = config: hostname: let + domain = config.fudo.hosts.${hostname}.domain; + host-network = config.fudo.networks.${domain}; + in host-network.hosts.${hostname}.ipv4-address; + + host-ipv6 = config: hostname: let + domain = config.fudo.hosts.${hostname}.domain; + host-network = config.fudo.networks.${domain}; + in host-network.hosts.${hostname}.ipv6-address; + + host-ips = config: hostname: let + ipv4 = host-ipv4 config hostname; + ipv6 = host-ipv6 config hostname; + not-null = o: o != null; + in filter not-null [ ipv4 ipv6 ]; + in { + inherit host-ipv4 host-ipv6 host-ips; + generate-mac-address = hostname: interface: let pkg = generate-mac-address hostname interface; - in builtins.readFile "${pkg}"; + in removeSuffix "\n" (builtins.readFile "${pkg}"); } diff --git a/lib/types/user.nix b/lib/types/user.nix index c454af3..e344e7b 100644 --- a/lib/types/user.nix +++ b/lib/types/user.nix @@ -79,12 +79,6 @@ rec { default = [ ]; }; - # home-manager-generator = mkOption { - # type = nullOr (functionTo attrs); - # description = "Home Manager configuration for the given user."; - # default = null; - # }; - home-directory = mkOption { type = nullOr str; description = "Default home directory for the given user.";