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.";