From 2954dfc1b2745ade6f7be8922327ca6d57cb3eb6 Mon Sep 17 00:00:00 2001 From: niten Date: Wed, 16 Mar 2022 09:49:54 -0700 Subject: [PATCH] Added live disk flake file --- config/domains.nix | 7 + config/domains/sea.fudo.org.nix | 19 ++ config/hardware/wormhole0.nix | 74 ++++++ config/host-config/nutboy3/cashew.nix | 216 ++++++++++++++++++ config/host-config/wormhole0.nix | 42 ++++ config/service/backplane.nix | 188 ++++++++++++++++ config/service/chute.nix | 100 +++++++++ config/service/local-network.nix | 207 +++++++++++++++++ config/service/logging.nix | 231 +++++++++++++++++++ config/service/mail-server.nix | 180 +++++++++++++++ config/service/metrics.nix | 311 ++++++++++++++++++++++++++ config/service/postgresql.nix | 78 +++++++ config/service/selby-forum.nix | 277 +++++++++++++++++++++++ config/service/wireguard-client.nix | 27 +++ config/service/wireguard-gateway.nix | 90 ++++++++ config/service/wireguard.nix | 28 +++ config/services.nix | 18 ++ config/wireguard.nix | 75 +++++++ config/zones.nix | 7 + config/zones/selby.ca.nix | 38 ++++ live-disk/flake.nix | 110 +++++++++ 21 files changed, 2323 insertions(+) create mode 100644 config/domains.nix create mode 100644 config/domains/sea.fudo.org.nix create mode 100644 config/hardware/wormhole0.nix create mode 100644 config/host-config/nutboy3/cashew.nix create mode 100644 config/host-config/wormhole0.nix create mode 100644 config/service/backplane.nix create mode 100644 config/service/chute.nix create mode 100644 config/service/local-network.nix create mode 100644 config/service/logging.nix create mode 100644 config/service/mail-server.nix create mode 100644 config/service/metrics.nix create mode 100644 config/service/postgresql.nix create mode 100644 config/service/selby-forum.nix create mode 100644 config/service/wireguard-client.nix create mode 100644 config/service/wireguard-gateway.nix create mode 100644 config/service/wireguard.nix create mode 100644 config/services.nix create mode 100644 config/wireguard.nix create mode 100644 config/zones.nix create mode 100644 config/zones/selby.ca.nix create mode 100644 live-disk/flake.nix diff --git a/config/domains.nix b/config/domains.nix new file mode 100644 index 0000000..80c171b --- /dev/null +++ b/config/domains.nix @@ -0,0 +1,7 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./domains/sea.fudo.org.nix + ]; +} diff --git a/config/domains/sea.fudo.org.nix b/config/domains/sea.fudo.org.nix new file mode 100644 index 0000000..e07bd19 --- /dev/null +++ b/config/domains/sea.fudo.org.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +let + fudo = config.fudo.domains."fudo.org"; +in { + config.fudo.domains."sea.fudo.org" = { + local-networks = fudo.local-networks; + + gssapi-realm = fudo.gssapi-realm; + kerberos-master = fudo.kerberos-master; + kerberos-slaves = fudo.kerberos-slaves; + + primary-mailserver = fudo.primary-mailserver; + + ldap-servers = fudo.ldap-servers; + + xmpp-servers = fudo.xmpp-servers; + }; +} diff --git a/config/hardware/wormhole0.nix b/config/hardware/wormhole0.nix new file mode 100644 index 0000000..103824a --- /dev/null +++ b/config/hardware/wormhole0.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, modulesPath, ... }: + +with lib; { + system.stateVersion = "22.05"; + + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot = { + initrd = { + availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" ]; + kernelModules = [ ]; + }; + loader = { + grub.enable = false; + # generic-extlinux-compatible.enable = true; + raspberryPi = { + enable = true; + version = 4; + }; + }; + + tmpOnTmpfs = true; + + kernelModules = [ ]; + kernelPackages = pkgs.linuxPackages_rpi4; + kernelParams = [ + "8250.nr_uarts=1" + "console=ttyAMA0,115200" + "console=tty1" + ]; + + extraModulePackages = [ ]; + }; + + hardware = { + enableRedistributableFirmware = true; + # raspberry-pi."4".fkms-3d.enable = true; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + + "/boot" = { + device = "/dev/disk/by-label/FIRMWARE"; + fsType = "vfat"; + options = [ "noatime" ]; + }; + }; + + swapDevices = [ ]; + + networking = { + useDHCP = mkDefault false; + + macvlans = { + intif0 = { + interface = "eth0"; + mode = "bridge"; + }; + }; + + interfaces = { + eth0.useDHCP = false; + intif0.macAddress = "02:fa:d4:07:cf:f4"; + }; + }; + + powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; + +} diff --git a/config/host-config/nutboy3/cashew.nix b/config/host-config/nutboy3/cashew.nix new file mode 100644 index 0000000..dbf2f4c --- /dev/null +++ b/config/host-config/nutboy3/cashew.nix @@ -0,0 +1,216 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + parent-config = config; + + host-ipv4 = "199.87.154.175"; + + local-packages = with pkgs; [ + bind + emacs-nox + mtr + vim + ]; + + fudo-zone = pkgs.lib.dns.zoneToZonefile + config.instance.build-timestamp "fudo.org" + config.fudo.zones."fudo.org"; + + selby-zone = pkgs.lib.dns.zoneToZonefile + config.instance.build-timestamp "selby.ca" + config.fudo.zones."selby.ca"; + +in { + environment.etc = { + "generated-zones/fudo.org".text = fudo-zone; + "generated-zones/selby.ca".text = selby-zone; + }; + + fudo = { + services.dns.zones = let + in { + "fudo.org" = { + enable = true; + external-nameservers = [ + { + ipv4-address = "209.177.102.102"; + ipv6-address = "2001:470:1f16:40::2"; + description = "Nameserver 2, Musashi.100percenthost.net, in Winnipeg, MB, CA"; + } + { + ipv4-address = "104.131.53.95"; + ipv6-address = "2604:a880:800:10::8:7001"; + description = "Nameserver 3, ns2.henchmman21.net, in New York City, NY, US"; + } + { + ipv4-address = "204.42.254.5"; + ipv6-address = "2001:418:3f4::5"; + description = "Nameserver 4, puck.nether.net, in Chicago, IL, US"; + } + ]; + }; + "selby.ca" = { + enable = true; + external-nameservers = map (n: let + i = toString n; + in { + authoritative-hostname = "ns${i}.fudo.org"; + description = "Nameserver ${i}, ns${i}.fudo.org."; + }) [2 3 4]; + }; + }; + + domains."selby.ca" = { + local-networks = config.fudo.domains."fudo.org".local-networks; + }; + + zones = { + "fudo.org" = { + default-host = host-ipv4; + verbatim-dns-records = [ + # TODO: create these automatically + "node._metrics._tcp IN SRV 0 0 443 france.fudo.org." + "node._metrics._tcp IN SRV 0 0 9900 hanover.fudo.org." + "node._metrics._tcp IN SRV 0 0 443 paris.fudo.org." + + "node._metrics._tcp IN SRV 0 0 443 legatus.fudo.org." + "node._metrics._tcp IN SRV 0 0 443 nutboy3.fudo.org." + + "dovecot._metrics._tcp IN SRV 0 0 443 mail.fudo.org." + "postfix._metrics._tcp IN SRV 0 0 443 mail.fudo.org." + "rspamd._metrics._tcp IN SRV 0 0 443 mail.fudo.org." + ]; + }; + "selby.ca" = { + default-host = host-ipv4; + }; + }; + }; + + containers.cashew = { + autoStart = true; + + bindMounts = { + "/state" = { + hostPath = "/state/cashew"; + isReadOnly = false; + }; + "/etc/bind" = { + hostPath = "/state/cashew/bind"; + isReadOnly = false; + }; + "/var/log" = { + hostPath = "/state/cashew/logs"; + isReadOnly = false; + }; + "/home" = { + hostPath = "/state/cashew/home"; + isReadOnly = false; + }; + "/etc/dns-root-data" = { + hostPath = "${pkgs.dns-root-data}/"; + isReadOnly = true; + }; + }; + + interfaces = [ "eno2" ]; + + config = { config, ... }: { + nixpkgs.pkgs = pkgs; + + environment = { + systemPackages = local-packages; + etc = { + "generated-zones/fudo.org" = { + text = fudo-zone; + }; + "generated-zones/selby.ca" = { + text = selby-zone; + }; + }; + }; + + users = { + users = { + niten = parent-config.users.users.niten; + reaper = parent-config.users.users.reaper // { + openssh.authorizedKeys.keys = [ + "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADtR1gMK7JnIOht8yZNPROr+0VHgt5eWrGFPscVPk1crVuEvIv1MF544Qk1IHi+2OA2xUvI1BTgmXp3TLvCjEn4lQF4Uc5hcUGENS6TNMPByHx69rAeXVMtmjW0sL4Tbhqd0iNh85STdtzXNZUY31+A6ugrJSnvnSt5wv9ZpMz0SFIE1Q==" + ]; + }; + root.openssh.authorizedKeys.keys = [ + "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADtR1gMK7JnIOht8yZNPROr+0VHgt5eWrGFPscVPk1crVuEvIv1MF544Qk1IHi+2OA2xUvI1BTgmXp3TLvCjEn4lQF4Uc5hcUGENS6TNMPByHx69rAeXVMtmjW0sL4Tbhqd0iNh85STdtzXNZUY31+A6ugrJSnvnSt5wv9ZpMz0SFIE1Q==" + ]; + }; + groups = { + wheel.members = [ + "niten" + "reaper" + ]; + dns = { + members = [ + "niten" + "reaper" + "named" + ]; + }; + }; + }; + + networking = { + defaultGateway = { + address = "208.81.4.81"; + interface = "eno2"; + }; + + interfaces.eno2 = { + ipv4.addresses = [ + { + address = "208.81.4.82"; + prefixLength = 29; + } + { + address = "208.81.1.141"; + prefixLength = 32; + } + ]; + }; + + firewall.enable = false; + }; + + # /etc/bind ended up not belonging to the correct user/group + systemd.services.bind-perms = { + requiredBy = [ "bind.service" ]; + before = [ "bind.service" ]; + script = "chown -R named:named /etc/bind"; + }; + + services = { + bind = { + enable = true; + configFile = "/etc/bind/named.conf"; + }; + + openssh = { + enable = true; + startWhenNeeded = true; + useDns = true; + permitRootLogin = "prohibit-password"; + hostKeys = [ + { + path = "/state/ssh/ssh_host_ed25519_key"; + type = "ed25519"; + } + { + path = "/state/ssh/ssh_host_rsa_key"; + type = "rsa"; + bits = 4096; + } + ]; + }; + }; + }; + }; +} diff --git a/config/host-config/wormhole0.nix b/config/host-config/wormhole0.nix new file mode 100644 index 0000000..01a67ea --- /dev/null +++ b/config/host-config/wormhole0.nix @@ -0,0 +1,42 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + primary-ip = "10.0.0.3"; + +in { + networking = { + hostName = "wormhole0"; + + firewall.enable = false; + + defaultGateway = { + address = "10.0.0.1"; + interface = "intif0"; + }; + + nameservers = [ "10.0.0.1" ]; + + interfaces = { + intif0 = { + ipv4.addresses = [{ + address = primary-ip; + prefixLength = 24; + }]; + }; + + wlan0.useDHCP = true; + }; + }; + + nix = { + # settings = { + # auto-optimise-store = true; + # }; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + }; +} diff --git a/config/service/backplane.nix b/config/service/backplane.nix new file mode 100644 index 0000000..cff0454 --- /dev/null +++ b/config/service/backplane.nix @@ -0,0 +1,188 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + domain-name = config.fudo.hosts.${hostname}.domain; + domain = config.fudo.domains.${domain-name}; + zone-name = config.fudo.domains.${domain-name}.zone; + + host-fqdn = hostname: "${hostname}.${config.fudo.hosts.${hostname}.domain}"; + + postgresql-server = domain.postgresql-server; + + isDatabase = hostname == postgresql-server; + isJabber = elem hostname domain.xmpp-servers; + isDNSBackplane = hostname == domain.backplane.dns-service; + backplaneEnabled = domain.backplane != null; + isNameserver = hostname == domain.backplane.nameserver; + + database-name = "backplane_dns"; + + make-passwd-file = hostname: let + name = "backplane-host-${hostname}-client-passwd"; + seed = "${name}-${config.instance.build-seed}"; + in pkgs.lib.passwd.stablerandom-passwd-file name seed; + + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + host-password-files = mapAttrs (hostname: hostOpts: + make-passwd-file hostname) config.fudo.hosts; + + backplane-user = "backplane_dns"; + database-backplane-user = "backplane_dns"; + database-powerdns-user = "backplane_powerdns_dns"; + + backplane-host-domain = config.fudo.hosts.${domain.backplane.dns-service}.domain; + backplane-server = head config.fudo.domains.${backplane-host-domain}.xmpp-servers; + backplane-host-fqdn = "${backplane-server}.${backplane-host-domain}"; + backplane-fqdn = "backplane.${backplane-host-domain}"; + + +in { + config = mkIf backplaneEnabled { + + fudo = let + powerdns-password = pkgs.lib.passwd.stablerandom-passwd-file + "backplane-powerdns-passwd-${postgresql-server}" + "backplane-powerdns-passwd-${postgresql-server}-${config.instance.build-seed}"; + backplane-database-password = pkgs.lib.passwd.stablerandom-passwd-file + "backplane-passwd-${postgresql-server}" + "backplane-passwd-${postgresql-server}-${config.instance.build-seed}"; + xmpp-password = pkgs.lib.passwd.stablerandom-passwd-file + "backplane-xmpp-passwd-${postgresql-server}" + "backplane-xmpp-passwd-${postgresql-server}-${config.instance.build-seed}"; + in { + secrets.host-secrets.${hostname} = { + powerdns-database-passwd = mkIf isNameserver { + source-file = powerdns-password; + target-file = "/run/backplane-powerdns/powerdns.passwd"; + user = config.fudo.powerdns.user; + }; + + backplane-database-passwd = mkIf isDNSBackplane { + source-file = backplane-database-password; + target-file = "/run/backplane-dns/database.passwd"; + user = config.fudo.backplane.dns.user; + }; + backplane-xmpp-passwd = mkIf isDNSBackplane { + source-file = xmpp-password; + target-file = "/run/backplane-dns/xmpp.passwd"; + user = config.fudo.backplane.dns.user; + }; + + database-powerdns-passwd = mkIf isDatabase { + source-file = powerdns-password; + target-file = "/run/postgres/powerdns.passwd"; + user = config.services.postgresql.superUser; + }; + database-backplane-passwd = mkIf isDatabase { + source-file = backplane-database-password; + target-file = "/run/postgres/backplane-database.passwd"; + user = config.services.postgresql.superUser; + }; + + ejabberd-backplane-passwd = mkIf isJabber { + source-file = xmpp-password; + target-file = "/run/backplane-jabber/service-dns.passwd"; + user = config.services.ejabberd.user; + }; + + backplane-client-passwd = { + source-file = host-password-files.${hostname}; + target-file = "/run/backplane-client/client.passwd"; + user = config.fudo.client.dns.user; + }; + }; + + client.dns = { + password-file = host-secrets.backplane-client-passwd.target-file; + domain = domain.backplane.domain; + }; + + zones.${zone-name} = { + aliases = { + backplane = "${backplane-host-fqdn}."; + }; + }; + + postgresql = mkIf isDatabase { + required-services = [ "fudo-passwords.target" ]; + + users = { + ${database-powerdns-user} = { + password-file = host-secrets.database-powerdns-passwd.target-file; + databases.${database-name} = { + access = "CONNECT"; + entity-access = { + "ALL TABLES IN SCHEMA public" = "SELECT,INSERT,UPDATE,DELETE"; + "ALL SEQUENCES IN SCHEMA public" = "SELECT,UPDATE"; + }; + }; + }; + ${database-backplane-user} = { + password-file = host-secrets.database-backplane-passwd.target-file; + databases.${database-name} = { + access = "CONNECT"; + entity-access = { + "ALL TABLES IN SCHEMA public" = "SELECT,INSERT,UPDATE,DELETE"; + "ALL SEQUENCES IN SCHEMA public" = "SELECT,UPDATE"; + }; + }; + }; + }; + + databases.${database-name}.users = config.instance.local-admins; + }; + + backplane = { + enable = isJabber; + + client-hosts = mapAttrs (hostname: hostOpts: { + password-file = host-password-files.${hostname}; + }) config.fudo.hosts; + + services = { + dns.password-file = host-secrets.ejabberd-backplane-passwd.source-file; + }; + + backplane-hostname = backplane-fqdn; + + dns = mkIf isDNSBackplane { + enable = true; + database = { + host = pkgs.lib.network.host-ipv4 config postgresql-server; + database = database-name; + username = database-backplane-user; + password-file = host-secrets.backplane-database-passwd.target-file; + }; + backplane-role = { + role = "service-dns"; + password-file = host-secrets.backplane-xmpp-passwd.target-file; + }; + }; + }; + + powerdns = mkIf (isNameserver) { + enable = true; + domains = let + served-domain = domain.backplane.domain; + in { + ${served-domain}.admin = domain.admin-email; + }; + listen-v4-addresses = let + ipv4-addr = pkgs.lib.network.host-ipv4 config hostname; + in [ ipv4-addr ]; + listen-v6-addresses = let + ipv6-addr = pkgs.lib.network.host-ipv6 config hostname; + in optional (ipv6-addr != null) ipv6-addr; + database = { + host = pkgs.lib.network.host-ipv4 config postgresql-server; + database = database-name; + user = database-powerdns-user; + password-file = host-secrets.powerdns-database-passwd.target-file; + }; + }; + }; + }; +} diff --git a/config/service/chute.nix b/config/service/chute.nix new file mode 100644 index 0000000..25fdfa4 --- /dev/null +++ b/config/service/chute.nix @@ -0,0 +1,100 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + cfg = config.informis.services.chute; + + make-env-file = { hostname, secret, passphrase, key, jabber-password ? null }: + pkgs.writeText "chute-environment" '' + COINBASE_API_HOSTNAME=${hostname} + COINBASE_API_SECRET=${secret} + COINBASE_API_PASSPHRASE=${passphrase} + COINBASE_API_KEY=${key} + ${optionalString (jabber-password != null) + "JABBER_PASSWORD=${jabber-password}"} + ''; + + read-and-trim = filename: + removeSuffix "\n" (readFile filename); + +in { + options.informis.services.chute = with types; { + enable = mkEnableOption "Enable the Chute cryptocurrency stopgap."; + + jabber-user = mkOption { + type = nullOr str; + description = "User to which messages will be sent."; + default = null; + }; + + staging = { + secret-file = mkOption { + type = path; + description = "Path to file containing Coinbase API secret."; + }; + + key-file = mkOption { + type = path; + description = "Path to file containing Coinbase API key."; + }; + + passphrase-file = mkOption { + type = path; + description = "Path to file containing Coinbase API passphrase."; + }; + }; + }; + + config = let + chute-jabber-passwd = + pkgs.lib.passwd.stablerandom-passwd-file + "chute-jabber-passwd" + "chute-jabber-passwd-${config.instance.build-seed}"; + in { + fudo = { + users.chute = { + uid = 11007; + primary-group = "informis"; + common-name = "Chute Cryptocurrency Trader"; + ldap-hashed-passwd = + pkgs.lib.passwd.hash-ldap-passwd "chute-jabber-passwd-ldaphash" + chute-jabber-passwd; + }; + + secrets.host-secrets.${hostname}.chute-staging-environment = + mkIf cfg.enable { + source-file = make-env-file { + hostname = "api-public.sandbox.exchange.coinbase.com"; + secret = read-and-trim cfg.staging.secret-file; + passphrase = read-and-trim cfg.staging.passphrase-file; + key = read-and-trim cfg.staging.key-file; + jabber-password = read-and-trim chute-jabber-passwd; + }; + target-file = "/run/chute/staging.env"; + user = "root"; + }; + }; + + informis.chute = { + enable = cfg.enable; + stages = { + staging = mkIf cfg.enable { + package = pkgs.chuteUnstable; + environment-file = + host-secrets.chute-staging-environment.target-file; + jabber = { + jid = "chute@jabber.fudo.org"; + target-jid = cfg.jabber-user; + resource = "procul-staging"; + }; + currencies = { + btc.stop-percentile = 98; + }; + }; + }; + }; + }; +} diff --git a/config/service/local-network.nix b/config/service/local-network.nix new file mode 100644 index 0000000..c72ba1e --- /dev/null +++ b/config/service/local-network.nix @@ -0,0 +1,207 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + + domain-name = config.fudo.hosts.${hostname}.domain; + site-name = config.fudo.hosts.${hostname}.site; + zone-name = config.fudo.domains.${domain-name}.zone; + + site = config.fudo.sites.${site-name}; + + cfg = config.fudo.services.local-network; + + resolverOpts = { + options = { + ip = mkOption { + type = str; + description = "IP address of the upstream recursive resolver."; + default = "1.1.1.1"; + }; + + port = mkOption { + type = port; + description = "Port of DNS server on the recursive resolver."; + default = 53; + }; + }; + }; + +in { + options.fudo.services.local-network = with types; { + enable = mkEnableOption "Enable local network server."; + + internal-interfaces = mkOption { + type = listOf str; + description = '' + Interfaces on which to: + + * Accept NAT traffic + * Serve DNS + * Serve DHCP + ''; + }; + + external-interface = mkOption { + type = str; + description = "Interface facing the larger internet."; + example = "extif0"; + }; + + resolvers = mkOption { + type = listOf (submodule resolverOpts); + description = "List of upstream DNS servers."; + default = [ + { ip = "1.1.1.1"; } + { ip = "1.0.0.1"; } + { ip = "9.9.9.9"; } + { ip = "149.112.112.112"; } + ]; + }; + + dns-filter-proxy = { + enable = mkEnableOption "Enable DNS filter."; + + http-listen-port = mkOption { + type = port; + description = "Port on localhost on which to listen for HTTP requests."; + default = 4080; + }; + + http-host-alias = mkOption { + type = str; + description = "Host alias for the DNS filter server."; + default = "dns-filter"; + }; + + dns-listen-port = mkOption { + type = port; + description = "Port on localhost on which to listen for DNS requests."; + default = 4053; + }; + + upstream-dns = mkOption { + type = listOf str; + description = "List of upstream DNS-over-HTTPS endpoints."; + default = [ + "https://1.1.1.1/dns-query" + "https://1.0.0.1/dns-query" + # These 11 addrs send the network, so the response can prefer closer answers + "https://9.9.9.11/dns-query" + "https://149.112.112.11/dns-query" + "https://2620:fe::11/dns-query" + "https://2620:fe::fe:11/dns-query" + ]; + }; + }; + }; + + config = mkIf (site.local-gateway != null) (let + host-ipv4 = pkgs.lib.network.host-ipv4 config; + gateway-host = site.local-gateway; + nameserver-host = gateway-host; + gateway-ip = host-ipv4 gateway-host; + nameserver-ip = host-ipv4 gateway-host; + is-gateway = hostname == gateway-host; + agp = cfg.dns-filter-proxy; + fqdn = hostname: "${hostname}.${domain-name}."; + in { + networking = { + nat = mkIf is-gateway { + enable = true; + externalInterface = cfg.external-interface; + internalInterfaces = cfg.internal-interfaces; + }; + + nameservers = [ nameserver-ip ]; + + firewall = if is-gateway then { + enable = true; + trustedInterfaces = cfg.internal-interfaces; + } else { + enable = false; + }; + }; + + fudo = { + adguard-dns-proxy = mkIf agp.enable { + enable = true; + http = { + listen-ip = "127.0.0.1"; + listen-port = agp.http-listen-port; + }; + dns = { + listen-ips = [ "127.0.0.1" ]; + listen-port = agp.dns-listen-port; + }; + local-domain-name = domain-name; + }; + + zones.${zone-name} = { + aliases = { + ${agp.http-host-alias} = optionalAttrs (agp.enable) + (fqdn gateway-host); + ns = (fqdn nameserver-host); + gw = (fqdn gateway-host); + }; + + hosts = { + gateway.ipv4-address = gateway-ip; + nameserver.ipv4-address = nameserver-ip; + }; + + nameservers = [ + "nameserver" + ]; + + srv-records = { + tcp.domain = [{ + host = "nameserver.${domain-name}"; + port = 53; + }]; + udp.domain = [{ + host = "nameserver.${domain-name}"; + port = 53; + }]; + }; + }; + + local-network = { + enable = is-gateway; + domain = domain-name; + dns-servers = [ nameserver-ip ]; + gateway = gateway-ip; + dhcp-interfaces = cfg.internal-interfaces; + dns-listen-ips = optionals is-gateway [ nameserver-ip "127.0.0.1" "127.0.1.1" ]; + dns-listen-ipv6s = optionals (is-gateway && config.networking.enableIPv6) [ "::1" ]; + recursive-resolver = if agp.enable then { + host = "127.0.0.1"; + port = agp.dns-listen-port; + } else { + host = cfg.resolver.ip; + port = cfg.resolver.port; + }; + network = site.network; + dhcp-dynamic-network = site.dynamic-network; + search-domains = [ domain-name "fudo.org" ]; + enable-reverse-mappings = true; + zone-definition = config.fudo.zones.${zone-name}; + }; + }; + + services.nginx = mkIf agp.enable { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + + virtualHosts = { + "${agp.http-host-alias}.${domain-name}" = { + locations."/".proxyPass = + "http://127.0.0.1:${toString agp.http-listen-port}"; + }; + }; + }; + }); +} diff --git a/config/service/logging.nix b/config/service/logging.nix new file mode 100644 index 0000000..7dcddae --- /dev/null +++ b/config/service/logging.nix @@ -0,0 +1,231 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + domain-name = config.fudo.hosts.${hostname}.domain; + domain = config.fudo.domains.${domain-name}; + + host-fqdn = hostname: let + host-domain = config.fudo.hosts.${hostname}.domain; + in "${hostname}.${host-domain}"; + + logAggregationEnabled = domain.log-aggregator != null; + isLogAggregator = hostname == domain.log-aggregator; + + is-private-network = let + site-name = config.fudo.hosts.${hostname}.site; + in config.fudo.sites.${site-name}.local-gateway != null; + + cfg = config.fudo.services.logging; + +in { + options.fudo.services.logging = with types; { + loki = { + port = mkOption { + type = port; + description = "Port on which to listen on localhost."; + default = 3021; + }; + + state-directory = mkOption { + type = str; + description = "Path at which to store state for Loki."; + }; + }; + + promtail = { + http-listen-port = mkOption { + type = port; + default = 6041; + }; + grpc-listen-port = mkOption { + type = port; + default = 6042; + }; + user = mkOption { + type = str; + description = "User as which to run promtail job."; + default = "promtail"; + }; + }; + }; + + config = mkIf logAggregationEnabled { + users = { + users.${cfg.promtail.user} = { + isSystemUser = true; + uid = 441; + group = cfg.promtail.user; + }; + groups = { + ${cfg.promtail.user}.members = [ cfg.promtail.user ]; + systemd-journal = { + members = [ cfg.promtail.user ]; + }; + }; + }; + + fudo = let + aggregator-domain = config.fudo.hosts.${domain.log-aggregator}.domain; + log-aggregator-fqdn = "${domain.log-aggregator}.${aggregator-domain}"; + in { + zones.${domain.zone} = { + aliases.log-aggregator = "${log-aggregator-fqdn}."; + }; + + metrics.grafana.datasources = let + scheme = if is-private-network then "http" else "https"; + in { + "loki-${domain.log-aggregator}" = { + url = "${scheme}://log-aggregator.${aggregator-domain}"; + type = "loki"; + }; + }; + }; + + services = { + nginx = mkIf isLogAggregator { + enable = true; + + virtualHosts."log-aggregator.${domain-name}" = { + enableACME = ! is-private-network; + forceSSL = ! is-private-network; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.loki.port}"; + extraConfig = let + local-networks = config.instance.local-networks; + in "${optionalString ((length local-networks) > 0) + (concatStringsSep "\n" + (map (network: "allow ${network};") local-networks)) + "\ndeny all;"}"; + }; + }; + }; + + loki = mkIf isLogAggregator { + enable = true; + dataDir = cfg.loki.state-directory; + # cargo-culted from https://gist.github.com/Xe/c3c786b41ec2820725ee77a7af551225 + configuration = { + auth_enabled = false; + + server.http_listen_port = cfg.loki.port; + + ingester = { + lifecycler = { + address = "127.0.0.1"; + ring = { + kvstore.store = "inmemory"; + replication_factor = 1; + }; + final_sleep = "0s"; + }; + chunk_idle_period = "1h"; + max_chunk_age = "1h"; + chunk_target_size = 10485576; + chunk_retain_period = "30s"; + max_transfer_retries = 0; + }; + + compactor = { + working_directory = "${cfg.loki.state-directory}/compactor"; + }; + + schema_config.configs = [ + { + from = "2022-01-01"; + store = "boltdb-shipper"; + object_store = "filesystem"; + schema = "v11"; + index = { + prefix = "index_"; + period = "24h"; + }; + } + ]; + + storage_config = { + boltdb_shipper = { + active_index_directory = "${cfg.loki.state-directory}/boltdb-shipper/active"; + cache_location = "${cfg.loki.state-directory}/boltdb-shipper/cache"; + cache_ttl = "24h"; + shared_store = "filesystem"; + }; + filesystem.directory = "${cfg.loki.state-directory}/chunks"; + }; + + limits_config = { + reject_old_samples = true; + reject_old_samples_max_age = "168h"; + }; + + chunk_store_config.max_look_back_period = "0s"; + + table_manager = { + retention_deletes_enabled = false; + retention_period = "0s"; + }; + }; + }; + }; + + systemd = { + tmpfiles.rules = mkIf isLogAggregator (let + user = config.services.loki.user; + statedir = cfg.loki.state-directory; + in [ + "d ${statedir} 0700 ${user} - - -" + "d ${statedir}/boltdb-shipper 0700 ${user} - - -" + "d ${statedir}/boltdb-shipper/active 0700 ${user} - - -" + "d ${statedir}/boltdb-shipper/cache 0700 ${user} - - -" + "d ${statedir}/chunks 0700 ${user} - - -" + "d ${statedir}/compactor 0700 ${user} - - -" + ]); + + services.promtail = let + scheme = if is-private-network then "http" else "https"; + + loki-host = host-fqdn domain.log-aggregator; + + config = builtins.toJSON { + server = { + http_listen_port = cfg.promtail.http-listen-port; + grpc_listen_port = cfg.promtail.grpc-listen-port; + }; + positions.filename = "/tmp/positions.yml"; + clients = [ + { url = "${scheme}://log-aggregator.${domain-name}/loki/api/v1/push"; } + ]; + scrape_configs = [ + { + job_name = "journal"; + journal = { + max_age = "12h"; + labels = { + job = "systemd-journal"; + host = hostname; + }; + }; + relabel_configs = [{ + source_labels = [ "__journal__systemd_unit" ]; + target_label = "unit"; + }]; + } + ]; + }; + + config-file = pkgs.writeText "promtail-config.json" config; + in { + description = "PromTail log aggregator client for Loki."; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${pkgs.grafana-loki}/bin/promtail --config.file ${config-file} + ''; + User = cfg.promtail.user; + PrivateTmp = true; + }; + }; + }; + }; +} diff --git a/config/service/mail-server.nix b/config/service/mail-server.nix new file mode 100644 index 0000000..4dde2c8 --- /dev/null +++ b/config/service/mail-server.nix @@ -0,0 +1,180 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.fudo.services.mail-server; + + hostname = config.instance.hostname; + domain-name = config.instance.local-domain; + domain = config.fudo.domains.${domain-name}; + + mailserver-host = domain.primary-mailserver; + mailserver-domain-name = config.fudo.hosts.${mailserver-host}.domain; + mailserver-domain = config.fudo.domains.${mailserver-domain-name}; + + mailserver-host-fqdn = "${mailserver-host}.${mailserver-domain-name}"; + + isMailServer = hostname == mailserver-host; + + isLocalMailserver = domain-name == mailserver-domain-name; + + metricsEnabled = mailserver-domain.prometheus-hosts != []; + + host-certs = config.fudo.acme.host-domains.${hostname}; + +in { + options.fudo.services.mail-server = with types; { + debug = mkEnableOption "Enable debug options for mailserver."; + + state-directory = mkOption { + type = str; + description = "Directory at which to store mailserver state."; + }; + }; + + config = { + services.nginx = mkIf (isMailServer && metricsEnabled) { + enable = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + + virtualHosts."mail-stats.${mailserver-domain-name}" = let + trusted-networks = config.instance.local-networks; + trustedNetworkString = optionalString (length trusted-networks > 0) + (concatStringsSep "\n" + (map (network: "allow ${network};") + trusted-networks)) + "\n\ndeny all;"; + in { + enableACME = true; + forceSSL = true; + + locations = let + monitor-cfg = config.fudo.mail-server.monitoring; + in { + "/metrics/dovecot" = { + proxyPass = "http://127.0.0.1:${toString monitor-cfg.dovecot-listen-port}/metrics"; + extraConfig = trustedNetworkString; + }; + "/metrics/postfix" = { + proxyPass = "http://127.0.0.1:${toString monitor-cfg.postfix-listen-port}/metrics"; + extraConfig = trustedNetworkString; + }; + "/metrics/rspamd" = { + proxyPass = "http://127.0.0.1:${toString monitor-cfg.rspamd-listen-port}/metrics"; + extraConfig = trustedNetworkString; + }; + }; + }; + }; + + fudo = { + acme.host-domains = mkIf isMailServer { + ${hostname} = { + "imap.${mailserver-domain-name}" = { + admin-email = "admin@${mailserver-domain-name}"; + local-copies.dovecot = { + user = config.services.dovecot2.user; + dependent-services = [ "dovecot2.services" ]; + }; + }; + "smtp.${mailserver-domain-name}" = { + admin-email = "admin@${mailserver-domain-name}"; + local-copies.postfix = { + user = config.services.postfix.user; + dependent-services = [ "postfix.services" ]; + }; + }; + }; + }; + + zones = mkIf isLocalMailserver { + ${mailserver-domain.zone} = let + server-ipv4 = pkgs.lib.network.host-ipv4 config mailserver-host; + server-ipv6 = pkgs.lib.network.host-ipv6 config mailserver-host; + + srv-record = host: port: [{ + host = "${host}.${mailserver-domain-name}"; + port = port; + }]; + + in { + hosts = genAttrs [ "imap" "smtp" ] (alias: { + ipv4-address = server-ipv4; + ipv6-address = server-ipv6; + description = "Primary ${toUpper alias} server for ${mailserver-domain-name}."; + }); + + mx = [ "smtp.${mailserver-domain-name}" ]; + + aliases = mkIf metricsEnabled { + mail-stats = "${mailserver-host-fqdn}."; + }; + + srv-records.tcp = { + pop3 = srv-record "imap" 110; + pop3s = srv-record "imap" 995; + + imap = srv-record "imap" 143; + imaps = srv-record "imap" 993; + + smtp = srv-record "smtp" 25; + submission = srv-record "smtp" 587; + }; + + metric-records = mkIf metricsEnabled + (genAttrs [ "dovecot" "postfix" "rspamd" ] + (_: srv-record "mail-stats" 443)); + }; + }; + + metrics.prometheus.service-discovery-dns = mkIf metricsEnabled + (genAttrs [ "dovecot" "postfix" "rspamd" ] + (mtype: [ "${mtype}._metrics._tcp.${mailserver-domain-name}" ])); + + mail-server = mkIf isLocalMailserver { + enable = isMailServer; + + domain = mailserver-domain-name; + mail-hostname = "smtp.${mailserver-domain-name}"; + monitoring.enable = metricsEnabled; + + debug = cfg.debug; + + clamav.enable = true; + + dkim.signing = true; + + dovecot = let + cert-copy = host-certs."imap.${mailserver-domain-name}".local-copies.dovecot; + in { + ssl-certificate = cert-copy.full-certificate; + ssl-private-key = cert-copy.private-key; + }; + + postfix = let + cert-copy = host-certs."smtp.${mailserver-domain-name}".local-copies.postfix; + in { + ssl-certificate = cert-copy.full-certificate; + ssl-private-key = cert-copy.private-key; + }; + + local-domains = [ mailserver-host-fqdn "smtp.${mailserver-domain-name}" ]; + + mail-directory = "${cfg.state-directory}/mail"; + state-directory = "${cfg.state-directory}/state"; + + trusted-networks = config.instance.local-networks; + + alias-users = genAttrs [ + "root" + "postmaster" + "hostmaster" + "webmaster" + "system" + "admin" + "dmarc-report" + ] (alias: config.instance.local-admins); + }; + }; + }; +} diff --git a/config/service/metrics.nix b/config/service/metrics.nix new file mode 100644 index 0000000..4af7c21 --- /dev/null +++ b/config/service/metrics.nix @@ -0,0 +1,311 @@ +{ config, lib, pkgs, ... }@toplevel: + +with lib; +let + hostname = config.instance.hostname; + domain-name = config.fudo.hosts.${hostname}.domain; + domain = config.fudo.domains.${domain-name}; + + pthru = obj: builtins.trace "TRACE(${hostname}): ${toString obj}" obj; + + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + notEmpty = lst: (length lst) > 0; + + metricsEnabled = notEmpty domain.prometheus-hosts; + metricsScraper = elem hostname domain.prometheus-hosts; + metricsMonitor = elem hostname domain.grafana-hosts; + + prometheus-cfg = config.fudo.services.metrics.prometheus; + grafana-cfg = config.fudo.services.metrics.grafana; + + host-fqdn = hostname: + let host-domain = config.fudo.hosts.${hostname}.domain; + in "${hostname}.${host-domain}"; + + host-auth-fqdn = hostname: "${host-fqdn hostname}."; + + make-alias-map = type: hosts: + listToAttrs + (imap0 (i: hostname: nameValuePair hostname "${type}-${toString i}") hosts); + + headOrNull = lst: if notEmpty lst then head lst else null; + + metrics-master = headOrNull domain.prometheus-hosts; + + monitor-master = headOrNull domain.grafana-hosts; + + metrics-alias-map = make-alias-map "metrics" domain.prometheus-hosts; + + monitor-alias-map = make-alias-map "monitor" domain.grafana-hosts; + + alias-map-to-cnames = + mapAttrs' (hostname: alias: nameValuePair alias (host-auth-fqdn hostname)); + + alias-map-to-hostnames = + mapAttrsToList (hostname: alias: "${alias}.${domain-name}"); + + grafana-smtp-password-file = + pkgs.lib.passwd.stablerandom-passwd-file "grafana-smtp-passwd" + "grafana-smtp-passwd-${config.instance.build-seed}"; + + grafana-auth-password-file = + pkgs.lib.passwd.stablerandom-passwd-file "grafana-auth-passwd" + "grafana-auth-passwd-${config.instance.build-seed}"; + + grafana-admin-password-file = + pkgs.lib.passwd.stablerandom-passwd-file "grafana-admin-passwd" + "grafana-admin-passwd-${config.instance.build-seed}"; + + grafana-secret-key-file = + pkgs.lib.passwd.stablerandom-passwd-file "grafana-secret-key" + "grafana-secret-key-${config.instance.build-seed}"; + + is-private-network = let site-name = config.fudo.hosts.${hostname}.site; + in config.fudo.sites.${site-name}.local-gateway != null; + + domainToBaseDn = domain: + concatStringsSep "," (map (el: "dc=${el}") (splitString "." domain)); + + ldapEnabled = domain.ldap-servers != [ ]; + +in { + options.fudo.services.metrics = with types; { + prometheus = { + static-targets = mkOption { + type = attrsOf (listOf str); + description = + "A map of exporter type to a list of host:ports from which to collect metrics."; + example = { dovecot = [ "my.host.name:1111" ]; }; + default = { }; + }; + state-directory = mkOption { + type = str; + description = "Path at which to store Prometheus state."; + default = "/var/lib/prometheus"; + }; + }; + grafana = { + smtp = { + username = mkOption { + type = str; + description = "Username from which to send Grafana alerts."; + default = "monitor"; + }; + hostname = mkOption { + type = str; + description = "Hostname of the SMTP host."; + default = "mail.${toplevel.config.instance.local-domain}"; + }; + }; + + ldap = let base-dn = domainToBaseDn config.instance.local-domain; + in { + base-dn = mkOption { + type = str; + description = "DN under which to search for users."; + default = base-dn; + }; + + bind-user = mkOption { + type = str; + description = "DN as which to bind to the LDAP server."; + default = "grafana_reader"; + }; + + bind-passwd = mkOption { + type = nullOr str; + description = "Path to file with bind password. Generated if null."; + default = null; + }; + }; + + database = { + hostname = mkOption { + type = str; + description = "Hostname of the postgresql database."; + default = "localhost"; + }; + user = mkOption { + type = str; + description = + "Username as which to authenticate to the postgresql database."; + }; + password-file = mkOption { + type = str; + description = + "Password file (on the target host) which to authenticate to the postgresql database."; + }; + name = mkOption { + type = str; + description = "Database name."; + default = "grafana"; + }; + }; + + state-directory = mkOption { + type = str; + description = "Path at which to store Grafana state."; + default = "/var/lib/grafana"; + }; + }; + }; + + config = mkIf metricsEnabled { + fudo = { + system-users = { + ${grafana-cfg.smtp.username} = { + description = "Grafana Alerts"; + ldap-hashed-password = + pkgs.lib.passwd.hash-ldap-passwd "grafana-smtp-passwd" + grafana-smtp-password-file; + }; + + ${grafana-cfg.ldap.bind-user} = mkIf ((domain.ldap-servers != [ ]) + && (grafana-cfg.ldap.bind-passwd == null)) { + description = "Grafana Authentication Reader"; + ldap-hashed-password = + pkgs.lib.passwd.hash-ldap-passwd "grafana-auth-passwd" + grafana-auth-password-file; + }; + }; + + secrets.host-secrets = mkIf metricsMonitor + (let grafana-user = config.systemd.services.grafana.serviceConfig.User; + in { + ${hostname} = { + grafana-smtp-password = { + source-file = grafana-smtp-password-file; + target-file = "/run/metrics/grafana/smtp.passwd"; + user = grafana-user; + }; + + grafana-admin-password = { + source-file = grafana-admin-password-file; + target-file = "/run/metrics/grafana/admin.passwd"; + user = grafana-user; + }; + + grafana-secret-key = { + source-file = grafana-secret-key-file; + target-file = "/run/metrics/grafana/secret.key"; + user = grafana-user; + }; + }; + }); + + zones.${domain.zone} = { + aliases = let + metrics-aliases = alias-map-to-cnames metrics-alias-map; + monitor-aliases = alias-map-to-cnames monitor-alias-map; + metrics-master-cname = optionalAttrs (metrics-master != null) { + metrics = "${metrics-master}.${domain-name}."; + }; + monitor-master-cname = optionalAttrs (monitor-master != null) { + monitor = "${monitor-master}.${domain-name}."; + }; + in metrics-aliases // monitor-aliases // metrics-master-cname + // monitor-master-cname; + + metric-records = let + domain-hosts = filterAttrs (hostname: hostOpts: + hostOpts.domain == domain-name && hostOpts.nixos-system) + config.fudo.hosts; + in { + node = map (hostname: { + host = "${hostname}.${domain-name}"; + port = if is-private-network then 80 else 443; + }) (attrNames domain-hosts); + }; + }; + + metrics = { + node-exporter = { + enable = true; + hostname = host-fqdn hostname; + private-network = is-private-network; + }; + + prometheus = mkIf metricsScraper { + enable = true; + service-discovery-dns = { + node = [ "node._metrics._tcp.${domain-name}" ]; + }; + static-targets = prometheus-cfg.static-targets; + hostname = let alias = metrics-alias-map.${hostname}; + in "${alias}.${domain-name}"; + state-directory = prometheus-cfg.state-directory; + private-network = is-private-network; + }; + + grafana = mkIf metricsMonitor { + enable = true; + hostname = let alias = monitor-alias-map.${hostname}; + in "${alias}.${domain-name}"; + smtp = let cfg = grafana-cfg.smtp; + in { + username = cfg.username; + password-file = host-secrets.grafana-smtp-password.target-file; + hostname = cfg.hostname; + email = "${cfg.username}@${domain-name}"; + }; + database = let cfg = grafana-cfg.database; + in { + name = cfg.name; + user = cfg.user; + password-file = cfg.password-file; + hostname = cfg.hostname; + }; + ldap = mkIf (domain.ldap-servers != [ ]) { + hosts = map host-fqdn domain.ldap-servers; + base-dn = grafana-cfg.ldap.base-dn; + bind-dn = + "cn=${grafana-cfg.ldap.bind-user},${grafana-cfg.ldap.base-dn}"; + bind-passwd = if (grafana-cfg.ldap.bind-passwd != null) then + grafana-cfg.ldap.bind-passwd + else + (readFile grafana-auth-password-file); + }; + admin-password-file = host-secrets.grafana-admin-password.target-file; + secret-key-file = host-secrets.grafana-secret-key.target-file; + datasources = let + scheme = if is-private-network then "http" else "https"; + host-config = hostname: { + url = "${scheme}://${hostname}.${domain-name}"; + type = "prometheus"; + default = hostname == "metrics-0"; + }; + in listToAttrs + (map (host: nameValuePair "prometheus-${host}" (host-config host)) + (attrValues metrics-alias-map)); + state-directory = grafana-cfg.state-directory; + private-network = is-private-network; + }; + }; + }; + + services.nginx = + mkIf (hostname == metrics-master || hostname == monitor-master) { + enable = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + + virtualHosts = + let scheme = if is-private-network then "http" else "https"; + in { + "metrics.${domain-name}" = mkIf (hostname == metrics-master) { + enableACME = !is-private-network; + forceSSL = !is-private-network; + locations."/".return = let alias = metrics-alias-map.${hostname}; + in "301 ${scheme}://${alias}.${domain-name}$request_uri"; + }; + "monitor.${domain-name}" = mkIf (hostname == monitor-master) { + enableACME = !is-private-network; + forceSSL = !is-private-network; + locations."/".return = let alias = monitor-alias-map.${hostname}; + in "301 ${scheme}://${alias}.${domain-name}$request_uri"; + }; + }; + }; + }; +} diff --git a/config/service/postgresql.nix b/config/service/postgresql.nix new file mode 100644 index 0000000..6cab4d5 --- /dev/null +++ b/config/service/postgresql.nix @@ -0,0 +1,78 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + domain-name = config.fudo.hosts.${hostname}.domain; + domain = config.fudo.domains.${domain-name}; + + cfg = config.fudo.services.postgresql; + + zone-name = domain.zone; + + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + postgresEnabled = domain.postgresql-server == hostname; + publicNetwork = let + site-name = config.fudo.hosts.${hostname}.site; + in config.fudo.sites.${site-name}.local-gateway == null; + isPostgresHost = hostname == domain.postgresql-server; + + postgresql-hostname = "postgresql.${domain-name}"; + + acme-copies = config.fudo.acme.host-domains.${hostname}; + + postgresUser = config.systemd.services.postgresql.serviceConfig.User; + +in { + options.fudo.services.postgresql = with types; { + state-directory = mkOption { + type = str; + description = "Path at which to store PostgreSQL state."; + }; + + keytab = mkOption { + type = str; + description = "Keytab for PostgreSQL."; + }; + }; + + config = mkIf postgresEnabled { + fudo = { + acme.host-domains.${hostname} = mkIf (publicNetwork && isPostgresHost) { + ${postgresql-hostname}.local-copies = { + postgresql = { + user = postgresUser; + dependent-services = [ "postgresql.service" ]; + part-of = [ config.fudo.postgresql.systemd-target ]; + }; + }; + }; + + secrets.host-secrets.${hostname}.postgres-keytab = mkIf (cfg.keytab != null) { + source-file = cfg.keytab; + target-file = "/run/postgresql/postgres.keytab"; + user = postgresUser; + }; + + zones.${zone-name}.aliases.postgresql = + "${domain.postgresql-server}.${domain-name}."; + + postgresql = mkIf isPostgresHost (let + ssl-config = optionalAttrs publicNetwork (let + cert-copy = acme-copies.${postgresql-hostname}.local-copies.postgresql; + in { + ssl-certificate = mkIf publicNetwork cert-copy.full-certificate; + ssl-private-key = mkIf publicNetwork cert-copy.private-key; + required-services = [ cert-copy.service ]; + }); + in { + enable = true; + keytab = mkIf (cfg.keytab != null) host-secrets.postgres-keytab.target-file; + local-networks = config.instance.local-networks; + state-directory = cfg.state-directory; + required-services = [ config.fudo.secrets.secret-target ]; + } // ssl-config); + }; + }; +} diff --git a/config/service/selby-forum.nix b/config/service/selby-forum.nix new file mode 100644 index 0000000..22088b5 --- /dev/null +++ b/config/service/selby-forum.nix @@ -0,0 +1,277 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + relative-hostname = "forum.test"; + + strip-hash = filename: head (builtins.match "^[a-zA-Z0-9]+-(.+)$" filename); + + clean-utf8-file = filename: + pkgs.stdenv.mkDerivation { + name = "${strip-hash (baseNameOf filename)}.utf8"; + + phases = [ "installPhase" ]; + + installPhase = "iconv -c -f utf-8 -t utf-8 -o $out ${filename}"; + }; + +in { + options.fudo.services.selby-forum = with types; { + enable = mkEnableOption "Enable Selby Forum on this host."; + + state-directory = mkOption { + type = str; + description = "Directory at which to store Selby Forum state."; + }; + + legacy-forum-data = mkOption { + type = path; + description = "Path to legacy Vanilla forum data."; + }; + + external-interface = mkOption { + type = str; + description = "Public-facing network interface on this host."; + }; + + mail = { + host = mkOption { + type = str; + description = "Mail server hostname."; + }; + }; + }; + + config = (let + hostname = config.instance.hostname; + + cfg = config.fudo.services.selby-forum; + + host-fqdn = let host-domain = config.fudo.hosts.${hostname}.domain; + in "${hostname}.${host-domain}"; + + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + parent-ip = "192.168.92.1"; + child-ip = "192.168.92.2"; + + mkPasswd = name: + pkgs.lib.passwd.stablerandom-passwd-file "selby-forum-${name}" + "selby-forum-${name}-${config.instance.build-seed}"; + + mail-user = "selby-forum"; + mail-password = mkPasswd "mail-password"; + + database-name = "forum_selby_ca"; + database-user = "forum_selby_ca"; + + runtime-path = "/run/selby/forum"; + + domain-name = "selby.ca"; + zone-name = config.fudo.domains.${domain-name}.zone; + + forum-hostname = "${relative-hostname}.${domain-name}"; + + in { + networking = mkIf cfg.enable { + nat = { + enable = true; + internalInterfaces = [ "ve-selby-forum" ]; + externalInterface = cfg.external-interface; + }; + }; + + services.nginx = mkIf cfg.enable { + enable = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + + virtualHosts.${forum-hostname} = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://${child-ip}:80"; + }; + }; + + fudo = { + # Email user + system-users.${mail-user} = { + description = "Selby Forum"; + ldap-hashed-password = + pkgs.lib.passwd.hash-ldap-passwd "selby-forum-mail-passwd" + mail-password; + }; + + zones.${zone-name}.aliases = { ${relative-hostname} = "${host-fqdn}."; }; + + secrets.host-secrets.${hostname} = mkIf cfg.enable + (let db-passwd = mkPasswd "database-password"; + in { + selby-forum-database-password = { + source-file = db-passwd; + target-file = "${runtime-path}/db.passwd"; + }; + postgres-selby-forum-password = { + source-file = db-passwd; + target-file = "/run/postgres-users/selby-forum.passwd"; + user = config.services.postgresql.superUser; + }; + selby-forum-admin-passwd = { + source-file = mkPasswd "admin-password"; + target-file = "${runtime-path}/admin.passwd"; + }; + selby-forum-mail-passwd = { + source-file = mail-password; + target-file = "${runtime-path}/mail.passwd"; + }; + legacy-selby-forum-data = { + source-file = "${clean-utf8-file cfg.legacy-forum-data}"; + target-file = "${runtime-path}/selby-forum-data.sql"; + }; + }); + + postgresql = mkIf cfg.enable { + local-networks = [ "192.168.92.0/30" ]; + databases.${database-name}.users = config.instance.local-admins; + users.${database-user} = { + password-file = + host-secrets.postgres-selby-forum-password.target-file; + databases.${database-name} = { + access = "CONNECT,CREATE"; + entity-access = { + "ALL TABLES IN SCHEMA public" = "SELECT,INSERT,UPDATE,DELETE"; + "ALL SEQUENCES IN SCHEMA public" = "SELECT,UPDATE"; + }; + }; + }; + }; + }; + + systemd = mkIf cfg.enable { + tmpfiles.rules = [ "d ${cfg.state-directory} 750 - - - -" ]; + + services.discourse-prepare-selby-forum = { + description = + "Perform superuser tasks for Discourse, which doesn't have SU perms."; + wantedBy = [ "container@selby-forum.service" ]; + before = [ "container@selby-forum.service" ]; + requires = [ config.fudo.postgresql.systemd-target ]; + after = [ config.fudo.postgresql.systemd-target ]; + path = with pkgs; [ postgresql ]; + serviceConfig = { + User = config.services.postgresql.superUser; + ExecStart = + pkgs.writeShellScript "discourse-prepare-selby-forum.sh" '' + psql -d ${database-name} -c "CREATE EXTENSION IF NOT EXISTS hstore;" + psql -d ${database-name} -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" + ''; + }; + }; + }; + + containers.selby-forum = mkIf cfg.enable { + ephemeral = true; + privateNetwork = true; + hostAddress = parent-ip; + localAddress = child-ip; + autoStart = true; + + bindMounts = { + ${runtime-path} = { + hostPath = runtime-path; + isReadOnly = true; + }; + "/var/lib/discourse" = { + hostPath = cfg.state-directory; + isReadOnly = false; + }; + }; + + config = { config, lib, ... }: + let + discourse-user = config.systemd.services.discourse.serviceConfig.User; + in { + networking = { + defaultGateway = parent-ip; + firewall.enable = false; + }; + + services.discourse = { + enable = true; + hostname = forum-hostname; + enableACME = false; + admin = { + username = "admin"; + fullName = "Admin"; + email = "admin@selby.ca"; + passwordFile = "/etc/selby-forum/admin.passwd"; + }; + database = { + name = database-name; + host = parent-ip; + username = database-user; + passwordFile = "/etc/selby-forum/db.passwd"; + }; + mail.outgoing = { + username = mail-user; + passwordFile = "/etc/selby-forum/mail.passwd"; + domain = domain-name; + forceTLS = true; + serverAddress = cfg.mail.host; + }; + }; + + environment.etc = { + "selby-forum/admin.passwd" = { + source = "/run/selby/forum/admin.passwd"; + user = discourse-user; + mode = "0400"; + }; + "selby-forum/db.passwd" = { + source = "/run/selby/forum/db.passwd"; + user = discourse-user; + mode = "0400"; + }; + "selby-forum/selby-forum-data.sql" = { + source = "/run/selby/forum/selby-forum-data.sql"; + user = discourse-user; + mode = "0400"; + }; + "selby-forum/mail.passwd" = { + source = "/run/selby/forum/mail.passwd"; + user = discourse-user; + mode = "0400"; + }; + }; + + systemd = { + tmpfiles.rules = + [ "d /var/lib/discourse 700 ${discourse-user} - - -" ]; + + services = { + discourse = { after = [ "multi-user.target" ]; }; + + discourse-import-selby-forum = let + env-without-path = filterAttrs (attr: _: attr != "PATH") + config.systemd.services.discourse.environment; + in { + description = "One-off job to import Vanilla Selby Forum data."; + path = config.systemd.services.discourse.path; + environment = env-without-path; + serviceConfig = { + User = discourse-user; + type = "oneshot"; + WorkingDirectory = + config.systemd.services.discourse.serviceConfig.WorkingDirectory; + ExecStart = pkgs.writeShellScript + "import-vanilla-selby-forum-data.sh" '' + ruby script/import_scripts/vanilla.rb /etc/selby-forum/selby-forum-data.sql + ''; + }; + }; + }; + }; + }; + }; + }); +} diff --git a/config/service/wireguard-client.nix b/config/service/wireguard-client.nix new file mode 100644 index 0000000..3982ca9 --- /dev/null +++ b/config/service/wireguard-client.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + host = config.fudo.hosts.${hostname}; + domain-name = config.instance.local-domain; + domain = config.fudo.domains.${domain-name}; + + gateway = domain.wireguard.gateway; + + is-wireguard-client = + gateway != null && gateway != hostname && + host.wireguard.private-key-file != null; + +in { + config = mkIf is-wireguard-client (let + + in { + fudo.wireguard-client = { + enable = true; + server = { + + }; + }; + }); +} diff --git a/config/service/wireguard-gateway.nix b/config/service/wireguard-gateway.nix new file mode 100644 index 0000000..b84e72e --- /dev/null +++ b/config/service/wireguard-gateway.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostname = config.instance.hostname; + host = config.fudo.hosts.${hostname}; + host-secrets = config.fudo.secrets.host-secrets.${hostname}; + + cfg = config.fudo.services.wireguard-gateway; + + peerOpts = { name, ... }: { + options = with types; { + public-key = mkOption { + type = str; + description = "Peer public key."; + }; + + assigned-ip = mkOption { + type = str; + description = "IP address assigned to this peer."; + }; + }; + }; + +in { + options.fudo.services.wireguard-gateway = with types; { + enable = mkEnableOption "Enable WireGuard gateway: let external clients join the local network."; + + network = mkOption { + type = str; + description = "IP address range to use for clients."; + default = "172.16.0.0/24"; + }; + + listen-port = mkOption { + type = port; + description = "Port on which to listen for incoming connections."; + default = 51820; + }; + + peers = mkOption { + type = attrsOf (submodule peerOpts); + description = "Map of peer to peer options."; + default = {}; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = all (peerOpts: + pkgs.lib.ip.ipv4OnNetwork peerOpts.assigned-ip cfg.network) + (attrValues cfg.peers); + message = "Peer IPs must be on the assigned network."; + } + { + assertion = host.wireguard.private-key-file != null; + message = "WireGuard gateway server private key file must be set."; + } + ]; + + fudo.secrets.host-secrets.${hostname} = { + wireguard-gateway-privkey-file = { + source-file = host.wireguard.private-key-file; + target-file = "/run/wireguard-gateway/key"; + }; + }; + + networking = { + firewall.allowedUDPPorts = [ cfg.listen-port ]; + wireguard.interfaces.wg-gw0 = { + ips = [ cfg.network ]; + listenPort = cfg.listen-port; + postSetup = + "${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${cfg.network} -o ${cfg.external-interface} -j MASQUERADE"; + postShutdown = + "${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${cfg.network} -o ${cfg.external-interface} -j MASQUERADE"; + + privateKeyFile = host-secrets.wireguard-gateway-privkey-file.target-file; + + peers = let + peerList = attrValues cfg.peers; + in map (peerOpts: { + publicKey = peerOpts.public-key; + allowedIPs = [ "${peerOpts.ip}/32" ]; + }) cfg.peers; + }; + }; + }; +} diff --git a/config/service/wireguard.nix b/config/service/wireguard.nix new file mode 100644 index 0000000..6725a23 --- /dev/null +++ b/config/service/wireguard.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + hostOpts = { name, ... }: { + options = with types; { + ip = mkOption { + type = str; + description = "IP of this host on the WireGuard network."; + }; + + public-key = mkOption { + type = str; + description = "WireGuard public key of this host."; + }; + }; + }; + + wg-keys = config.fudo.secrets.files.wireguard.keys; + +in { + options.fudo.services.wireguard = with types; { + hosts = mkOption { + type = attrsOf (submodule hostOpts); + default = {}; + }; + }; +} diff --git a/config/services.nix b/config/services.nix new file mode 100644 index 0000000..3ac9723 --- /dev/null +++ b/config/services.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./service/backplane.nix + ./service/chute.nix + ./service/dns.nix + ./service/fudo-auth.nix + ./service/jabber.nix + ./service/local-network.nix + ./service/logging.nix + ./service/mail-server.nix + ./service/metrics.nix + ./service/postgresql.nix + ./service/selby-forum.nix + # ./service/wireguard-gateway.nix + ]; +} diff --git a/config/wireguard.nix b/config/wireguard.nix new file mode 100644 index 0000000..535b822 --- /dev/null +++ b/config/wireguard.nix @@ -0,0 +1,75 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + wg-keys = config.fudo.secrets.files.wireguard.keys; + + has-key = hostname: _: hasAttr hostname wg-keys; + + keyed-hosts = filterAttrs has-key config.fudo.hosts; + + sites = config.fudo.sites; + + generatePublicKeyPkg = hostname: privkey-file: pkgs.stdenv.mkDerivation { + name = "wireguard-${hostname}-key.pub"; + phases = "installPhase"; + buildInputs = [ pkgs.wireguard ]; + installPhase = '' + wg pubkey < ${privkey-file} > $out + ''; + }; + + generatePublicKey = hostname: privkey-file: + readFile "${generatePublicKeyPkg hostname privkey-file}"; + +in { + config = { + fudo.services.wireguard.networks = { + fudo-local = { + network = "10.0.0.0/8"; + captured-network = "10.192.0.0/10"; + + external-peers = { + niten-phone = { + public-key = ""; + assigned-ip = "10.192.0.100"; + }; + }; + + hosts = mapAttrs (hostname: hostOpts: let + private-key-file = wg-keys.${hostname}; + in { + inherit private-key-file; + public-key = generatePublicKey hostname private-key-file; + }) keyed-hosts; + + sites = { + seattle = { + network = sites.seattle.private-network; + gateway = sites.seattle.local-gateway; + }; + + nuttyclub = { + network = sites.nuttyclub.private-network; + gateway = "nutboy3"; + }; + + portage = { + network = sites.portage.private-network; + gateway = "france"; + }; + + worldstream = { + network = sites.worldstream.private-network; + gateway = "legatus"; + }; + + russell = { + network = sites.russell.private-network; + gateway = sites.russell.local-gateway; + }; + }; + }; + }; + }; +} diff --git a/config/zones.nix b/config/zones.nix new file mode 100644 index 0000000..e76974b --- /dev/null +++ b/config/zones.nix @@ -0,0 +1,7 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./zones/selby.ca.nix + ]; +} diff --git a/config/zones/selby.ca.nix b/config/zones/selby.ca.nix new file mode 100644 index 0000000..f6077e8 --- /dev/null +++ b/config/zones/selby.ca.nix @@ -0,0 +1,38 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + fudo = config.fudo.zones."fudo.org"; + + getIfAttrs = attrs: attrmap: let + has-attr = attr: hasAttr attr attrmap; + in getAttrs (filter has-attr attrs) attrmap; + +in { + config = { + fudo.zones."selby.ca" = { + srv-records = let + # Mail records will be created, no need to copy + shared-tcp-attrs = + ["domain" + "kerberos" + "kerberos-adm" + "ldap" + "ldaps" + "minecraft" + "xmpp-client" + "xmpp-server"]; + + shared-udp-attrs = + [ + "kerberos" + "kerberos-master" + "kpasswd" + ]; + in { + tcp = getIfAttrs shared-tcp-attrs fudo.srv-records.tcp; + udp = getIfAttrs shared-udp-attrs fudo.srv-records.udp; + }; + }; + }; +} diff --git a/live-disk/flake.nix b/live-disk/flake.nix new file mode 100644 index 0000000..1d2d8a2 --- /dev/null +++ b/live-disk/flake.nix @@ -0,0 +1,110 @@ +{ + description = "Live Disk Flake"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-21.05"; + + fudo-home = { + url = "git+https://git.fudo.org/fudo-nix/home.git"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # This MUST be a clean git repo, because we use the timestamp. + fudo-entities = { + url = "git+https://git.fudo.org/fudo-nix/entities.git"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + fudo-lib = { + url = "git+https://git.fudo.org/fudo-nix/lib.git"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + fudo-pkgs.url = "git+https://git.fudo.org/fudo-nix/pkgs.git"; + }; + + outputs = { self, nixpkgs, fudo-home, fudo-entities, fudo-lib, fudo-pkgs, ... + }@inputs: { + nixosConfigurations.live-cd-x86_64-linux = let + system = "x86_64-linux"; + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + overlays = [ fudo-pkgs.overlay ]; + }; + in { + inherit system; + modules = [ + ({ config, ... }: { + imports = [ + fudo-home.nixosModule + "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" + "${nixpkgs}/nixos/modules/installer/cd-dvd/channel.nix" + ]; + config = with pkgs.lib; { + environment.etc.nixos-live.source = ./.; + hardware.enableAllFirmware = true; + environment.systemPackages = with pkgs; [ + btrfs-progs + emacs + git + parted + gparted + nix-prefetch-scripts + wget + ]; + + services.openssh = { + enable = true; + startWhenNeeded = true; + permitRootLogin = mkDefault "prohibit-password"; + }; + + i18n.defaultLocale = "en_US.UTF-8"; + console.useXkbConfig = true; + + services.xserver = { + layout = "us"; + xkbVariant = "dvp"; + xkbOptions = "ctrl:nocaps"; + }; + + nix = { + package = pkgs.nixFlakes; + extraOptions = "experimental-features = nix-command flakes"; + }; + + programs = { + ssh = { + startAgent = true; + + package = pkgs.openssh_gssapi; + + extraConfig = '' + GSSAPIAuthentication yes + GSSAPIDelegateCredentials yes + ''; + }; + }; + + krb5.libdefaults.default_realm = "FUDO.ORG"; + + users.users = { + niten = { + isNormalUser = true; + createHome = true; + hashedPassword = + "$6$a1q2Duoe35hd5$IaZGXPfqyGv9uq5DQm7DZq0vIHsUs39sLktBiBBqMiwl/f/Z4jSvNZLJp9DZJYe5u2qGBYh1ca.jsXvQA8FPZ/"; + extraGroups = [ "wheel" ]; + }; + + root.openssh.authorizedKeys.keys = [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGVez4of30f+j0cWKj5kYCKeFjyNsYvG9UbOMxF5hImD2lP5MSbFBv31gFgHjx3yCG4zQRZlpuyU5uWo0qIwe9N84/LcZcB9WrWKZXDmuof7zPFy0J+Hj+LVLDQI/mVXHNwkMhBMHpPrdwA05EYDAYCYklWT4cSByu10pHtST+olF8i+A+UQgUzgNZzdJVeiYZv6MBDTYsJWptGeDUkl2B0Es3gtbGYcCCfnyS3RC7DIXlDo3NBbAr7WaHY2MBbT+R/+jicn9E3IY3NCM5jENxqmvHy9MDsxEEYgFNm7IDwq4V1VRUWy277YsvRbmEaHb+osOA5u1VNN4z3UftOZcSZgR5C/vR71cENXoPt1YQpCzu7i38ojtvL+tDVEKT7sIovrQw8q1sszNlW2nXh8RSPiIq5TMnrV73MP0egKcr9n3tfxwi1BIkLjvfom/02BkTK9R9v+VMNhYU1YwROhORCiMIgoxUGiUvtH8u38JGr7E0hhMoAjCE5k80WPUivl0=" + ]; + }; + }; + }) + ]; + }; + }; +}