From b5cdfc72932fecea688cfa9a216294c6f448d89f Mon Sep 17 00:00:00 2001 From: niten Date: Tue, 16 Nov 2021 16:56:43 -0800 Subject: [PATCH] Added cashew, did some backplane yak shaving --- config/client.nix | 37 +++ config/default.nix | 24 +- config/domains.nix | 4 +- config/hardware/cashew.nix | 5 + config/host-config/cashew.nix | 35 +++ config/host-config/france.nix | 18 +- config/host-config/france/jabber.nix | 10 - config/host-config/nutboy3.nix | 368 +++++++++++++++------------ config/hosts/cashew.nix | 12 + config/profile-config/server.nix | 2 + config/sites.nix | 12 + initialize.nix | 5 +- lib/fudo/acme-certs.nix | 88 +++++-- lib/fudo/backplane/common.nix | 154 +++++++++++ lib/fudo/backplane/default.nix | 2 + lib/fudo/backplane/dns.nix | 269 ++++++-------------- lib/fudo/backplane/jabber.nix | 89 +++++++ lib/fudo/kdc.nix | 30 +-- lib/fudo/ldap.nix | 171 +++++++++---- lib/fudo/postgres.nix | 129 ++++++---- lib/instance.nix | 8 + 21 files changed, 933 insertions(+), 539 deletions(-) create mode 100644 config/client.nix create mode 100644 config/hardware/cashew.nix create mode 100644 config/host-config/cashew.nix create mode 100644 config/hosts/cashew.nix create mode 100644 lib/fudo/backplane/common.nix create mode 100644 lib/fudo/backplane/jabber.nix diff --git a/config/client.nix b/config/client.nix new file mode 100644 index 0000000..a45f40a --- /dev/null +++ b/config/client.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + make-passwd-file = hostname: + pkgs.lib.fudo.passwd.stablerandom-passwd-file + "${hostname}-fudo-client-passwd" + config.instance.build-seed; + + secrets = + config.fudo.secrets.host-secrets.${config.instance.hostname}; + + host-password-files = mapAttrs (hostname: hostOpts: + make-password-file hostname) config.fudo.hosts; + +in { + config = { + fudo = { + secrets.host-secrets = mapAttrs (hostname: hostOpts: { + backplane-client-passwd = { + source-file = host-password-files.${hostname}; + target-file = "/var/fudo/client/passwd"; + user = config.fudo.client.dns.user; + }; + }) config.fudo.hosts; + + client.dns = { + password-file = + secrets.backplane-client-passwd.target-file; + }; + + backplane.client-hosts = mapAttrs (hostname: hostOpts: { + password-file = host-password-files.${hostname}; + }) config.fudo.hosts; + }; + }; +} diff --git a/config/default.nix b/config/default.nix index 633eab5..4a81385 100644 --- a/config/default.nix +++ b/config/default.nix @@ -1,16 +1,16 @@ { config, lib, pkgs, ... }: { - imports = [ - ./aliases.nix - ./bash.nix - ./common.nix - ./domains.nix - ./groups.nix - ./hosts.nix - ./networks.nix - ./sites.nix - ./users.nix - ./wireless-networks.nix - ]; + imports = [ + ./aliases.nix + ./bash.nix + ./common.nix + ./domains.nix + ./groups.nix + ./hosts.nix + ./networks.nix + ./sites.nix + ./users.nix + ./wireless-networks.nix + ]; } diff --git a/config/domains.nix b/config/domains.nix index e9deac6..5a51d17 100644 --- a/config/domains.nix +++ b/config/domains.nix @@ -14,6 +14,7 @@ local-admins = [ "niten" "reaper" ]; admin-email = "admin@fudo.org"; gssapi-realm = "FUDO.ORG"; + kerberos-master = "nutboy3"; }; "sea.fudo.org" = { @@ -63,7 +64,6 @@ admin-email = "viator@informis.land"; gssapi-realm = "INFORMIS.LAND"; kerberos-master = "procul"; - kerberos-slaves = [ "legatus" ]; primary-nameserver = "procul"; }; @@ -77,7 +77,7 @@ local-users = [ "niten" "reaper" ]; local-groups = [ "admin" ]; local-admins = [ "niten" "reaper" ]; - admin-email = "nitenn@fudo.org"; + admin-email = "niten@fudo.org"; gssapi-realm = "FUDO.ORG"; # kerberos-master = "legatus"; primary-nameserver = "legatus"; diff --git a/config/hardware/cashew.nix b/config/hardware/cashew.nix new file mode 100644 index 0000000..865d469 --- /dev/null +++ b/config/hardware/cashew.nix @@ -0,0 +1,5 @@ +{ config, lib, pkgs, ... }: + +{ + +} diff --git a/config/host-config/cashew.nix b/config/host-config/cashew.nix new file mode 100644 index 0000000..b78744d --- /dev/null +++ b/config/host-config/cashew.nix @@ -0,0 +1,35 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + host-ipv4 = pkgs.lib.fudo.network.host-ipv4 config hostname; + site-name = config.fudo.hosts.${hostname}.site; + site = config.fudo.sites.${site-name}; + + network-prefix-length = + pkgs.lib.fudo.ip.getNetworkMask site.network; + + local-packages = with pkgs; [ + bind + ]; + +in { + config = { + networking = { + defaultGateway = { + address = site.gateway-v4; + interface = "extif0"; + }; + + interfaces.extif0 = { + ipv4.addresses = [{ + address = host-ipv4; + prefixLength = network-prefix-length; + }]; + }; + }; + + environment.systemPackages = local-packages; + }; +} diff --git a/config/host-config/france.nix b/config/host-config/france.nix index ea23c12..356f2e3 100644 --- a/config/host-config/france.nix +++ b/config/host-config/france.nix @@ -51,12 +51,6 @@ in { options = [ "noatime" "nodiratime" "noexec" "subvol=grafana" ]; }; - "/srv/gitlab" = { - fsType = "btrfs"; - label = "pool0"; - options = [ "noatime" "nodiratime" "noexec" "subvol=grafana" ]; - }; - ${mail-directory} = { fsType = "btrfs"; label = "pool0"; @@ -278,12 +272,12 @@ in { locations."/" = { proxyPass = "http://127.0.0.1:8001"; extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-By $server_addr:$server_port; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - ''; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-By $server_addr:$server_port; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + ''; }; }; }; diff --git a/config/host-config/france/jabber.nix b/config/host-config/france/jabber.nix index 5b2cbdc..1c8342e 100644 --- a/config/host-config/france/jabber.nix +++ b/config/host-config/france/jabber.nix @@ -7,16 +7,6 @@ let cfg = config.fudo.france.jabber; - generate-auth-file = name: files: let - make-entry = name: passwd-file: - ''("${name}" . "${readFile passwd-file}")''; - entries = mapAttrsToList make-entry files; - content = concatStringsSep "\n" entries; - in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})"; - - host-auth-file = generate-auth-file "host" cfg.backplane.host-passwd-files; - service-auth-file = generate-auth-file "service" cfg.backplane.service-passwd-files; - ldap-password-file = pkgs.lib.fudo.passwd.random-passwd-file "ejabberd-ldap-auth-user" 30; diff --git a/config/host-config/nutboy3.nix b/config/host-config/nutboy3.nix index 270bcd9..3a7a955 100644 --- a/config/host-config/nutboy3.nix +++ b/config/host-config/nutboy3.nix @@ -3,6 +3,7 @@ with lib; let hostname = "nutboy3"; + host-fqdn = config.instance.host-fqdn; host-ipv4 = "199.87.154.175"; domain-name = config.fudo.hosts.${hostname}.domain; domain = config.fudo.domains.${domain-name}; @@ -13,184 +14,221 @@ let secrets = config.fudo.secrets.host-secrets.${hostname}; + postgresql-user = + config.systemd.services.postgresql.serviceConfig.User; + + files = config.fudo.secrets.files; + + acme-copies = config.fudo.acme.host-domains.${hostname}; + in { - networking = { - enableIPv6 = true; - nameservers = [ "1.1.1.1" ]; - defaultGateway = { - address = site.gateway-v4; - interface = "extif0"; + config = { + networking = { + enableIPv6 = true; + + nameservers = [ "1.1.1.1" ]; + defaultGateway = { + address = site.gateway-v4; + interface = "extif0"; + }; + + interfaces.extif0.ipv4.addresses = [{ + address = host-ipv4; + prefixLength = 31; + }]; }; - interfaces.extif0.ipv4.addresses = [{ - address = host-ipv4; - prefixLength = 31; - }]; - }; + systemd.tmpfiles.rules = [ + "L /etc/adjtime - - - - /state/etc/adjtime" + ]; - systemd.tmpfiles.rules = [ - "L /etc/adjtime - - - - /state/etc/adjtime" - ]; + environment.systemPackages = local-packages; - environment.systemPackages = local-packages; + fudo = { + hosts.${hostname}.external-interfaces = [ "extif0" ]; - # networking.firewall.allowedTCPPorts = [ 80 443 ]; + 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; + }; - # informis.cl-gemini = { - # enable = true; + ldap-keytab = { + source-file = files.service-keytabs.${hostname}.openldap; + target-file = "/run/openldap/ldap.keytab"; + user = config.services.openldap.user; + }; - # hostname = "gemini.informis.land"; - # server-ip = host-ipv4; - # document-root = "/srv/gemini/root"; - # textfiles-archive = "${pkgs.textfiles}"; - # slynk-port = 4005; + postgresql-keytab = { + source-file = files.service-keytabs.nutboy3.postgres; + target-file = "/run/postgresql/postgres.keytab"; + user = postgresql-user; + }; + }; - # feeds = { - # viator = { - # title = "viator's phlog"; - # path = "/home/viator/gemini-public/feed/"; - # url = "gemini://informis.land/user/viator/feed/"; - # }; - # }; - # }; + acme.host-domains.${hostname}.${host-fqdn}.local-copies = { + openldap = { + user = config.services.openldap.user; + dependent-services = [ "openldap.service" ]; + part-of = [ config.fudo.auth.ldap-server.systemd-target ]; + }; - fudo = { - hosts.${hostname}.external-interfaces = [ "extif0" ]; - secrets.host-secrets.nutboy3 = let - files = config.fudo.secrets.files; + postgresql = { + user = postgresql-user; + dependent-services = [ "postgresql.service" ]; + part-of = [ config.fudo.postgresql.systemd-target ]; + }; + }; + + client.dns = { + ipv4 = true; + ipv6 = true; + user = "fudo-client"; + external-interface = "extif0"; + }; + + auth = { + ldap-server = let + ldap-copy = acme-copies.${host-fqdn}.local-copies.openldap; + in { + enable = true; + base = "dc=fudo,dc=org"; + organization = "Fudo"; + kerberos-host = host-fqdn; + kerberos-keytab = secrets.ldap-keytab.target-file; + listen-uris = [ "ldap:///" "ldaps:///" "ldapi:///"]; + required-services = [ ldap-copy.service ]; + + users = config.fudo.users; + groups = config.fudo.groups; + system-users = config.fudo.system-users; + + state-directory = "/state/openldap"; + + ssl-chain = ldap-copy.chain; + ssl-certificate = ldap-copy.certificate; + ssl-private-key = ldap-copy.private-key; + ssl-ca-certificate = "${pkgs.letsencrypt-ca}"; + }; + + kdc = { + master-key-file = + secrets.heimdal-master-key.target-file; + state-directory = "/state/kerberos"; + }; + }; + + # dns.state-directory = "/state/nsd"; + + # mail-server = { + # enableContainer = 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 = let + cert-copy = + config.fudo.acme.host-domains.${hostname}.${host-fqdn}.local-copies.postgresql; + in { + enable = true; + ssl-certificate = cert-copy.full-certificate; + ssl-private-key = cert-copy.private-key; + keytab = secrets.postgresql-keytab.target-file; + local-networks = config.instance.local-networks; + state-directory = "/state/postgresql"; + required-services = [ cert-copy.service ]; + }; + + # 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; + # }; + # }; + }; + + containers.cashew = let + initialize-host = import ../../initialize.nix; + build-timestamp = config.instance.build-timestamp; + site = "nuttyclub-vm"; + domain = config.instance.local-domain; + profile = "container"; in { - # heimdal-master-key = { - # source-file = files.realm-master-keys."FUDO.ORG"; - # target-file = "/run/heimdal/master-key"; - # user = config.fudo.auth.kdc.user; - # }; + autoStart = true; - # ipropd-keytab = { - # source-file = files.service-keytabs.legatus.ipropd; - # target-file = "/run/heimdal/ipropd.keytab"; - # user = config.fudo.auth.kdc.user; - # }; + bindMounts = { + + }; + + config = { pkgs, ... }: { + imports = [ + (initialize-host { + inherit + lib + pkgs + build-timestamp + site + domain + profile; + hostname = "cashew"; + }) + ]; + + instance.build-seed = build-seed; + }; }; - - 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/hosts/cashew.nix b/config/hosts/cashew.nix new file mode 100644 index 0000000..ced361a --- /dev/null +++ b/config/hosts/cashew.nix @@ -0,0 +1,12 @@ +{ + description = "fudo.org primary dns server."; + rp = "reaper"; + admin-email = "reaper@fudo.org"; + domain = "fudo.org"; + site = "nuttyclub-vm"; + profile = "container"; + enable-gui = false; + arch = "x86_64-linux"; + nixos-system = true; + machine-id = "e5f456e3183a4dc186181a70bc3af2d1"; +} diff --git a/config/profile-config/server.nix b/config/profile-config/server.nix index 441927a..720ed0a 100644 --- a/config/profile-config/server.nix +++ b/config/profile-config/server.nix @@ -31,6 +31,8 @@ let TIMEOUT=15m fi + TMP=$(mktemp -d /tmp/fudo-test-config-XXXXXXX) + SYNCFILE=$TMP/sync-$(date +"%Y%m%d-%H%M%N") touch $SYNCFILE ${pkgs.utillinux}/bin/wall "Launching config. System will restart in $TIMEOUT if $SYNCFILE still exists." diff --git a/config/sites.nix b/config/sites.nix index ab516c4..1fdd6c0 100644 --- a/config/sites.nix +++ b/config/sites.nix @@ -50,6 +50,18 @@ mail-server = "mail.fudo.org"; }; + nuttyclub-vm = { + gateway-v4 = "FIXME"; + network = "FIXME/29"; + 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" ]; diff --git a/initialize.nix b/initialize.nix index 3b00085..f7bcd30 100644 --- a/initialize.nix +++ b/initialize.nix @@ -1,5 +1,6 @@ { lib, pkgs, hostname, site, domain, profile, build-timestamp, ... }: +with lib; let # Get info on this host so we know what to load config-dir = ./. + "/config"; @@ -8,13 +9,13 @@ in { imports = [ ./lib ./config - + ] ++ (filter pathExists [ (config-dir + /hardware/${hostname}.nix) (config-dir + /host-config/${hostname}.nix) (config-dir + /profile-config/${profile}.nix) (config-dir + /domain-config/${domain}.nix) (config-dir + /site-config/${site}.nix) - ]; + ]); config = { instance = { diff --git a/lib/fudo/acme-certs.nix b/lib/fudo/acme-certs.nix index 58b5766..c11627e 100644 --- a/lib/fudo/acme-certs.nix +++ b/lib/fudo/acme-certs.nix @@ -1,7 +1,9 @@ { config, lib, pkgs, ... } @ toplevel: with lib; -let +let + hostname = config.instance.hostname; + domainOpts = { name, ... }: let domain = name; in { @@ -23,7 +25,7 @@ let copy = name; in { options = with types; let - target-path = "/var/run/${domain}/${copy}"; + target-path = "/run/ssl-certificates/${domain}/${copy}"; in { user = mkOption { type = str; @@ -39,7 +41,7 @@ let service = mkOption { type = str; description = "systemd job to copy certs."; - default = "fudo-${domain}-${copy}-certs.service"; + default = "fudo-acme-${domain}-${copy}-certs.service"; }; certificate = mkOption { @@ -65,6 +67,18 @@ let description = "Full path to the local copy certificate."; default = "${target-path}/key.pem"; }; + + dependent-services = mkOption { + type = listOf str; + description = "List of systemd services depending on this copy."; + default = [ ]; + }; + + part-of = mkOption { + type = listOf str; + description = "List of systemd targets to which this copy belongs."; + default = [ ]; + }; }; }; in mkOption { @@ -82,9 +96,9 @@ let concatMapAttrs = f: attrs: foldr (a: b: a // b) {} (mapAttrsToList f attrs); - hostname = config.instance.hostname; cfg = config.fudo.acme; - localDomains = if (hasAttr hostname cfg.host-domains) then + hasLocalDomains = hasAttr hostname cfg.host-domains; + localDomains = if hasLocalDomains then cfg.host-domains.${hostname} else {}; optionalStringOr = str: default: @@ -104,6 +118,29 @@ in { email = domainOpts.email; extraDomainNames = domainOpts.extra-domains; }) localDomains; + + # Assume that if we're acquiring SSL certs, we have a real IP for the + # host. nginx must have an acme dir for security.acme to work. + services.nginx = mkIf hasLocalDomains { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + + virtualHosts.${config.instance.host-fqdn} = { + enableACME = true; + forceSSL = true; + + # Just...force override if you want this to point somewhere. + locations."/" = { + return = "403 Forbidden"; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; systemd = { tmpfiles.rules = let @@ -112,7 +149,7 @@ in { perms = copyOpts: if (copyOpts.group != null) then "0550" else "0500"; copy-paths = mapAttrsToList (copy: copyOpts: let - dir-entry = copyOpts: file: "D \"${dirOf file}\" ${perms copyOpts} ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -"; + dir-entry = copyOpts: file: "d \"${dirOf file}\" ${perms copyOpts} ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -"; in map (dir-entry copyOpts) [ copyOpts.certificate copyOpts.full-certificate @@ -122,41 +159,48 @@ in { in unique (concatMap (i: unique i) copy-paths); services = concatMapAttrs (domain: domainOpts: - mapAttrs' (copy: copyOpts: let + concatMapAttrs (copy: copyOpts: let key-perms = copyOpts: if (copyOpts.group != null) then "0440" else "0400"; source = config.security.acme.certs.${domain}.directory; target = copyOpts.path; + owners = + if (copyOpts.group != null) then + "${copyOpts.user}:${copyOpts.group}" + else copyOpts.user; install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" '' - cp cert.pem ${copyOpts.certificate} + cp ${source}/cert.pem ${copyOpts.certificate} chmod 0444 ${copyOpts.certificate} + chown ${owners} ${copyOpts.certificate} - cp full.pem ${copyOpts.full-certificate} + cp ${source}/full.pem ${copyOpts.full-certificate} chmod 0444 ${copyOpts.full-certificate} + chown ${owners} ${copyOpts.full-certificate} - cp chain.pem ${copyOpts.chain} + cp ${source}/chain.pem ${copyOpts.chain} chmod 0444 ${copyOpts.chain} + chown ${owners} ${copyOpts.chain} - cp key.pem ${copyOpts.private-key} + cp ${source}/key.pem ${copyOpts.private-key} chmod ${key-perms copyOpts} ${copyOpts.private-key} + chown ${owners} ${copyOpts.private-key} ''; - remove-certs = pkgs.writeShellScript "fudo-remove-${domain}-${copy}-certs.sh" '' - rm -f ${copyOpts.private-key} - rm -f ${copyOpts.chain} - rm -f ${copyOpts.full-certificate} - rm -f ${copyOpts.certificate} - ''; - in nameValuePair - (rm-service-ext copyOpts.service) { + + service-name = rm-service-ext copyOpts.service; + in { + ${service-name} = { description = "Copy ${domain} ACME certs for ${copy}."; after = [ "acme-${domain}.service" ]; + before = copyOpts.dependent-services; + wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services; + partOf = copyOpts.part-of; serviceConfig = { - Type = "oneshot"; + Type = "simple"; ExecStart = install-certs; - ExecStop = remove-certs; RemainAfterExit = true; StandardOutput = "journal"; }; - }) domainOpts.local-copies) localDomains; + }; + }) domainOpts.local-copies) localDomains; }; }; } diff --git a/lib/fudo/backplane/common.nix b/lib/fudo/backplane/common.nix new file mode 100644 index 0000000..a1d3e95 --- /dev/null +++ b/lib/fudo/backplane/common.nix @@ -0,0 +1,154 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.fudo.backplane.dns; + + powerdns-conf-dir = "${cfg.powerdns.home}/conf.d"; + + clientHostOpts = { name, ... }: { + options = with types; { + password-file = mkOption { + type = path; + description = + "Location (on the build host) of the file containing the host password."; + }; + }; + }; + + serviceOpts = { name, ... }: { + options = with types; { + password-file = mkOption { + type = path; + description = + "Location (on the build host) of the file containing the service password."; + }; + }; + }; + + databaseOpts = { ... }: { + options = with types; { + host = mkOption { + type = str; + description = "Hostname or IP of the PostgreSQL server."; + }; + + database = mkOption { + type = str; + description = "Database to use for DNS backplane."; + default = "backplane_dns"; + }; + + username = mkOption { + type = str; + description = "Database user for DNS backplane."; + default = "backplane_dns"; + }; + + password-file = mkOption { + type = str; + description = "File containing password for database user."; + }; + }; + }; + +in { + options.fudo.backplane = with types; { + + client-hosts = mkOption { + type = attrsOf (submodule clientHostOpts); + description = "List of backplane client options."; + default = {}; + }; + + services = mkOption { + type = attrsOf (submodule serviceOpts); + description = "List of backplane service options."; + default = {}; + }; + + backplane-host = mkOption { + type = types.str; + description = "Hostname of the backplane XMPP server."; + }; + + dns = { + enable = mkEnableOption "Enable backplane dynamic DNS server."; + + port = mkOption { + type = port; + description = "Port on which to serve authoritative DNS requests."; + default = 53; + }; + + listen-v4-addresses = mkOption { + type = listOf str; + description = "IPv4 addresses on which to listen for dns requests."; + default = [ "0.0.0.0" ]; + }; + + listen-v6-addresses = mkOption { + type = listOf str; + description = "IPv6 addresses on which to listen for dns requests."; + example = [ "[abcd::1]" ]; + default = [ ]; + }; + + required-services = mkOption { + type = listOf str; + description = + "A list of services required before the DNS server can start."; + default = [ ]; + }; + + user = mkOption { + type = str; + description = "User as which to run DNS backplane listener service."; + default = "backplane-dns"; + }; + + group = mkOption { + type = str; + description = "Group as which to run DNS backplane listener service."; + default = "backplane-dns"; + }; + + database = mkOption { + type = submodule databaseOpts; + description = "Database settings for the DNS server."; + }; + + powerdns = { + home = mkOption { + type = str; + description = "Directory at which to store powerdns configuration and state."; + default = "/run/backplane-dns/powerdns"; + }; + + user = mkOption { + type = str; + description = "Username as which to run PowerDNS."; + default = "backplane-powerdns"; + }; + + database = mkOption { + type = submodule databaseOpts; + description = "Database settings for the DNS server."; + }; + }; + + backplane-role = { + role = mkOption { + type = types.str; + description = "Backplane XMPP role name for the DNS server."; + default = "service-dns"; + }; + + password-file = mkOption { + type = types.str; + description = "File containing XMPP password for backplane role."; + }; + }; + }; + }; +} diff --git a/lib/fudo/backplane/default.nix b/lib/fudo/backplane/default.nix index c27c2df..5596440 100644 --- a/lib/fudo/backplane/default.nix +++ b/lib/fudo/backplane/default.nix @@ -3,6 +3,8 @@ with lib; { imports = [ + ./common.nix ./dns.nix + ./jabber.nix ]; } diff --git a/lib/fudo/backplane/dns.nix b/lib/fudo/backplane/dns.nix index c9dda2a..6c97556 100644 --- a/lib/fudo/backplane/dns.nix +++ b/lib/fudo/backplane/dns.nix @@ -2,134 +2,13 @@ with lib; let - cfg = config.fudo.backplane.dns; + backplane-cfg = config.fudo.backplane; + + cfg = backplane-cfg.dns; powerdns-conf-dir = "${cfg.powerdns.home}/conf.d"; - backplaneOpts = { ... }: { - options = { - host = mkOption { - type = types.str; - description = "Hostname of the backplane jabber server."; - }; - - role = mkOption { - type = types.str; - description = "Backplane XMPP role name for the DNS server."; - default = "service-dns"; - }; - - password-file = mkOption { - type = types.str; - description = "File containing XMPP password for backplane role."; - }; - - database = mkOption { - type = with types; submodule databaseOpts; - description = "Database settings for backplane server."; - }; - - cl-wrapper-package = mkOption { - type = types.package; - description = "Common Lisp wrapper package to use."; - default = pkgs.lispPackages.clwrapper; - }; - }; - }; - - databaseOpts = { ... }: { - options = { - host = mkOption { - type = types.str; - description = "Hostname or IP of the PostgreSQL server."; - }; - - database = mkOption { - type = types.str; - description = "Database to use for DNS backplane."; - default = "backplane_dns"; - }; - - username = mkOption { - type = types.str; - description = "Database user for DNS backplane."; - default = "backplane_dns"; - }; - - password-file = mkOption { - type = types.str; - description = "File containing password for database user."; - }; - }; - }; - in { - options.fudo.backplane.dns = with types; { - enable = mkEnableOption "Enable backplane dynamic DNS server."; - - port = mkOption { - type = port; - description = "Port on which to serve authoritative DNS requests."; - default = 53; - }; - - listen-v4-addresses = mkOption { - type = listOf str; - description = "IPv4 addresses on which to listen for dns requests."; - default = [ "0.0.0.0" ]; - }; - - listen-v6-addresses = mkOption { - type = listOf str; - description = "IPv6 addresses on which to listen for dns requests."; - example = [ "[abcd::1]" ]; - default = [ ]; - }; - - required-services = mkOption { - type = listOf str; - description = - "A list of services required before the DNS server can start."; - default = [ ]; - }; - - user = mkOption { - type = str; - description = "User as which to run DNS backplane listener service."; - default = "backplane-dns"; - }; - - group = mkOption { - type = str; - description = "Group as which to run DNS backplane listener service."; - default = "backplane-dns"; - }; - - database = mkOption { - type = submodule databaseOpts; - description = "Database settings for the DNS server."; - }; - - backplane = mkOption { - type = submodule backplaneOpts; - description = "Backplane Jabber settings for the DNS server."; - }; - - powerdns = { - home = mkOption { - type = str; - description = "Directory at which to store powerdns configuration and state."; - default = "/run/backplane-dns/powerdns"; - }; - - user = mkOption { - type = str; - description = "Username as which to run PowerDNS."; - default = "backplane-powerdns"; - }; - }; - }; - config = mkIf cfg.enable { users = { users = { @@ -147,88 +26,88 @@ in { }; groups = { - "${cfg.group}" = { members = [ cfg.user ]; }; + ${cfg.group} = { members = [ cfg.user ]; }; ${cfg.powerdns.user} = { members = [ cfg.powerdns.user ]; }; }; }; - fudo.system.services = { - backplane-powerdns-config-generator = { - description = - "Generate postgres configuration for backplane DNS server."; - requires = cfg.required-services; - type = "oneshot"; - restartIfChanged = true; - partOf = [ "backplane-dns.target" ]; + fudo = { + system.services = { + backplane-powerdns-config-generator = { + description = + "Generate postgres configuration for backplane DNS server."; + requires = cfg.required-services; + type = "oneshot"; + restartIfChanged = true; + partOf = [ "backplane-dns.target" ]; - readWritePaths = [ powerdns-conf-dir ]; + readWritePaths = [ powerdns-conf-dir ]; - preStart = '' - mkdir -p ${powerdns-conf-dir} - chown ${cfg.powerdns.user}:${cfg.powerdns.user} ${powerdns-conf-dir} - ''; + # This builds the config in a bash script, to avoid storing the password + # in the nix store at any point + script = let + user = cfg.powerdns.user; + db = cfg.powerdns.database; + in '' + TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t pdns-XXXXXXXXXX) + TMPCONF=$TMPDIR/pdns.local.gpgsql.conf - # This builds the config in a bash script, to avoid storing the password - # in the nix store at any point - script = '' - if [ ! -d ${powerdns-conf-dir} ]; then - mkdir ${powerdns-conf-dir} - fi + if [ ! -f ${cfg.database.password-file} ]; then + echo "${cfg.database.password-file} does not exist!" + exit 1 + fi - TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t pdns-XXXXXXXXXX) - TMPCONF=$TMPDIR/pdns.local.gpgsql.conf + touch $TMPCONF + chmod go-rwx $TMPCONF + chown ${user} $TMPCONF + PASSWORD=$(cat ${db.password-file}) + echo "launch+=gpgsql" >> $TMPCONF + echo "gpgsql-host=${db.host}" >> $TMPCONF + echo "gpgsql-dbname=${db.database}" >> $TMPCONF + echo "gpgsql-user=${db.username}" >> $TMPCONF + echo "gpgsql-password=$PASSWORD" >> $TMPCONF + echo "gpgsql-dnssec=yes" >> $TMPCONF - if [ ! -f ${cfg.database.password-file} ]; then - echo "${cfg.database.password-file} does not exist!" - exit 1 - fi + mv $TMPCONF ${powerdns-conf-dir}/pdns.local.gpgsql.conf + rm -rf $TMPDIR - touch $TMPCONF - chown ${cfg.powerdns.user}:${cfg.powerdns.user} $TMPCONF - chmod go-rwx $TMPCONF - PASSWORD=$(cat ${cfg.database.password-file}) - echo "launch+=gpgsql" >> $TMPCONF - echo "gpgsql-host=${cfg.database.host}" >> $TMPCONF - echo "gpgsql-dbname=${cfg.database.database}" >> $TMPCONF - echo "gpgsql-user=${cfg.database.username}" >> $TMPCONF - echo "gpgsql-password=$PASSWORD" >> $TMPCONF - echo "gpgsql-dnssec=yes" >> $TMPCONF + exit 0 + ''; + }; - mv $TMPCONF ${powerdns-conf-dir}/pdns.local.gpgsql.conf - rm -rf $TMPDIR + backplane-dns = { + description = "Fudo DNS Backplane Server"; + restartIfChanged = true; + path = with pkgs; [ backplane-dns-server ]; + execStart = "launch-backplane-dns.sh"; + pidFile = "/run/backplane-dns.$USERNAME.pid"; + user = cfg.user; + group = cfg.group; + partOf = [ "backplane-dns.target" ]; + requires = cfg.required-services ++ [ "postgresql.service" ]; + environment = { + FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = backplane-cfg.backplane-host; + FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane-role.role; + FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane-role.password-file; + FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.database.host; + FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.database.database; + FUDO_DNS_BACKPLANE_DATABASE_USERNAME = + cfg.database.username; + FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE = + cfg.database.password-file; - exit 0 - ''; - }; - - backplane-dns = { - description = "Fudo DNS Backplane Server"; - restartIfChanged = true; - path = with pkgs; [ backplane-dns-server ]; - execStart = "launch-backplane-dns.sh"; - pidFile = "/run/backplane-dns.$USERNAME.pid"; - user = cfg.user; - group = cfg.group; - partOf = [ "backplane-dns.target" ]; - requires = cfg.required-services ++ [ "postgresql.service" ]; - environment = { - FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane.host; - FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane.role; - FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane.password-file; - FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.backplane.database.host; - FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.backplane.database.database; - FUDO_DNS_BACKPLANE_DATABASE_USERNAME = - cfg.backplane.database.username; - FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE = - cfg.backplane.database.password-file; - - CL_SOURCE_REGISTRY = - pkgs.lib.fudo.lisp.lisp-source-registry pkgs.backplane-dns-server; + CL_SOURCE_REGISTRY = + pkgs.lib.fudo.lisp.lisp-source-registry pkgs.backplane-dns-server; + }; }; }; }; systemd = { + tmpfiles.rules = [ + "d ${powerdns-conf-dir} 0700 ${cfg.powerdns.user} - - -" + ]; + targets = { backplane-dns = { description = "Fudo DNS backplane services."; @@ -240,12 +119,12 @@ in { services = { backplane-powerdns = let pdns-config-dir = pkgs.writeTextDir "pdns.conf" '' - local-address=${lib.concatStringsSep ", " cfg.listen-v4-addresses} - local-ipv6=${lib.concatStringsSep ", " cfg.listen-v6-addresses} - local-port=${toString cfg.port} - launch= - include-dir=${powerdns-conf-dir}/ - ''; + local-address=${lib.concatStringsSep ", " cfg.listen-v4-addresses} + local-ipv6=${lib.concatStringsSep ", " cfg.listen-v6-addresses} + local-port=${toString cfg.port} + launch= + include-dir=${powerdns-conf-dir}/ + ''; in { description = "Backplane PowerDNS name server"; requires = [ diff --git a/lib/fudo/backplane/jabber.nix b/lib/fudo/backplane/jabber.nix new file mode 100644 index 0000000..d3bac4f --- /dev/null +++ b/lib/fudo/backplane/jabber.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.fudo.backplane; + + backplane-server = cfg.backplane-host; + + generate-auth-file = name: files: let + make-entry = name: passwd-file: + ''("${name}" . "${readFile passwd-file}")''; + entries = mapAttrsToList make-entry files; + content = concatStringsSep "\n" entries; + in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})"; + + host-auth-file = generate-auth-file "host" + (mapAttrs (hostname: hostOpts: hostOpts.password-file) + cfg.client-hosts); + + service-auth-file = generate-auth-file "service" + (mapAttrs (service: serviceOpts: serviceOpts.password-file) + cfg.services); + +in { + config = mkIf config.fudo.jabber.enable { + + fudo = { + secrets.host-secrets.${hostname} = { + backplane-host-auth = { + source-file = host-auth-file; + target-file = "/var/backplane/host-passwords.scm"; + user = config.fudo.jabber.user; + }; + backplane-service-auth = { + source-file = service-auth-file; + target-file = "/var/backplane/service-passwords.scm"; + user = config.fudo.jabber.user; + }; + }; + + jabber = { + environment = { + FUDO_HOST_PASSWD_FILE = + secrets.backplane-host-auth.target-file; + FUDO_SERVICE_PASSWD_FILE = + secrets.backplane-service-auth.target-file; + }; + + sites.${backplane-server} = { + site-config = { + auth_method = "external"; + extauth_program = + "${pkgs.guile}/bin/guile -s ${pkgs.backplane-auth}/backplane-auth.scm"; + extauth_pool_size = 3; + auth_use_cache = true; + + modules = { + mod_adhoc = {}; + mod_caps = {}; + mod_carboncopy = {}; + mod_client_state = {}; + mod_configure = {}; + mod_disco = {}; + mod_fail2ban = {}; + mod_last = {}; + mod_offline = { + access_max_user_messages = 5000; + }; + mod_ping = {}; + mod_pubsub = { + access_createnode = "pubsub_createnode"; + ignore_pep_from_offline = true; + last_item_cache = false; + plugins = [ + "flat" + "pep" + ]; + }; + mod_roster = {}; + mod_stream_mgmt = {}; + mod_time = {}; + mod_version = {}; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/fudo/kdc.nix b/lib/fudo/kdc.nix index a195093..c093080 100644 --- a/lib/fudo/kdc.nix +++ b/lib/fudo/kdc.nix @@ -6,6 +6,16 @@ let hostname = config.instance.hostname; + localhost-ips = let + addr-only = addrinfo: addrinfo.address; + interface = config.networking.interfaces.lo; + in + (map addr-only interface.ipv4.addresses) ++ + (map addr-only interface.ipv6.addresses); + + host-ips = + (pkgs.lib.fudo.network.host-ips hostname) ++ localhost-ips; + state-directory = toplevel.config.fudo.auth.kdc.state-directory; database-file = "${state-directory}/principals.db"; @@ -249,7 +259,7 @@ in { bind-addresses = mkOption { type = listOf str; description = "A list of IP addresses on which to bind."; - default = [ ]; + default = host-ips; }; user = mkOption { @@ -392,22 +402,6 @@ in { 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; @@ -477,7 +471,7 @@ in { }; heimdal-ipropd-slave = { - #wantedBy = [ "multi-user.target" ]; + wantedBy = [ "multi-user.target" ]; description = "Receive changes propagated from the KDC master server."; path = with pkgs; [ heimdalFull ]; execStart = concatStringsSep " " [ diff --git a/lib/fudo/ldap.nix b/lib/fudo/ldap.nix index 31ed896..ebb4bab 100644 --- a/lib/fudo/ldap.nix +++ b/lib/fudo/ldap.nix @@ -7,17 +7,23 @@ let user-type = import ../types/user.nix { inherit lib; }; - stringJoin = joiner: attrList: - if (length attrList) == 0 then - "" - else - foldr (lAttr: rAttr: "${lAttr}${joiner}${rAttr}") (last attrList) - (init attrList); + stringJoin = concatStringsSep; getUserGidNumber = user: group-map: group-map.${user.primary-group}.gid; attrOr = attrs: attr: value: if attrs ? ${attr} then attrs.${attr} else value; + ca-path = "${cfg.state-directory}/ca.pem"; + + build-ca-script = target: ca-cert: site-chain: let + user = config.services.openldap.user; + group = config.services.openldap.group; + in pkgs.writeShellScript "build-openldap-ca-script.sh" '' + cat ${site-chain} ${ca-cert} > ${target} + chmod 440 ${target} + chown ${user}:${group} ${target} + ''; + mkHomeDir = username: user-opts: if (user-opts.primary-group == "admin") then "/home/${username}" @@ -72,7 +78,7 @@ let usersLdif = base: group-map: user-map: stringJoin "\n" - (mapAttrsToList (name: opts: userLdif base name group-map opts) user-map); + (mapAttrsToList (name: opts: userLdif base name group-map opts) user-map); in { @@ -103,6 +109,13 @@ in { ''; }; + ssl-chain = mkOption { + type = str; + description = '' + The path to the SSL chain to to the certificate for the server. + ''; + }; + ssl-private-key = mkOption { type = str; description = '' @@ -111,8 +124,7 @@ in { }; ssl-ca-certificate = mkOption { - type = with types; nullOr str; - + type = nullOr str; description = '' The path to the SSL CA cert used to sign the certificate. ''; @@ -189,9 +201,21 @@ in { description = "System users to be added to the Fudo LDAP database."; }; - database-directory = mkOption { + state-directory = mkOption { type = str; - description = "Path at which to store the database."; + description = "Path at which to store openldap database & state."; + }; + + systemd-target = mkOption { + type = str; + description = "Systemd target for running ldap server."; + default = "fudo-ldap-server.target"; + }; + + required-services = mkOption { + type = listOf str; + description = "Systemd services on which the server depends."; + default = [ ]; }; }; }; @@ -214,34 +238,65 @@ in { }; }; - systemd.services.openldap = { - environment = { KRB5_KTNAME = cfg.kerberos-keytab; }; - # FIXME: THIS IS ALL UNPROVEN - serviceConfig = { - PrivateDevices = true; - PrivateTmp = true; - PrivateMounts = true; - ProtectControlGroups = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectSystem = true; - ProtectHostname = true; - ProtectHome = true; - ProtectClock = true; - ProtectKernelLogs = true; - KeyringMode = "private"; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; - AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - Restart = "on-failure"; - LockPersonality = true; - RestrictRealtime = true; - MemoryDenyWriteExecute = true; - SystemCallFilter = - "~@clock @debug @module @mount @raw-io @reboot @swap @privileged @resources @cpu-emulation @obsolete"; - UMask = "7007"; - InaccessiblePaths = [ "/home" "/root" ]; - LimitNOFILE = 49152; - PermissionsStartOnly = true; + networking.firewall = { + allowedTCPPorts = [ 389 636 ]; + allowedUDPPorts = [ 389 ]; + }; + + systemd = { + tmpfiles.rules = let + ca-dir = dirOf ca-path; + user = config.services.openldap.user; + group = config.services.openldap.group; + in [ + "d ${ca-dir} 0700 ${user} ${group} - -" + ]; + + services.openldap = { + partOf = [ cfg.systemd-target ]; + requires = cfg.required-services; + environment.KRB5_KTNAME = cfg.kerberos-keytab; + preStart = mkBefore + "${build-ca-script ca-path + cfg.ssl-chain + cfg.ssl-ca-certificate}"; + serviceConfig = { + PrivateDevices = true; + PrivateTmp = true; + PrivateMounts = true; + ProtectControlGroups = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectSystem = true; + ProtectHostname = true; + ProtectHome = true; + ProtectClock = true; + ProtectKernelLogs = true; + KeyringMode = "private"; + # RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + Restart = "on-failure"; + LockPersonality = true; + RestrictRealtime = true; + MemoryDenyWriteExecute = true; + SystemCallFilter = concatStringsSep " " [ + "~@clock" + "@debug" + "@module" + "@mount" + "@raw-io" + "@reboot" + "@swap" + # "@privileged" + "@resources" + "@cpu-emulation" + "@obsolete" + ]; + UMask = "7007"; + InaccessiblePaths = [ "/home" "/root" ]; + LimitNOFILE = 49152; + PermissionsStartOnly = true; + }; }; }; @@ -254,7 +309,7 @@ in { makeAccessLine = target: perm-map: let perm-entries = mapAttrsToList makePermEntry perm-map; - in "to ${target} ${concatStringsSep " " perm-entries} done"; + in "to ${target} ${concatStringsSep " " perm-entries}"; makeAccess = access-map: let access-lines = mapAttrsToList makeAccessLine; @@ -268,7 +323,7 @@ in { olcPidFile = "/run/slapd/slapd.pid"; olcTLSCertificateFile = cfg.ssl-certificate; olcTLSCertificateKeyFile = cfg.ssl-private-key; - olcTLSCACertificateFile = cfg.ssl-ca-certificate; + olcTLSCACertificateFile = ca-path; olcSaslSecProps = "noplain,noanonymous"; olcAuthzRegexp = let authz-regex-entry = i: { regex, target }: @@ -297,70 +352,74 @@ in { ]; }; children = { - "olcDatabase{-1}frontend" = { + "cn=schema" = { + includes = [ + "${pkgs.openldap}/etc/schema/core.ldif" + "${pkgs.openldap}/etc/schema/cosine.ldif" + "${pkgs.openldap}/etc/schema/inetorgperson.ldif" + "${pkgs.openldap}/etc/schema/nis.ldif" + ]; + }; + "olcDatabase={-1}frontend" = { attrs = { objectClass = [ "olcDatabaseConfig" "olcFrontendConfig" ]; olcDatabase = "{-1}frontend"; olcAccess = makeAccess { "*" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "dn.exact=cn=admin,${cfg.base}" = "manage"; "*" = "none"; }; }; }; }; - "olcDatabase{0}config" = { + "olcDatabase={0}config" = { attrs = { objectClass = [ "olcDatabaseConfig" ]; olcDatabase = "{0}config"; - olcAccess = [ "by * none" ]; + olcAccess = makeAccess { + "*" = { + "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; + "*" = "none"; + }; + }; }; }; - "olcDatabase{1}mdb" = { + "olcDatabase={1}mdb" = { attrs = { objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; olcDatabase = "{1}mdb"; olcSuffix = cfg.base; # olcRootDN = "cn=admin,${cfg.base}"; # olcRootPW = FIXME; # NOTE: this should be hashed... - olcDbDirectory = cfg.database-directory; + olcDbDirectory = "${cfg.state-directory}/database"; olcDbIndex = [ "objectClass eq" "uid eq" ]; olcAccess = makeAccess { "attrs=userPassword,shadowLastChange" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write"; "dn.exact=cn=auth_reader,${cfg.base}" = "read"; "dn.exact=cn=replicator,${cfg.base}" = "read"; "self" = "write"; "*" = "auth"; }; - "dn.base=cn=admin,ou=groups,${cfg.base}" = { + "dn=cn=admin,ou=groups,${cfg.base}" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "dn.exact=cn=admin,ou=groups,${cfg.base}" = "write"; "users" = "read"; "*" = "none"; }; "dn.subtree=ou=groups,${cfg.base} attrs=memberUid" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "dn.exact=cn=admin,ou=groups,${cfg.base}" = "write"; "dn.regex=cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" = "write"; - "group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write"; "users" = "read"; "*" = "none"; }; "dn.subtree=ou=members,${cfg.base} attrs=cn,sn,homeDirectory,loginShell,gecos,description,homeDirectory,uidNumber,gidNumber" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "dn.exact=cn=admin,ou=groups,${cfg.base}" = "write"; - "group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "write"; "dn.exact=cn=user_db_reader,${cfg.base}" = "read"; "users" = "read"; "*" = "none"; }; "*" = { "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "dn.exact=cn=admin,ou=groups,${cfg.base}" = "write"; - "group/groupOfNames/member.exact=cn=admin,ou=groups,${cfg.base}" = "read"; "users" = "read"; "*" = "none"; }; diff --git a/lib/fudo/postgres.nix b/lib/fudo/postgres.nix index 669aa70..8ab72cc 100644 --- a/lib/fudo/postgres.nix +++ b/lib/fudo/postgres.nix @@ -11,6 +11,9 @@ let join-lines = lib.concatStringsSep "\n"; + strip-ext = filename: + head (builtins.match "^(.+)[.][^.]+$" filename); + userDatabaseOpts = { database, ... }: { options = { access = mkOption { @@ -127,33 +130,33 @@ let in { - options.fudo.postgresql = { + options.fudo.postgresql = with types; { enable = mkEnableOption "Fudo PostgreSQL Server"; ssl-private-key = mkOption { - type = types.str; + type = str; description = "Location of the server SSL private key."; }; ssl-certificate = mkOption { - type = types.str; + type = str; description = "Location of the server SSL certificate."; }; keytab = mkOption { - type = types.str; + type = str; description = "Location of the server Kerberos keytab."; }; local-networks = mkOption { - type = with types; listOf str; + type = listOf str; description = "A list of networks from which to accept connections."; example = [ "10.0.0.1/16" ]; default = [ ]; }; users = mkOption { - type = with types; attrsOf (submodule userOpts); + type = attrsOf (submodule userOpts); description = "A map of users to user attributes."; example = { sampleUser = { @@ -170,35 +173,53 @@ in { }; databases = mkOption { - type = with types; attrsOf (submodule databaseOpts); + type = attrsOf (submodule databaseOpts); description = "A map of databases to database options."; default = { }; }; socket-directory = mkOption { - type = types.str; + type = str; description = "Directory in which to place unix sockets."; default = "/run/postgresql"; }; socket-group = mkOption { - type = types.str; + type = str; description = "Group for accessing sockets."; default = "postgres_local"; }; local-users = mkOption { - type = with types; listOf str; + type = listOf str; description = "Users able to access the server via local socket."; default = [ ]; }; required-services = mkOption { - type = with types; listOf str; + type = listOf str; description = "List of services that should run before postgresql."; default = [ ]; example = [ "password-generator.service" ]; }; + + state-directory = mkOption { + type = nullOr str; + description = "Path at which to store database state data."; + default = null; + }; + + cleanup-tasks = mkOption { + type = listOf str; + description = "List of actions to take during shutdown of the service."; + default = []; + }; + + systemd-target = mkOption { + type = str; + description = "Name of the systemd target for postgresql"; + default = "postgresql.target"; + }; }; config = mkIf cfg.enable { @@ -206,28 +227,28 @@ in { environment = { systemPackages = with pkgs; [ postgresql_11_gssapi ]; - etc = { - "postgresql/private/privkey.pem" = { - mode = "0400"; - user = "postgres"; - group = "postgres"; - source = cfg.ssl-private-key; - }; + # etc = { + # "postgresql/private/privkey.pem" = { + # mode = "0400"; + # user = "postgres"; + # group = "postgres"; + # source = cfg.ssl-private-key; + # }; - "postgresql/cert.pem" = { - mode = "0444"; - user = "postgres"; - group = "postgres"; - source = cfg.ssl-certificate; - }; + # "postgresql/cert.pem" = { + # mode = "0444"; + # user = "postgres"; + # group = "postgres"; + # source = cfg.ssl-certificate; + # }; - "postgresql/private/postgres.keytab" = { - mode = "0400"; - user = "postgres"; - group = "postgres"; - source = cfg.keytab; - }; - }; + # "postgresql/private/postgres.keytab" = { + # mode = "0400"; + # user = "postgres"; + # group = "postgres"; + # source = cfg.keytab; + # }; + # }; }; users.groups = { @@ -249,11 +270,11 @@ in { }) opts.users)) cfg.databases))); settings = { - krb_server_keyfile = "/etc/postgresql/private/postgres.keytab"; + krb_server_keyfile = cfg.keytab; ssl = true; - ssl_cert_file = "/etc/postgresql/cert.pem"; - ssl_key_file = "/etc/postgresql/private/privkey.pem"; + ssl_cert_file = cfg.ssl-certificate; + ssl_key_file = cfg.ssl-private-key; unix_socket_directories = cfg.socket-directory; unix_socket_group = cfg.socket-group; @@ -272,12 +293,21 @@ in { # local networks ${makeNetworksEntry cfg.local-networks} ''; + + dataDir = mkIf (cfg.state-directory != null) cfg.state-directory; }; systemd = { - services = { + tmpfiles.rules = optional (cfg.state-directory != null) (let + user = config.systemd.services.postgresql.serviceConfig.User; + in "d ${cfg.state-directory} 0700 ${user} - - -"); + targets.${strip-ext cfg.systemd-target} = { + description = "Postgresql and associated systemd services."; + }; + + services = { postgresql-password-setter = let passwords-script = passwords-setter-script cfg.users; password-wrapper-script = @@ -308,23 +338,32 @@ in { Type = "oneshot"; User = config.services.postgresql.superUser; }; + partOf = [ cfg.systemd-target ]; script = "${password-wrapper-script}"; }; - postgresql.postStart = let - allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;"; + postgresql = { + requires = cfg.required-services; + after = cfg.required-services; + partOf = [ cfg.systemd-target ]; - extra-settings-sql = pkgs.writeText "settings.sql" '' + postStart = let + allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;"; + + extra-settings-sql = pkgs.writeText "settings.sql" '' ${concatStringsSep "\n" - (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))} + (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))} ${usersAccessSql cfg.users} ''; - in '' - ${pkgs.postgresql}/bin/psql --port ${ - toString config.services.postgresql.port - } -d postgres -f ${extra-settings-sql} - ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL* - ''; + in '' + ${pkgs.postgresql}/bin/psql --port ${ + toString config.services.postgresql.port + } -d postgres -f ${extra-settings-sql} + ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL* + ''; + + postStop = concatStringsSep "\n" cfg.cleanup-tasks; + }; }; }; }; diff --git a/lib/instance.nix b/lib/instance.nix index ed87475..c70652b 100644 --- a/lib/instance.nix +++ b/lib/instance.nix @@ -12,6 +12,11 @@ in { description = "Hostname of this specific host (without domain)."; }; + host-fqdn = mkOption { + type = str; + description = "Fully-qualified name of this host."; + }; + build-timestamp = mkOption { type = int; description = "Timestamp associated with the build. Used for e.g. DNS serials."; @@ -98,9 +103,12 @@ in { local-profile = host.profile; + host-fqdn = "${config.instance.hostname}.${local-domain}"; + in { instance = { inherit + host-fqdn local-domain local-site local-users