diff --git a/config/client.nix b/config/client.nix index f88dbcf..2635fd8 100644 --- a/config/client.nix +++ b/config/client.nix @@ -3,7 +3,7 @@ with lib; let make-passwd-file = hostname: - pkgs.lib.fudo.passwd.stablerandom-passwd-file + pkgs.lib.passwd.stablerandom-passwd-file "${hostname}-fudo-client-passwd" config.instance.build-seed; diff --git a/config/default.nix b/config/default.nix index 169e985..a98edb6 100644 --- a/config/default.nix +++ b/config/default.nix @@ -6,14 +6,10 @@ ./bash.nix ./client.nix ./common.nix - ./domains.nix ./dns.nix ./groups.nix - ./hosts.nix ./instance.nix ./kerberos.nix - ./networks.nix - ./sites.nix ./users.nix ./user-config.nix ./wireless-networks.nix diff --git a/config/dns.nix b/config/dns.nix index bf84435..58c8fdf 100644 --- a/config/dns.nix +++ b/config/dns.nix @@ -18,8 +18,8 @@ let in { config = { fudo.dns = mkIf is-primary (let - primary-ip = pkgs.lib.fudo.network.host-ipv4 config hostname; - all-ips = pkgs.lib.fudo.network.host-ips config hostname; + primary-ip = pkgs.lib.network.host-ipv4 config hostname; + all-ips = pkgs.lib.network.host-ips config hostname; in { enable = true; identity = "${hostname}.${domain}"; @@ -44,9 +44,9 @@ in { # TODO: there's no guarantee this exists... dmarc-report-address = "dmarc-report@${domain}"; - network-definition = let - network = config.fudo.networks.${domain}; - in network // { + zone-definition = let + zone = config.fudo.zones.${domain}; + in zone // { srv-records = { tcp = { domain = [{ diff --git a/config/hardware/lambda.nix b/config/hardware/lambda.nix index 6d125df..2319ae1 100644 --- a/config/hardware/lambda.nix +++ b/config/hardware/lambda.nix @@ -35,7 +35,7 @@ in { "/boot" = { device = "/dev/disk/by-label/lambda-boot"; fsType = "ext4"; - options = [ "noexec" ]; + options = [ "noexec" "noatime" "nodiratime" ]; }; "/" = { @@ -47,19 +47,20 @@ in { "/nix" = { device = "lambda/transient/nix"; fsType = "zfs"; + options = [ "noatime" "nodiratime" ]; }; "/var/log" = { device = "lambda/transient/logs"; fsType = "zfs"; neededForBoot = true; - options = [ "noexec" ]; + options = [ "noexec" "noatime" "nodiratime" ]; }; "/state" = { device = "lambda/persistent/state"; fsType = "zfs"; - options = [ "noexec" ]; + options = [ "noexec" "noatime" "nodiratime" ]; }; }; diff --git a/config/hardware/legatus.nix b/config/hardware/legatus.nix index 6272fc3..e0aab38 100644 --- a/config/hardware/legatus.nix +++ b/config/hardware/legatus.nix @@ -68,7 +68,7 @@ with lib; { interfaces = { extif0 = { macAddress = - pkgs.lib.fudo.network.generate-mac-address "legatus" "extif0"; + pkgs.lib.network.generate-mac-address "legatus" "extif0"; }; }; }; diff --git a/config/hardware/nutboy3.nix b/config/hardware/nutboy3.nix index 7c83d65..a96d4c7 100644 --- a/config/hardware/nutboy3.nix +++ b/config/hardware/nutboy3.nix @@ -72,7 +72,7 @@ with lib; { interfaces = { extif0 = { macAddress = - pkgs.lib.fudo.network.generate-mac-address config.instance.hostname "extif0"; + pkgs.lib.network.generate-mac-address config.instance.hostname "extif0"; }; }; }; diff --git a/config/host-config/france.nix b/config/host-config/france.nix index 7dd17eb..18f1b4f 100644 --- a/config/host-config/france.nix +++ b/config/host-config/france.nix @@ -88,7 +88,7 @@ in { }; fudo = let - backplane-dns-password-file = pkgs.lib.fudo.passwd.stablerandom-passwd-file + backplane-dns-password-file = pkgs.lib.passwd.stablerandom-passwd-file "dns-service-backplane-passwd" "dns-service-backplane-passwd-${config.instance.build-seed}"; in { @@ -118,7 +118,7 @@ in { }; ldap-root-passwd = { source-file = - pkgs.lib.fudo.passwd.random-passwd-file "ldap-root-passwd" 20; + pkgs.lib.passwd.random-passwd-file "ldap-root-passwd" 20; target-file = "/run/openldap/root.passwd"; user = ldap-user; group = ldap-group; diff --git a/config/host-config/lambda.nix b/config/host-config/lambda.nix index beec181..3ecf93a 100644 --- a/config/host-config/lambda.nix +++ b/config/host-config/lambda.nix @@ -36,12 +36,6 @@ in { # secret-paths = [ "/state/secrets" ]; # }; - fudo.games.valheim = { - enable = true; - state-directory = "/state/games/valheim"; - password = "silkymilky"; - }; - systemd.tmpfiles.rules = [ "L /root/.gnupg - - - - ${state-dir}/user/root/gnupg" "L /root/.ssh/id_rsa - - - - ${state-dir}/user/root/ssh/id_rsa" diff --git a/config/host-config/limina.nix b/config/host-config/limina.nix index 55fec23..f6406c2 100644 --- a/config/host-config/limina.nix +++ b/config/host-config/limina.nix @@ -57,7 +57,7 @@ in { dhcp-dynamic-network = site.dynamic-network; search-domains = [ domain-name "fudo.org" ]; enable-reverse-mappings = true; - network-definition = config.fudo.networks.${domain-name}; + zone-definition = config.fudo.zones.${domain-name}; }; client.dns.external-interface = "enp1s0"; diff --git a/config/host-config/nostromo.nix b/config/host-config/nostromo.nix index 00706d3..e66568b 100644 --- a/config/host-config/nostromo.nix +++ b/config/host-config/nostromo.nix @@ -17,13 +17,15 @@ in { intif0 = { useDHCP = true; }; }; + + firewall.enable = false; }; # Hopefully this'll help with NFS... boot.kernelModules = [ "rpcsec_gss_krb5" ]; services.nfs = { - # See lib/fudo/users.nix for the user@REALM -> user mapping + # See ../user-config.nix for the user@REALM -> user mapping server = { enable = true; createMountPoints = false; @@ -56,12 +58,4 @@ in { # users = [ "niten" ]; # api-address = "/ip4/0.0.0.0/tcp/5001"; # }; - - virtualisation = { - libvirtd = { - enable = true; - qemuPackage = pkgs.qemu_kvm; - onShutdown = "shutdown"; - }; - }; } diff --git a/config/hosts/atom.nix b/config/hosts/atom.nix index c59dbca..8cff1fe 100644 --- a/config/hosts/atom.nix +++ b/config/hosts/atom.nix @@ -3,9 +3,10 @@ enable-gui = false; rp = "niten"; admin-email = "niten@fudo.org"; - domain = "mobile.fudo.org"; - site = "mobile"; + domain = "sea.fudo.org"; + site = "seattle"; profile = "laptop"; arch = "x86_64-linux"; - nixos-system = true; + # FIXME: Need a better story for laptops + nixos-system = false; } diff --git a/config/kerberos.nix b/config/kerberos.nix index f104c0a..55e2ab6 100644 --- a/config/kerberos.nix +++ b/config/kerberos.nix @@ -19,9 +19,8 @@ in { auth.kdc = mkIf (is-master || is-slave) { enable = true; realm = cfg.gssapi-realm; - # TODO: Also bind to ::1? bind-addresses = - (pkgs.lib.fudo.network.host-ips config hostname) ++ + (pkgs.lib.network.host-ips config hostname) ++ [ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"); master-config = mkIf is-master { acl = let @@ -40,7 +39,7 @@ in { }; dns.domains.${domain} = { - network-definition = mkIf kerberized-domain { + zone-definition = mkIf kerberized-domain { srv-records = let get-fqdn = hostname: "${hostname}.${config.fudo.hosts.${hostname}.domain}"; diff --git a/config/user-config.nix b/config/user-config.nix index ff1dea2..d40869a 100644 --- a/config/user-config.nix +++ b/config/user-config.nix @@ -10,6 +10,8 @@ with lib; hostname = config.instance.hostname; host-cfg = config.fudo.hosts.${hostname}; + + sys = config.instance; in { fudo.auth.ldap-server = { users = filterAttrs diff --git a/flake.lock b/flake.lock index e000f3f..c8715f6 100644 --- a/flake.lock +++ b/flake.lock @@ -24,6 +24,28 @@ "type": "path" } }, + "deploy-rs": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "nixpkgs" + ], + "utils": "utils" + }, + "locked": { + "lastModified": 1632822684, + "narHash": "sha256-lt7eayYmgsD5OQwpb1XYfHpxttn43bWo7G7hIJs+zJw=", + "owner": "serokell", + "repo": "deploy-rs", + "rev": "9a02de4373e0ec272d08a417b269a28ac8b961b4", + "type": "github" + }, + "original": { + "owner": "serokell", + "repo": "deploy-rs", + "type": "github" + } + }, "dnssec-keys": { "flake": false, "locked": { @@ -48,7 +70,7 @@ "explain-pause-mode": "explain-pause-mode", "flake-utils": "flake-utils_2", "nix-straight": "nix-straight", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "nose": "nose", "ob-racket": "ob-racket", "org": "org", @@ -213,6 +235,22 @@ "type": "path" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1627913399, + "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "locked": { "lastModified": 1637014545, @@ -247,10 +285,12 @@ "inputs": { "flake-utils": "flake-utils", "fudo-lib": "fudo-lib", - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "narHash": "sha256-0hx0bItc00j8+Rnm0MDZ0EWhWpnVeaEBtnVP7Bzc0Rw=", + "narHash": "sha256-DnIQNMu2E6gQh0SJk4h6iz2cL/vK6VIobp35hS7qQmo=", "path": "/state/fudo-entities", "type": "path" }, @@ -264,10 +304,12 @@ "doom-emacs": "doom-emacs", "home-manager": "home-manager", "niten-doom-config": "niten-doom-config", - "nixpkgs": "nixpkgs_3" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "narHash": "sha256-TpFI+nD+c9JXhKKDBgIHJhIfveTScBD6gotTPt8tvg4=", + "narHash": "sha256-oygpdcJGaDoABzehQLt9zZ/1aa2lWHw35UeAzO6nUgA=", "path": "/state/nixops/fudo-home", "type": "path" }, @@ -278,7 +320,7 @@ }, "fudo-lib": { "locked": { - "narHash": "sha256-Syzn7n7RzfbFFfE4luEEbcVo05Eq0eLed9QtCwXux1c=", + "narHash": "sha256-r8qffvJPWbBJmN32thMIxjdmY9X5VCc3TuyQyFAIdks=", "path": "/state/fudo-lib", "type": "path" }, @@ -289,7 +331,7 @@ }, "fudo-lib_2": { "locked": { - "narHash": "sha256-Syzn7n7RzfbFFfE4luEEbcVo05Eq0eLed9QtCwXux1c=", + "narHash": "sha256-1unDTB52rrC4MhomFowHi4kzTea0/dk15A9D59Cpu0Q=", "path": "/state/fudo-lib", "type": "path" }, @@ -399,20 +441,6 @@ } }, "nixpkgs": { - "locked": { - "lastModified": 1637600646, - "narHash": "sha256-Nr/+8/ySh2Ezis0lWEn8jqdRax8ZVHAA9IGV0m0NA9I=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "4418f362e68c69226807a5837fcbfc94f0b92adb", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "type": "indirect" - } - }, - "nixpkgs_2": { "locked": { "lastModified": 1626852498, "narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=", @@ -427,28 +455,13 @@ "type": "indirect" } }, - "nixpkgs_3": { + "nixpkgs_2": { "locked": { - "lastModified": 1636944046, - "narHash": "sha256-74KLDsiWSBsYXKj/ql9EGbw1TbIJRE7clFkhl30HV/c=", + "lastModified": 1637973395, + "narHash": "sha256-wp/QgsnnKJAIMZeQgJT1bWQ/OutZ+80lHwtpYAUWDfE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "46251a79f752ae1d46ef733e8e9760b6d3429da4", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-21.05", - "type": "indirect" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1637448181, - "narHash": "sha256-ujcXli4esmtIHUBjE1BjmMuBWrcNvlHZrVXx56i5B1M=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d5b65f812cd4f5a8fa74b406075b59a46f1cfd98", + "rev": "4f37689c8a219a9d756c5ff38525ad09349f422f", "type": "github" }, "original": { @@ -583,12 +596,13 @@ }, "root": { "inputs": { + "deploy-rs": "deploy-rs", "fudo-entities": "fudo-entities", "fudo-home": "fudo-home", "fudo-lib": "fudo-lib_2", "fudo-pkgs": "fudo-pkgs", "fudo-secrets": "fudo-secrets", - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_2" } }, "rotate-text": { @@ -642,6 +656,21 @@ "path": "/state/secrets/ssh-keypairs", "type": "path" } + }, + "utils": { + "locked": { + "lastModified": 1631561581, + "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 82f019c..b263f1c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,19 +4,34 @@ inputs = { nixpkgs.url = "nixpkgs/nixos-21.05"; - fudo-home.url = "path:/state/nixops/fudo-home"; + fudo-home = { + url = "path:/state/nixops/fudo-home"; + inputs.nixpkgs.follows = "nixpkgs"; + }; - fudo-entities.url = "path:/state/fudo-entities"; + fudo-entities = { + url = "path:/state/fudo-entities"; + inputs.nixpkgs.follows = "nixpkgs"; + }; - fudo-lib.url = "path:/state/fudo-lib"; + fudo-lib = { + url = "path:/state/fudo-lib"; + inputs.nixpkgs.follows = "nixpkgs"; + }; fudo-pkgs.url = "path:/state/nixops/fudo-pkgs"; fudo-secrets.url = "path:/state/secrets"; + + deploy-rs = { + url = "github:serokell/deploy-rs"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, + deploy-rs, fudo-home, fudo-lib, fudo-entities, @@ -44,6 +59,7 @@ overlays = [ fudo-lib.overlay fudo-pkgs.overlay + deploy-rs.overlay ]; }; in { @@ -67,17 +83,23 @@ network-hosts = config.fudo.networks.${domain}.hosts; in { imports = [ + ./config fudo-entities.nixosModule (config-path + /hardware/${hostname}.nix) - (config-path + /host-config/${hostname}.nix) - (config-path + /profile-config/${profile}.nix) - (config-path + /domain-config/${domain}.nix) - (config-path + /site-config/${site}.nix) - ]; + ] ++ (filter pathExists + [ + (config-path + /host-config/${hostname}.nix) + (config-path + /profile-config/${profile}.nix) + (config-path + /domain-config/${domain}.nix) + (config-path + /site-config/${site}.nix) + ] + ); instance = { inherit hostname; build-seed = builtins.readFile config.fudo.secrets.files.build-seed; + # build-timestamp = + # self.sourceInfo.lastModified; }; nixpkgs.pkgs = pkgs-for system; }) diff --git a/lib/default.nix b/lib/default.nix deleted file mode 100644 index 62d5a20..0000000 --- a/lib/default.nix +++ /dev/null @@ -1,57 +0,0 @@ -{ lib, config, pkgs, ... }: - -with lib; { - imports = [ - ../config - - ./instance.nix - - ./fudo/acme-certs.nix - ./fudo/acme-for-hostname.nix - ./fudo/authentication.nix - ./fudo/backplane - ./fudo/chat.nix - ./fudo/client/dns.nix - ./fudo/deploy.nix - ./fudo/distributed-builds.nix - ./fudo/dns.nix - ./fudo/domains.nix - ./fudo/garbage-collector.nix - ./fudo/git.nix - ./fudo/global.nix - ./fudo/grafana.nix - ./fudo/hosts.nix - ./fudo/host-filesystems.nix - ./fudo/initrd-network.nix - ./fudo/ipfs.nix - ./fudo/jabber.nix - ./fudo/kdc.nix - ./fudo/ldap.nix - ./fudo/local-network.nix - ./fudo/mail.nix - ./fudo/mail-container.nix - ./fudo/minecraft-server.nix - ./fudo/netinfo-email.nix - ./fudo/networks.nix - ./fudo/node-exporter.nix - ./fudo/nsd.nix - ./fudo/password.nix - ./fudo/postgres.nix - ./fudo/prometheus.nix - ./fudo/secrets.nix - ./fudo/secure-dns-proxy.nix - ./fudo/sites.nix - ./fudo/slynk.nix - ./fudo/ssh.nix - ./fudo/system.nix - ./fudo/system-networking.nix - ./fudo/users.nix - ./fudo/vpn.nix - ./fudo/webmail.nix - ./fudo/wireless-networks.nix - - ./fudo/games/valheim.nix - - ./informis/cl-gemini.nix - ]; -} diff --git a/lib/dns.nix b/lib/dns.nix deleted file mode 100644 index c0979f4..0000000 --- a/lib/dns.nix +++ /dev/null @@ -1,70 +0,0 @@ -{ pkgs, ... }: - -with pkgs.lib; -let - join-lines = concatStringsSep "\n"; - - dump = obj: builtins.trace obj obj; - - makeSrvRecords = protocol: service: records: let - service-blah = (dump service); - record-blah = (dump records); - in - join-lines (map (record: - "_${service}._${protocol} IN SRV ${toString record.priority} ${ - toString record.weight - } ${toString record.port} ${record.host}.") records); - - makeSrvProtocolRecords = protocol: services: - join-lines (mapAttrsToList (makeSrvRecords protocol) services); - - srvRecordOpts = with types; { - options = { - weight = mkOption { - type = int; - description = "Weight relative to other records."; - default = 1; - }; - - priority = mkOption { - type = int; - description = "Priority to give this record."; - default = 0; - }; - - port = mkOption { - type = port; - description = "Port to use when connecting."; - }; - - host = mkOption { - type = str; - description = "Host to contact for this service."; - example = "my-host.my-domain.com."; - }; - }; - }; - - srvRecordPair = domain: protocol: service: record: { - "_${service}._${protocol}.${domain}" = - "${toString record.priority} ${toString record.weight} ${ - toString record.port - } ${record.host}."; - }; - -in rec { - - srvRecords = with types; attrsOf (attrsOf (listOf (submodule srvRecordOpts))); - - srvRecordsToBindZone = srvRecords: - join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords); - - concatMapAttrs = f: attrs: - concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs); - - srvRecordsToPairs = domain: srvRecords: - listToAttrs (concatMapAttrs (protocol: services: - concatMapAttrs - (service: records: map (srvRecordPair domain protocol service) records) services) - srvRecords); -} diff --git a/lib/fudo-lib.nix b/lib/fudo-lib.nix deleted file mode 100644 index b23cfd0..0000000 --- a/lib/fudo-lib.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ lib, ... }: - -# NOTE: OBSOLETE! See overlay.nix - -{ - lib.overlays = [ - (final: prev: - prev.lib // { - fudo = let - lib = prev.lib; - in { - ip = import ./ip.nix { inherit lib; }; - dns = import ./dns.nix { inherit lib; }; - passwd = import ./passwd.nix { inherit lib; }; - lisp = import ./lisp.nix { inherit lib; }; - }; - }) - ]; -} diff --git a/lib/fudo/acme-certs.nix b/lib/fudo/acme-certs.nix deleted file mode 100644 index c11627e..0000000 --- a/lib/fudo/acme-certs.nix +++ /dev/null @@ -1,206 +0,0 @@ -{ config, lib, pkgs, ... } @ toplevel: - -with lib; -let - hostname = config.instance.hostname; - - domainOpts = { name, ... }: let - domain = name; - in { - options = with types; { - email = mkOption { - type = str; - description = "Domain administrator email."; - default = "admin@${domain}"; - }; - - extra-domains = mkOption { - type = listOf str; - description = "List of domains to add to this certificate."; - default = []; - }; - - local-copies = let - localCopyOpts = { name, ... }: let - copy = name; - in { - options = with types; let - target-path = "/run/ssl-certificates/${domain}/${copy}"; - in { - user = mkOption { - type = str; - description = "User to which this copy belongs."; - }; - - group = mkOption { - type = nullOr str; - description = "Group to which this copy belongs."; - default = null; - }; - - service = mkOption { - type = str; - description = "systemd job to copy certs."; - default = "fudo-acme-${domain}-${copy}-certs.service"; - }; - - certificate = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/cert.pem"; - }; - - full-certificate = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/fullchain.pem"; - }; - - chain = mkOption { - type = str; - description = "Full path to the local copy certificate."; - default = "${target-path}/chain.pem"; - }; - - private-key = mkOption { - type = str; - 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 { - type = attrsOf (submodule localCopyOpts); - description = "Map of copies to make for use by services."; - default = {}; - }; - }; - }; - - head-or-null = lst: if (lst == []) then null else head lst; - rm-service-ext = filename: - head-or-null (builtins.match "^(.+)\.service$" filename); - - concatMapAttrs = f: attrs: - foldr (a: b: a // b) {} (mapAttrsToList f attrs); - - cfg = config.fudo.acme; - hasLocalDomains = hasAttr hostname cfg.host-domains; - localDomains = if hasLocalDomains then - cfg.host-domains.${hostname} else {}; - - optionalStringOr = str: default: - if (str != null) then str else default; - -in { - options.fudo.acme = with types; { - host-domains = mkOption { - type = attrsOf (attrsOf (submodule domainOpts)); - description = "Map of host to domains to domain options."; - default = { }; - }; - }; - - config = { - security.acme.certs = mapAttrs (domain: domainOpts: { - 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 - copies = concatMapAttrs (domain: domainOpts: - domainOpts.local-copies) localDomains; - 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 "-"} - -"; - in map (dir-entry copyOpts) [ - copyOpts.certificate - copyOpts.full-certificate - copyOpts.chain - copyOpts.private-key - ]) copies; - in unique (concatMap (i: unique i) copy-paths); - - services = concatMapAttrs (domain: domainOpts: - 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 ${source}/cert.pem ${copyOpts.certificate} - chmod 0444 ${copyOpts.certificate} - chown ${owners} ${copyOpts.certificate} - - cp ${source}/full.pem ${copyOpts.full-certificate} - chmod 0444 ${copyOpts.full-certificate} - chown ${owners} ${copyOpts.full-certificate} - - cp ${source}/chain.pem ${copyOpts.chain} - chmod 0444 ${copyOpts.chain} - chown ${owners} ${copyOpts.chain} - - cp ${source}/key.pem ${copyOpts.private-key} - chmod ${key-perms copyOpts} ${copyOpts.private-key} - chown ${owners} ${copyOpts.private-key} - ''; - - 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 = "simple"; - ExecStart = install-certs; - RemainAfterExit = true; - StandardOutput = "journal"; - }; - }; - }) domainOpts.local-copies) localDomains; - }; - }; -} diff --git a/lib/fudo/acme-for-hostname.nix b/lib/fudo/acme-for-hostname.nix deleted file mode 100644 index 0451170..0000000 --- a/lib/fudo/acme-for-hostname.nix +++ /dev/null @@ -1,69 +0,0 @@ -# Starts an Nginx server on $HOSTNAME just to get a cert for this host - -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.acme; - - # wwwRoot = hostname: - # pkgs.writeTextFile { - # name = "index.html"; - - # text = '' - # - # - # ${hostname} - # - # - #

${hostname} - # - # - # ''; - # destination = "/www"; - # }; - -in { - - options.fudo.acme = { - enable = mkEnableOption "Fetch ACME certs for supplied local hostnames."; - - hostnames = mkOption { - type = with types; listOf str; - description = "A list of hostnames mapping to this host, for which to acquire SSL certificates."; - default = []; - example = [ - "my.hostname.com" - "alt.hostname.com" - ]; - }; - - admin-address = mkOption { - type = types.str; - description = "The admin address in charge of these addresses."; - default = "admin@fudo.org"; - }; - }; - - config = mkIf cfg.enable { - - services.nginx = { - enable = true; - - virtualHosts = listToAttrs - (map - (hostname: - nameValuePair hostname - { - enableACME = true; - forceSSL = true; - # root = (wwwRoot hostname) + ("/" + "www"); - }) - cfg.hostnames); - }; - - security.acme.certs = listToAttrs - (map (hostname: nameValuePair hostname { email = cfg.admin-address; }) - cfg.hostnames); - }; -} diff --git a/lib/fudo/authentication.nix b/lib/fudo/authentication.nix deleted file mode 100644 index c88fa7c..0000000 --- a/lib/fudo/authentication.nix +++ /dev/null @@ -1,67 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.authentication; -in { - options.fudo.authentication = { - enable = mkEnableOption "Use Fudo users & groups from LDAP."; - - ssl-ca-certificate = mkOption { - type = types.str; - description = "Path to the CA certificate to use to bind to the server."; - }; - - bind-passwd-file = mkOption { - type = types.str; - description = "Path to a file containing the password used to bind to the server."; - }; - - ldap-url = mkOption { - type = types.str; - description = "URL of the LDAP server."; - example = "ldaps://auth.fudo.org"; - }; - - base = mkOption { - type = types.str; - description = "The LDAP base in which to look for users."; - default = "dc=fudo,dc=org"; - }; - - bind-dn = mkOption { - type = types.str; - description = "The DN with which to bind the LDAP server."; - default = "cn=auth_reader,dc=fudo,dc=org"; - }; - }; - - config = mkIf cfg.enable { - users.ldap = { - enable = true; - base = cfg.base; - bind = { - distinguishedName = cfg.bind-dn; - passwordFile = cfg.bind-passwd-file; - timeLimit = 5; - }; - loginPam = true; - nsswitch = true; - server = cfg.ldap-url; - timeLimit = 5; - useTLS = true; - extraConfig = '' - TLS_CACERT ${cfg.ssl-ca-certificate} - TSL_REQCERT allow - ''; - - daemon = { - enable = true; - extraConfig = '' - tls_cacertfile ${cfg.ssl-ca-certificate} - tls_reqcert allow - ''; - }; - }; - }; -} diff --git a/lib/fudo/backplane/common.nix b/lib/fudo/backplane/common.nix deleted file mode 100644 index a1d3e95..0000000 --- a/lib/fudo/backplane/common.nix +++ /dev/null @@ -1,154 +0,0 @@ -{ 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 deleted file mode 100644 index 5596440..0000000 --- a/lib/fudo/backplane/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -{ - imports = [ - ./common.nix - ./dns.nix - ./jabber.nix - ]; -} diff --git a/lib/fudo/backplane/dns.nix b/lib/fudo/backplane/dns.nix deleted file mode 100644 index 6c97556..0000000 --- a/lib/fudo/backplane/dns.nix +++ /dev/null @@ -1,143 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - backplane-cfg = config.fudo.backplane; - - cfg = backplane-cfg.dns; - - powerdns-conf-dir = "${cfg.powerdns.home}/conf.d"; - -in { - config = mkIf cfg.enable { - users = { - users = { - "${cfg.user}" = { - isSystemUser = true; - group = cfg.group; - createHome = true; - home = "/var/home/${cfg.user}"; - }; - ${cfg.powerdns.user} = { - isSystemUser = true; - home = cfg.powerdns.home; - createHome = true; - }; - }; - - groups = { - ${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" ]; - - readWritePaths = [ 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 - - if [ ! -f ${cfg.database.password-file} ]; then - echo "${cfg.database.password-file} does not exist!" - exit 1 - fi - - 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 - - mv $TMPCONF ${powerdns-conf-dir}/pdns.local.gpgsql.conf - rm -rf $TMPDIR - - 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 = 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; - - 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."; - wantedBy = [ "multi-user.target" ]; - after = cfg.required-services ++ [ "postgresql.service" ]; - }; - }; - - 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}/ - ''; - in { - description = "Backplane PowerDNS name server"; - requires = [ - "postgresql.service" - "backplane-powerdns-config-generator.service" - ]; - after = [ "network.target" ]; - path = with pkgs; [ powerdns postgresql ]; - serviceConfig = { - ExecStart = "pdns_server --setuid=${cfg.powerdns.user} --setgid=${cfg.powerdns.user} --chroot=${cfg.powerdns.home} --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${pdns-config-dir}"; - }; - }; - }; - }; - }; -} diff --git a/lib/fudo/backplane/jabber.nix b/lib/fudo/backplane/jabber.nix deleted file mode 100644 index 8f6e988..0000000 --- a/lib/fudo/backplane/jabber.nix +++ /dev/null @@ -1,90 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -{ - config = mkIf config.fudo.jabber.enable { - fudo = let - cfg = config.fudo.backplane; - - hostname = config.instance.hostname; - - 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 { - 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/chat.nix b/lib/fudo/chat.nix deleted file mode 100644 index b885373..0000000 --- a/lib/fudo/chat.nix +++ /dev/null @@ -1,262 +0,0 @@ -{ pkgs, lib, config, ... }: - -with lib; -let - cfg = config.fudo.chat; - mattermost-config-target = "/run/chat/mattermost/mattermost-config.json"; - -in { - options.fudo.chat = with types; { - enable = mkEnableOption "Enable chat server"; - - hostname = mkOption { - type = str; - description = "Hostname at which this chat server is accessible."; - example = "chat.mydomain.com"; - }; - - site-name = mkOption { - type = str; - description = "The name of this chat server."; - example = "My Fancy Chat Site"; - }; - - smtp = { - server = mkOption { - type = str; - description = "SMTP server to use for sending notification emails."; - example = "mail.my-site.com"; - }; - - user = mkOption { - type = str; - description = "Username with which to connect to the SMTP server."; - }; - - password-file = mkOption { - type = str; - description = - "Path to a file containing the password to use while connecting to the SMTP server."; - }; - }; - - state-directory = mkOption { - type = str; - description = "Path at which to store server state data."; - default = "/var/lib/mattermost"; - }; - - database = mkOption { - type = (submodule { - options = { - name = mkOption { - type = str; - description = "Database name."; - }; - - hostname = mkOption { - type = str; - description = "Database host."; - }; - - user = mkOption { - type = str; - description = "Database user."; - }; - - password-file = mkOption { - type = str; - description = "Path to file containing database password."; - }; - }; - }); - description = "Database configuration."; - example = { - name = "my_database"; - hostname = "my.database.com"; - user = "db_user"; - password-file = /path/to/some/file.pw; - }; - }; - }; - - config = mkIf cfg.enable (let - pkg = pkgs.mattermost; - default-config = builtins.fromJSON (readFile "${pkg}/config/config.json"); - modified-config = recursiveUpdate default-config { - ServiceSettings.SiteURL = "https://${cfg.hostname}"; - ServiceSettings.ListenAddress = "127.0.0.1:8065"; - TeamSettings.SiteName = cfg.site-name; - EmailSettings = { - RequireEmailVerification = true; - SMTPServer = cfg.smtp.server; - SMTPPort = 587; - EnableSMTPAuth = true; - SMTPUsername = cfg.smtp.user; - SMTPPassword = "__SMTP_PASSWD__"; - SendEmailNotifications = true; - ConnectionSecurity = "STARTTLS"; - FeedbackEmail = "chat@fudo.org"; - FeedbackName = "Admin"; - }; - EnableEmailInvitations = true; - SqlSettings.DriverName = "postgres"; - SqlSettings.DataSource = "postgres://${ - cfg.database.user - }:__DATABASE_PASSWORD__@${ - cfg.database.hostname - }:5432/${ - cfg.database.name - }"; - }; - mattermost-config-file-template = - pkgs.writeText "mattermost-config.json.template" (builtins.toJSON modified-config); - mattermost-user = "mattermost"; - mattermost-group = "mattermost"; - - generate-mattermost-config = target: template: smtp-passwd-file: db-passwd-file: - pkgs.writeScript "mattermost-config-generator.sh" '' - SMTP_PASSWD=$( cat ${smtp-passwd-file} ) - DATABASE_PASSWORD=$( cat ${db-passwd-file} ) - sed -e 's/__SMTP_PASSWD__/"$SMTP_PASSWD"/' -e 's/__DATABASE_PASSWORD__/"$DATABASE_PASSWORD"/' ${template} > ${target} - ''; - - in { - users = { - users = { - ${mattermost-user} = { - isSystemUser = true; - group = mattermost-group; - }; - }; - - groups = { ${mattermost-group} = { members = [ mattermost-user ]; }; }; - }; - - fudo.system.services.mattermost = { - description = "Mattermost Chat Server"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - - preStart = '' - ${generate-mattermost-config - mattermost-config-target - mattermost-config-file-template - cfg.smtp.password-file - cfg.database.password-file} - cp ${cfg.smtp.password-file} ${cfg.state-directory}/config/config.json - cp -uRL ${pkg}/client ${cfg.state-directory} - chown ${mattermost-user}:${mattermost-group} ${cfg.state-directory}/client - chmod 0750 ${cfg.state-directory}/client - ''; - execStart = "${pkg}/bin/mattermost"; - workingDirectory = cfg.state-directory; - user = mattermost-user; - group = mattermost-group; - }; - - systemd = { - - tmpfiles.rules = [ - "d ${cfg.state-directory} 0750 ${mattermost-user} ${mattermost-group} - -" - "d ${cfg.state-directory}/config 0750 ${mattermost-user} ${mattermost-group} - -" - "L ${cfg.state-directory}/bin - - - - ${pkg}/bin" - "L ${cfg.state-directory}/fonts - - - - ${pkg}/fonts" - "L ${cfg.state-directory}/i18n - - - - ${pkg}/i18n" - "L ${cfg.state-directory}/templates - - - - ${pkg}/templates" - ]; - - # services.mattermost = { - # description = "Mattermost Chat Server"; - # wantedBy = [ "multi-user.target" ]; - # after = [ "network.target" ]; - - # preStart = '' - # ${generate-mattermost-config - # mattermost-config-target - # mattermost-config-file-template - # cfg.smtp.password-file - # cfg.database.password-file} - # cp ${cfg.smtp.password-file} ${cfg.state-directory}/config/config.json - # cp -uRL ${pkg}/client ${cfg.state-directory} - # chown ${mattermost-user}:${mattermost-group} ${cfg.state-directory}/client - # chmod 0750 ${cfg.state-directory}/client - # ''; - - # serviceConfig = { - # PermissionsStartOnly = true; - # ExecStart = "${pkg}/bin/mattermost"; - # WorkingDirectory = cfg.state-directory; - # Restart = "always"; - # RestartSec = "10"; - # LimitNOFILE = "49152"; - # User = mattermost-user; - # Group = mattermost-group; - # }; - # }; - }; - - services.nginx = { - enable = true; - - appendHttpConfig = '' - proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mattermost_cache:10m max_size=3g inactive=120m use_temp_path=off; - ''; - - virtualHosts = { - "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; - - locations."/" = { - proxyPass = "http://127.0.0.1:8065"; - - extraConfig = '' - client_max_body_size 50M; - proxy_set_header Connection ""; - 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 $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - proxy_buffers 256 16k; - proxy_buffer_size 16k; - proxy_read_timeout 600s; - proxy_cache mattermost_cache; - proxy_cache_revalidate on; - proxy_cache_min_uses 2; - proxy_cache_use_stale timeout; - proxy_cache_lock on; - proxy_http_version 1.1; - ''; - }; - - locations."~ /api/v[0-9]+/(users/)?websocket$" = { - proxyPass = "http://127.0.0.1:8065"; - - extraConfig = '' - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - client_max_body_size 50M; - 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 $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - proxy_buffers 256 16k; - proxy_buffer_size 16k; - client_body_timeout 60; - send_timeout 300; - lingering_timeout 5; - proxy_connect_timeout 90; - proxy_send_timeout 300; - proxy_read_timeout 90s; - ''; - }; - }; - }; - }; - }); -} diff --git a/lib/fudo/client/dns.nix b/lib/fudo/client/dns.nix deleted file mode 100644 index c81292e..0000000 --- a/lib/fudo/client/dns.nix +++ /dev/null @@ -1,131 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - cfg = config.fudo.client.dns; - - ssh-key-files = - map (host-key: host-key.path) config.services.openssh.hostKeys; - - ssh-key-args = concatStringsSep " " (map (file: "-f ${file}") ssh-key-files); - -in { - options.fudo.client.dns = { - ipv4 = mkOption { - type = types.bool; - default = true; - description = "Report host external IPv4 address to Fudo DynDNS server."; - }; - - ipv6 = mkOption { - type = types.bool; - default = true; - description = "Report host external IPv6 address to Fudo DynDNS server."; - }; - - sshfp = mkOption { - type = types.bool; - default = true; - description = "Report host SSH fingerprints to the Fudo DynDNS server."; - }; - - domain = mkOption { - type = types.str; - description = "Domain under which this host is registered."; - default = "fudo.link"; - }; - - server = mkOption { - type = types.str; - description = "Backplane DNS server to which changes will be reported."; - default = "backplane.fudo.org"; - }; - - password-file = mkOption { - type = types.str; - description = "File containing host password for backplane."; - example = "/path/to/secret.passwd"; - }; - - frequency = mkOption { - type = types.str; - description = - "Frequency at which to report the local IP(s) to backplane."; - default = "*:0/15"; - }; - - user = mkOption { - type = types.str; - description = - "User as which to run the client script (must have access to password file)."; - default = "backplane-dns-client"; - }; - - external-interface = mkOption { - type = with types; nullOr str; - description = - "Interface with which this host communicates with the larger internet."; - default = null; - }; - }; - - config = { - - users.users = { - "${cfg.user}" = { - isSystemUser = true; - createHome = true; - home = "/var/home/${cfg.user}"; - }; - }; - - systemd = { - tmpfiles.rules = [ - "d /var/home 755 root - - -" - "d /var/home/${cfg.user} 700 ${cfg.user} - - -" - ]; - - timers.backplane-dns-client = { - enable = true; - description = "Report local IP addresses to Fudo backplane."; - partOf = [ "backplane-dns-client.service" ]; - wantedBy = [ "timers.target" ]; - requires = [ "network-online.target" ]; - timerConfig = { OnCalendar = cfg.frequency; }; - }; - - services.backplane-dns-client-pw-file = { - enable = true; - requiredBy = [ "backplane-dns-client.services" ]; - reloadIfChanged = true; - serviceConfig = { Type = "oneshot"; }; - script = '' - chmod 400 ${cfg.password-file} - chown ${cfg.user} ${cfg.password-file} - ''; - }; - - services.backplane-dns-client = { - enable = true; - serviceConfig = { - Type = "oneshot"; - StandardOutput = "journal"; - User = cfg.user; - ExecStart = pkgs.writeShellScript "start-backplane-dns-client.sh" '' - ${pkgs.backplane-dns-client}/bin/backplane-dns-client ${ - optionalString cfg.ipv4 "-4" - } ${optionalString cfg.ipv6 "-6"} ${ - optionalString cfg.sshfp ssh-key-args - } ${ - optionalString (cfg.external-interface != null) - "--interface=${cfg.external-interface}" - } --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file} - ''; - }; - # Needed to generate SSH fingerprinst - path = [ pkgs.openssh ]; - reloadIfChanged = true; - }; - }; - }; -} diff --git a/lib/fudo/common.nix b/lib/fudo/common.nix deleted file mode 100644 index 92b7bb7..0000000 --- a/lib/fudo/common.nix +++ /dev/null @@ -1,5 +0,0 @@ -# General Fudo config, shared across packages -{ config, lib, pkgs, ... }: - -with lib; -{ } diff --git a/lib/fudo/deploy.nix b/lib/fudo/deploy.nix deleted file mode 100644 index 522ce4d..0000000 --- a/lib/fudo/deploy.nix +++ /dev/null @@ -1,13 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - site-cfg = config.fudo.sites.${config.instance.local-site}; - -in { - config = { - users.users.root.openssh.authorizedKeys.keys = - mkIf (site-cfg.deploy-pubkeys != null) - site-cfg.deploy-pubkeys; - }; -} diff --git a/lib/fudo/distributed-builds.nix b/lib/fudo/distributed-builds.nix deleted file mode 100644 index ab21008..0000000 --- a/lib/fudo/distributed-builds.nix +++ /dev/null @@ -1,48 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - - site-cfg = config.fudo.sites.${config.instance.local-site}; - - has-build-servers = (length (attrNames site-cfg.build-servers)) > 0; - - build-keypair = config.fudo.secrets.host-secrets.${hostname}.build-keypair; - - enable-distributed-builds = - site-cfg.enable-distributed-builds && has-build-servers && build-keypair != null; - - local-build-cfg = if (hasAttr hostname site-cfg.build-servers) then - site-cfg.build-servers.${hostname} - else null; - -in { - config = { - nix = mkIf enable-distributed-builds { - buildMachines = mapAttrsToList (hostname: buildOpts: { - hostName = "${hostname}.${domain-name}"; - maxJobs = buildOpts.max-jobs; - speedFactor = buildOpts.speed-factor; - supportedFeatures = buildOpts.supportedFeatures; - sshKey = build-keypair.private-key; - sshUser = buildOpts.user; - }) site-cfg.build-servers; - distributedBuilds = true; - - trustedUsers = mkIf (local-build-cfg != null) [ - local-build-host.build-user - ]; - }; - - users.users = mkIf (local-build-cfg != null) { - ${local-build-cfg.build-user} = { - isSystemUser = true; - openssh.authorizedKeys.keyFiles = - concatLists - (mapAttrsToList (host: hostOpts: hostOpts.build-pubkeys) - config.instance.local-hosts); - }; - }; - }; -} diff --git a/lib/fudo/dns.nix b/lib/fudo/dns.nix deleted file mode 100644 index fcee95b..0000000 --- a/lib/fudo/dns.nix +++ /dev/null @@ -1,178 +0,0 @@ -{ lib, config, pkgs, ... }: - -with lib; -let - cfg = config.fudo.dns; - - join-lines = concatStringsSep "\n"; - - domainOpts = { domain, ... }: { - options = with types; { - dnssec = mkOption { - type = bool; - description = "Enable DNSSEC security for this zone."; - default = true; - }; - - dmarc-report-address = mkOption { - type = nullOr str; - description = "The email to use to recieve DMARC reports, if any."; - example = "admin-user@domain.com"; - default = null; - }; - - network-definition = mkOption { - type = submodule (import ../types/network-definition.nix); - description = "Definition of network to be served by local server."; - }; - - default-host = mkOption { - type = str; - description = "The host to which the domain should map by default."; - }; - - mx = mkOption { - type = listOf str; - description = "The hosts which act as the domain mail exchange."; - default = []; - }; - - gssapi-realm = mkOption { - type = nullOr str; - description = "The GSSAPI realm of this domain."; - default = null; - }; - }; - }; - - networkHostOpts = import ../types/network-host.nix { inherit lib; }; - - hostRecords = hostname: nethost-data: let - # FIXME: RP doesn't work. - # generic-host-records = let - # host-data = if (hasAttr hostname config.fudo.hosts) then config.fudo.hosts.${hostname} else null; - # in - # if (host-data == null) then [] else ( - # (map (sshfp: "${hostname} IN SSHFP ${sshfp}") host-data.ssh-fingerprints) ++ (optional (host-data.rp != null) "${hostname} IN RP ${host-data.rp}") - # ); - sshfp-records = if (hasAttr hostname config.fudo.hosts) then (map (sshfp: "${hostname} IN SSHFP ${sshfp}") config.fudo.hosts.${hostname}.ssh-fingerprints) else []; - a-record = optional (nethost-data.ipv4-address != null) "${hostname} IN A ${nethost-data.ipv4-address}"; - aaaa-record = optional (nethost-data.ipv6-address != null) "${hostname} IN AAAA ${nethost-data.ipv6-address}"; - description-record = optional (nethost-data.description != null) "${hostname} IN TXT \"${nethost-data.description}\""; - in - join-lines (a-record ++ aaaa-record ++ description-record ++ sshfp-records); - - makeSrvRecords = protocol: type: records: - join-lines (map (record: - "_${type}._${protocol} IN SRV ${toString record.priority} ${ - toString record.weight - } ${toString record.port} ${toString record.host}.") records); - - makeSrvProtocolRecords = protocol: types: - join-lines (mapAttrsToList (makeSrvRecords protocol) types); - - cnameRecord = alias: host: "${alias} IN CNAME ${host}"; - - mxRecords = mxs: concatStringsSep "\n" (map (mx: "@ IN MX 10 ${mx}.") mxs); - - dmarcRecord = dmarc-email: - optionalString (dmarc-email != null) '' - _dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"''; - - nsRecords = domain: ns-hosts: - join-lines - (mapAttrsToList (host: _: "@ IN NS ${host}.${domain}.") ns-hosts); - -in { - - options.fudo.dns = with types; { - enable = mkEnableOption "Enable master DNS services."; - - # FIXME: This should allow for AAAA addresses too... - nameservers = mkOption { - type = attrsOf (submodule networkHostOpts); - description = "Map of domain nameserver FQDNs to IP."; - example = { - "ns1.domain.com" = { - ipv4-address = "1.1.1.1"; - description = "my fancy dns server"; - }; - }; - }; - - identity = mkOption { - type = str; - description = "The identity (CH TXT ID.SERVER) of this host."; - }; - - domains = mkOption { - type = attrsOf (submodule domainOpts); - default = { }; - description = "A map of domain to domain options."; - }; - - listen-ips = mkOption { - type = listOf str; - description = "A list of IPs on which to listen for DNS queries."; - example = [ "1.2.3.4" ]; - }; - - state-directory = mkOption { - type = str; - description = "Path at which to store nameserver state, including DNSSEC keys."; - default = "/var/lib/nsd"; - }; - }; - - config = mkIf cfg.enable { - networking.firewall = { - allowedTCPPorts = [ 53 ]; - allowedUDPPorts = [ 53 ]; - }; - - fudo.nsd = { - enable = true; - identity = cfg.identity; - interfaces = cfg.listen-ips; - stateDir = cfg.state-directory; - zones = mapAttrs' (dom: dom-cfg: let - net-cfg = dom-cfg.network-definition; - in nameValuePair "${dom}." { - dnssec = dom-cfg.dnssec; - - data = '' - $ORIGIN ${dom}. - $TTL 12h - - @ IN SOA ns1.${dom}. hostmaster.${dom}. ( - ${toString config.instance.build-timestamp} - 30m - 2m - 3w - 5m) - - ${optionalString (dom-cfg.default-host != null) - "@ IN A ${dom-cfg.default-host}"} - - ${mxRecords dom-cfg.mx} - - $TTL 6h - - ${optionalString (dom-cfg.gssapi-realm != null) - ''_kerberos IN TXT "${dom-cfg.gssapi-realm}"''} - - ${nsRecords dom cfg.nameservers} - ${join-lines (mapAttrsToList hostRecords cfg.nameservers)} - - ${dmarcRecord dom-cfg.dmarc-report-address} - - ${join-lines - (mapAttrsToList makeSrvProtocolRecords net-cfg.srv-records)} - ${join-lines (mapAttrsToList hostRecords net-cfg.hosts)} - ${join-lines (mapAttrsToList cnameRecord net-cfg.aliases)} - ${join-lines net-cfg.verbatim-dns-records} - ''; - }) cfg.domains; - }; - }; -} diff --git a/lib/fudo/domain/dns.nix b/lib/fudo/domain/dns.nix deleted file mode 100644 index bf84435..0000000 --- a/lib/fudo/domain/dns.nix +++ /dev/null @@ -1,69 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - domain = config.instance.local-domain; - cfg = config.fudo.domains.${domain}; - - served-domain = cfg.primary-nameserver != null; - - is-primary = hostname == cfg.primary-nameserver; - - create-srv-record = port: hostname: { - port = port; - host = hostname; - }; - -in { - config = { - fudo.dns = mkIf is-primary (let - primary-ip = pkgs.lib.fudo.network.host-ipv4 config hostname; - all-ips = pkgs.lib.fudo.network.host-ips config hostname; - in { - enable = true; - identity = "${hostname}.${domain}"; - nameservers = { - ns1 = { - ipv4-address = primary-ip; - description = "Primary ${domain} nameserver"; - }; - }; - - # Deliberately leaving out localhost so the primary nameserver - # can use a custom recursor - listen-ips = all-ips; - - domains = { - ${domain} = { - dnssec = true; - default-host = primary-ip; - gssapi-realm = cfg.gssapi-realm; - mx = optional (cfg.primary-mailserver != null) - cfg.primary-mailserver; - # TODO: there's no guarantee this exists... - dmarc-report-address = "dmarc-report@${domain}"; - - network-definition = let - network = config.fudo.networks.${domain}; - in network // { - srv-records = { - tcp = { - domain = [{ - host = "ns1.${domain}"; - port = 53; - }]; - }; - udp = { - domain = [{ - host = "ns1.${domain}"; - port = 53; - }]; - }; - }; - }; - }; - }; - }); - }; -} diff --git a/lib/fudo/domain/kerberos.nix b/lib/fudo/domain/kerberos.nix deleted file mode 100644 index f104c0a..0000000 --- a/lib/fudo/domain/kerberos.nix +++ /dev/null @@ -1,74 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - domain = config.instance.local-domain; - cfg = config.fudo.domains.${domain}; - -in { - config = let - hostname = config.instance.hostname; - is-master = hostname == cfg.kerberos-master; - is-slave = elem hostname cfg.kerberos-slaves; - - kerberized-domain = cfg.kerberos-master != null; - - in { - fudo = { - auth.kdc = mkIf (is-master || is-slave) { - enable = true; - realm = cfg.gssapi-realm; - # TODO: Also bind to ::1? - bind-addresses = - (pkgs.lib.fudo.network.host-ips config hostname) ++ - [ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"); - master-config = mkIf is-master { - acl = let - admin-entries = genAttrs cfg.local-admins - (admin: { - perms = [ "add" "change-password" "list" ]; - }); - in admin-entries // { - "*/root" = { perms = [ "all" ]; }; - }; - }; - slave-config = mkIf is-slave { - master-host = cfg.kerberos-master; - # You gotta provide the keytab yourself, sorry... - }; - }; - - dns.domains.${domain} = { - network-definition = mkIf kerberized-domain { - srv-records = let - get-fqdn = hostname: - "${hostname}.${config.fudo.hosts.${hostname}.domain}"; - - create-srv-record = port: hostname: { - port = port; - host = hostname; - }; - - all-servers = map get-fqdn - ([cfg.kerberos-master] ++ cfg.kerberos-slaves); - - master-servers = - map get-fqdn [cfg.kerberos-master]; - - in { - tcp = { - kerberos = map (create-srv-record 88) all-servers; - kerberos-adm = map (create-srv-record 749) master-servers; - }; - udp = { - kerberos = map (create-srv-record 88) all-servers; - kerberos-master = map (create-srv-record 88) master-servers; - kpasswd = map (create-srv-record 464) master-servers; - }; - }; - }; - }; - }; - }; -} diff --git a/lib/fudo/domains.nix b/lib/fudo/domains.nix deleted file mode 100644 index 5b6202b..0000000 --- a/lib/fudo/domains.nix +++ /dev/null @@ -1,94 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - domain = config.instance.local-domain; - - domainOpts = { name, ... }: let - domain = name; - in { - options = with types; { - domain = mkOption { - type = str; - description = "Domain name."; - default = domain; - }; - - local-networks = mkOption { - type = listOf str; - description = - "A list of networks to be considered trusted on this network."; - default = [ ]; - }; - - local-users = mkOption { - type = listOf str; - description = - "A list of users who should have local (i.e. login) access to _all_ hosts in this domain."; - default = [ ]; - }; - - local-admins = mkOption { - type = listOf str; - description = - "A list of users who should have admin access to _all_ hosts in this domain."; - default = [ ]; - }; - - local-groups = mkOption { - type = listOf str; - description = "List of groups which should exist within this domain."; - default = [ ]; - }; - - admin-email = mkOption { - type = str; - description = "Email for the administrator of this domain."; - default = "admin@${domain}"; - }; - - gssapi-realm = mkOption { - type = str; - description = "GSSAPI (i.e. Kerberos) realm of this domain."; - default = toUpper domain; - }; - - kerberos-master = mkOption { - type = nullOr str; - description = "Hostname of the Kerberos master server for the domain, if applicable."; - default = null; - }; - - kerberos-slaves = mkOption { - type = listOf str; - description = "List of hosts acting as Kerberos slaves for the domain."; - default = []; - }; - - primary-nameserver = mkOption { - type = nullOr str; - description = "Hostname of the primary nameserver for this domain."; - default = null; - }; - - primary-mailserver = mkOption { - type = nullOr str; - description = "Hostname of the primary mail server for this domain."; - default = null; - }; - }; - }; - -in { - options.fudo.domains = mkOption { - type = with types; attrsOf (submodule domainOpts); - description = "Domain configurations for all domains known to the system."; - default = { }; - }; - - imports = [ - ./domain/kerberos.nix - ./domain/dns.nix - ]; -} diff --git a/lib/fudo/games/default.nix b/lib/fudo/games/default.nix deleted file mode 100644 index 1858f1f..0000000 --- a/lib/fudo/games/default.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ ... }: - -{ - imports = [ ./valheim.nix ]; -} diff --git a/lib/fudo/games/valheim.nix b/lib/fudo/games/valheim.nix deleted file mode 100644 index 9c6d83f..0000000 --- a/lib/fudo/games/valheim.nix +++ /dev/null @@ -1,63 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let cfg = config.fudo.games.valheim; -in { - options.fudo.games.valheim = with types; { - enable = mkEnableOption "Enable dedicated Valheim server."; - - state-directory = mkOption { - type = str; - description = "Directory at which to store Valheim state data."; - default = "/var/lib/valheim"; - }; - - port = mkOption { - type = port; - description = "Port on which to listen for connections."; - default = 2456; - }; - - password = mkOption { - type = str; - description = "Password required by connecting users."; - }; - }; - - config = mkIf cfg.enable { - users.users.valheim = { - home = cfg.state-directory; - createHome = true; - isSystemUser = true; - }; - - systemd.services.fudo-valheim = { - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - ExecStartPre = '' - [[ -d ${cfg.state-directory}/state ]] || mkdir ${cfg.state-directory}/state - ${pkgs.steamcmd}/bin/steamcmd \ - +login anonymous \ - +force_install_dir ${cfg.state-directory}/state \ - +app_update 896660 \ - +quit - ''; - - ExecStart = '' - ${pkgs.glibc}/lib/ld-linux-x86-64.so.2 ./valheim_server.x86_64 \ - -name "CoCo Valheim" \ - -port ${toString cfg.port} \ - -world "Dedicated" \ - -password ${cfg.password} \ - -public 1 - ''; - Nice = -5; - Restart = "always"; - User = "valheim"; - StateDirectory = "${cfg.state-directory}/state"; - WorkingDirectory = cfg.state-directory; - }; - environment = { LD_LIBRARY_PATH = "linux64:${pkgs.glibc}/lib"; }; - }; - }; -} diff --git a/lib/fudo/garbage-collector.nix b/lib/fudo/garbage-collector.nix deleted file mode 100644 index dfa2488..0000000 --- a/lib/fudo/garbage-collector.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let cfg = config.fudo.garbage-collector; - -in { - - options.fudo.garbage-collector = { - enable = mkEnableOption "Enable periodic NixOS garbage collection"; - - timing = mkOption { - type = types.str; - default = "weekly"; - description = - "Period (systemd format) at which to run garbage collector."; - }; - - age = mkOption { - type = types.str; - default = "30d"; - description = "Age of garbage to collect (eg. 30d)."; - }; - }; - - config = mkIf cfg.enable { - fudo.system.services.fudo-garbage-collector = { - description = "Collect NixOS garbage older than ${cfg.age}."; - onCalendar = cfg.timing; - type = "oneshot"; - script = - "${pkgs.nix}/bin/nix-collect-garbage --delete-older-than ${cfg.age}"; - addressFamilies = [ ]; - }; - }; -} diff --git a/lib/fudo/git.nix b/lib/fudo/git.nix deleted file mode 100644 index 4988645..0000000 --- a/lib/fudo/git.nix +++ /dev/null @@ -1,171 +0,0 @@ -{ pkgs, lib, config, ... }: - -with lib; -let - cfg = config.fudo.git; - - databaseOpts = { ... }: { - options = { - name = mkOption { - type = types.str; - description = "Database name."; - }; - hostname = mkOption { - type = types.str; - description = "Hostname of the database server."; - }; - user = mkOption { - type = types.str; - description = "Database username."; - }; - password-file = mkOption { - type = types.path; - description = "File containing the database user's password."; - }; - }; - }; - - sshOpts = { ... }: - with types; { - options = { - listen-ip = mkOption { - type = str; - description = "IP on which to listen for SSH connections."; - }; - - listen-port = mkOption { - type = port; - description = - "Port on which to listen for SSH connections, on ."; - default = 22; - }; - }; - }; - -in { - options.fudo.git = with types; { - enable = mkEnableOption "Enable Fudo git web server."; - - hostname = mkOption { - type = str; - description = "Hostname at which this git server is accessible."; - example = "git.fudo.org"; - }; - - site-name = mkOption { - type = str; - description = "Name to use for the git server."; - default = "Fudo Git"; - }; - - database = mkOption { - type = (submodule databaseOpts); - description = "Gitea database options."; - }; - - repository-dir = mkOption { - type = str; - description = "Path at which to store repositories."; - example = "/srv/git/repo"; - }; - - state-dir = mkOption { - type = str; - description = "Path at which to store server state."; - example = "/srv/git/state"; - }; - - user = mkOption { - type = with types; nullOr str; - description = "System user as which to run."; - default = "git"; - }; - - local-port = mkOption { - type = port; - description = - "Local port to which the Gitea server will bind. Not globally accessible."; - default = 3543; - }; - - ssh = mkOption { - type = nullOr (submodule sshOpts); - description = "SSH listen configuration."; - default = null; - }; - }; - - config = mkIf cfg.enable { - security.acme.certs.${cfg.hostname}.email = - let domain-name = config.fudo.hosts.${config.instance.hostname}.domain; - in config.fudo.domains.${domain-name}.admin-email; - - networking.firewall.allowedTCPPorts = - mkIf (cfg.ssh != null) [ cfg.ssh.listen-port ]; - - environment.systemPackages = with pkgs; let - gitea-admin = writeShellScriptBin "gitea-admin" '' - TMP=$(mktemp -d /tmp/gitea-XXXXXXXX) - ${gitea}/bin/gitea --custom-path ${cfg.state-dir}/custom --config ${cfg.state-dir}/custom/conf/app.ini --work-path $TMP $@ - ''; - in [ - gitea-admin - ]; - - services = { - gitea = { - enable = true; - appName = cfg.site-name; - database = { - createDatabase = false; - host = cfg.database.hostname; - name = cfg.database.name; - user = cfg.database.user; - passwordFile = cfg.database.password-file; - type = "postgres"; - }; - domain = cfg.hostname; - httpAddress = "127.0.0.1"; - httpPort = cfg.local-port; - repositoryRoot = cfg.repository-dir; - stateDir = cfg.state-dir; - rootUrl = "https://${cfg.hostname}/"; - user = mkIf (cfg.user != null) cfg.user; - ssh = { - enable = true; - clonePort = cfg.ssh.listen-port; - }; - settings = mkIf (cfg.ssh != null) { - server = { - SSH_DOMAIN = cfg.hostname; - SSH_LISTEN_PORT = cfg.ssh.listen-port; - SSH_LISTEN_HOST = cfg.ssh.listen-ip; - }; - }; - }; - - nginx = { - enable = true; - - virtualHosts = { - "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; - - locations."/" = { - proxyPass = "http://127.0.0.1:${toString cfg.local-port}"; - - 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; - ''; - }; - }; - }; - }; - }; - }; -} diff --git a/lib/fudo/global.nix b/lib/fudo/global.nix deleted file mode 100644 index f8e497b..0000000 --- a/lib/fudo/global.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; { - config = { }; -} diff --git a/lib/fudo/grafana.nix b/lib/fudo/grafana.nix deleted file mode 100644 index 6bed0e5..0000000 --- a/lib/fudo/grafana.nix +++ /dev/null @@ -1,143 +0,0 @@ -# NOTE: this assumes that postgres is running locally. - -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.grafana; - fudo-cfg = config.fudo.common; - - database-name = "grafana"; - database-user = "grafana"; - - databaseOpts = { ... }: { - options = { - name = mkOption { - type = types.str; - description = "Database name."; - }; - hostname = mkOption { - type = types.str; - description = "Hostname of the database server."; - }; - user = mkOption { - type = types.str; - description = "Database username."; - }; - password-file = mkOption { - type = types.path; - description = "File containing the database user's password."; - }; - }; - }; - -in { - - options.fudo.grafana = { - enable = mkEnableOption "Fudo Metrics Display Service"; - - hostname = mkOption { - type = types.str; - description = "Grafana site hostname."; - example = "fancy-graphs.fudo.org"; - }; - - smtp-username = mkOption { - type = types.str; - description = "Username with which to send email."; - }; - - smtp-password-file = mkOption { - type = types.path; - description = "Path to a file containing the email user's password."; - }; - - database = mkOption { - type = (types.submodule databaseOpts); - description = "Grafana database configuration."; - }; - - admin-password-file = mkOption { - type = types.path; - description = "Path to a file containing the admin user's password."; - }; - - secret-key-file = mkOption { - type = types.path; - description = "Path to a file containing the server's secret key, used for signatures."; - }; - - prometheus-host = mkOption { - type = types.str; - description = "The URL of the prometheus data source."; - }; - }; - - config = mkIf cfg.enable { - security.acme.certs.${cfg.hostname}.email = fudo-cfg.admin-email; - - services.nginx = { - enable = true; - - virtualHosts = { - "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; - - locations."/" = { - proxyPass = "http://127.0.0.1:3000"; - - 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; - ''; - }; - }; - }; - }; - - services.grafana = { - enable = true; - - addr = "127.0.0.1"; - protocol = "http"; - port = 3000; - domain = "${cfg.hostname}"; - rootUrl = "https://${cfg.hostname}/"; - - security = { - adminPasswordFile = cfg.admin-password-file; - secretKeyFile = cfg.secret-key-file; - }; - - smtp = { - enable = true; - fromAddress = "metrics@fudo.org"; - host = "mail.fudo.org:25"; - user = cfg.smtp-username; - passwordFile = cfg.smtp-password-file; - }; - - database = { - host = cfg.database.hostname; - name = cfg.database.name; - user = cfg.database.user; - passwordFile = cfg.database.password-file; - type = "postgres"; - }; - - provision.datasources = [ - { - editable = false; - isDefault = true; - name = cfg.prometheus-host; - type = "prometheus"; - url = "https://${cfg.prometheus-host}/"; - } - ]; - }; - }; -} diff --git a/lib/fudo/host-filesystems.nix b/lib/fudo/host-filesystems.nix deleted file mode 100644 index e3c3214..0000000 --- a/lib/fudo/host-filesystems.nix +++ /dev/null @@ -1,123 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - host-filesystems = config.fudo.hosts.${hostname}.encrypted-filesystems; - - optionalOrDefault = str: default: if (str != null) then str else default; - - filesystemsToMountpointLists = mapAttrsToList - (fs: fsOpts: fsOpts.mountpoints); - - concatMapAttrs = f: as: concatMap (i: i) (mapAttrsToList f as); - - concatMapAttrsToList = f: attrs: - concatMap (i: i) (mapAttrsToList f attrs); - -in { - config = { - users.groups = let - site-name = config.instance.local-site; - site-hosts = filterAttrs - (hostname: hostOpts: hostOpts.site == site-name) - config.fudo.hosts; - site-mountpoints = concatMapAttrsToList - (host: hostOpts: concatMapAttrsToList - (fs: fsOpts: attrValues fsOpts.mountpoints) - hostOpts.encrypted-filesystems) - site-hosts; - in listToAttrs - (map (mp: nameValuePair mp.group { members = mp.users; }) - site-mountpoints); - - systemd = { - # Ensure the mountpoints exist - tmpfiles.rules = let - mpPerms = mpOpts: if mpOpts.world-readable then "755" else "750"; - mountpointToPath = mp: mpOpts: - "d '${mp}' ${mpPerms mpOpts} root ${optionalOrDefault mpOpts.group "-"} - -"; - filesystemsToMountpointLists = mapAttrsToList - (fs: fsOpts: fsOpts.mountpoints); - mountpointListsToPaths = concatMap - (mps: mapAttrsToList mountpointToPath mps); - in mountpointListsToPaths (filesystemsToMountpointLists host-filesystems); - - # Actual mounts of decrypted filesystems - mounts = let - filesystems = mapAttrsToList - (fs: opts: { filesystem = fs; opts = opts; }) - host-filesystems; - - mounts = concatMap - (fs: mapAttrsToList - (mp: mp-opts: - { - what = "/dev/mapper/${fs.filesystem}"; - type = fs.opts.filesystem-type; - where = mp; - options = concatStringsSep "," (fs.opts.options ++ mp-opts.options); - description = "${fs.opts.filesystem-type} filesystem on ${fs.filesystem} mounted to ${mp}"; - requires = [ "${fs.filesystem}-decrypt.service" ]; - partOf = [ "${fs.filesystem}.target" ]; - wantedBy = [ "${fs.filesystem}.target" ]; - }) - fs.opts.mountpoints) - filesystems; - in mounts; - - # Jobs to decrypt the encrypted devices - services = mapAttrs' (filesystem-name: opts: - nameValuePair "${filesystem-name}-decrypt" - { - description = "Decrypt the ${filesystem-name} filesystem when the key is available at ${opts.key-path}"; - path = with pkgs; [ cryptsetup ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = pkgs.writeShellScript "decrypt-${filesystem-name}.sh" '' - [ -e /dev/mapper/${filesystem-name} ] || cryptsetup open --type luks --key-file ${opts.key-path} ${opts.encrypted-device} ${filesystem-name} - ''; - ExecStartPost = pkgs.writeShellScript "remove-${filesystem-name}-key.sh" '' - rm ${opts.key-path} - ''; - ExecStop = pkgs.writeShellScript "close-${filesystem-name}.sh" '' - cryptsetup close /dev/mapper/${filesystem-name} - ''; - }; - restartIfChanged = true; - }) - host-filesystems; - - # Watch the path of the key, trigger decrypt when it's available - paths = let - decryption-jobs = mapAttrs' (filesystem-name: opts: - nameValuePair "${filesystem-name}-decrypt" - { - wantedBy = [ "default.target" ]; - description = "Watch for decryption key, then decrypt the target filesystem."; - pathConfig = { - PathExists = opts.key-path; - Unit = "${filesystem-name}-decrypt.service"; - }; - }) host-filesystems; - - post-decryption-jobs = mapAttrs' (filesystem-name: opts: - nameValuePair "${filesystem-name}-mount" - { - wantedBy = [ "default.target" ]; - description = "Mount ${filesystem-name} filesystems once the decrypted device is available."; - pathConfig = { - PathExists = "/dev/mapper/${filesystem-name}"; - Unit = "${filesystem-name}.target"; - }; - }) host-filesystems; - in decryption-jobs // post-decryption-jobs; - - targets = mapAttrs (filesystem-name: opts: - { - description = "${filesystem-name} enabled and available."; - }) host-filesystems; - }; - }; -} diff --git a/lib/fudo/hosts.nix b/lib/fudo/hosts.nix deleted file mode 100644 index 3c169f8..0000000 --- a/lib/fudo/hosts.nix +++ /dev/null @@ -1,127 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - mapOptional = f: val: if (val != null) then (f val) else null; - - host = import ../types/host.nix { inherit lib; }; - - hostname = config.instance.hostname; - - generate-string-hash = name: str: let - string-hash-pkg = pkgs.stdenv.mkDerivation { - name = "${name}-string-hash"; - phases = "installPhase"; - buildInputs = [ pkgs.openssl ]; - installPhase = "openssl passwd -6 ${str} > $out"; - }; - in string-hash-pkg; - -in { - options.fudo.hosts = with types; - mkOption { - type = attrsOf (submodule host.hostOpts); - description = "Host configurations for all hosts known to the system."; - default = { }; - }; - - config = let - hostname = config.instance.hostname; - host-cfg = config.fudo.hosts.${hostname}; - site-name = host-cfg.site; - site = config.fudo.sites.${site-name}; - domain-name = host-cfg.domain; - domain = config.fudo.domains.${domain-name}; - has-build-servers = (length (attrNames site.build-servers)) > 0; - has-build-keys = (length host-cfg.build-pubkeys) > 0; - - in { - security.sudo.extraConfig = '' - # I get it, I get it - Defaults lecture = never - ''; - - networking = { - hostName = config.instance.hostname; - domain = domain-name; - nameservers = site.nameservers; - # This will cause a loop on the gateway itself - #defaultGateway = site.gateway-v4; - #defaultGateway6 = site.gateway-v6; - - firewall = mkIf ((length host-cfg.external-interfaces) > 0) { - enable = true; - allowedTCPPorts = [ 22 2112 ]; # Make sure _at least_ SSH is allowed - trustedInterfaces = let - all-interfaces = attrNames config.networking.interfaces; - in subtractLists host-cfg.external-interfaces all-interfaces; - }; - - hostId = mkIf (host-cfg.machine-id != null) - (substring 0 8 host-cfg.machine-id); - }; - - environment = { - etc = { - # NixOS generates a stupid hosts file, just force it - hosts = let - host-entries = mapAttrsToList - (ip: hostnames: "${ip} ${concatStringsSep " " hostnames}") - config.fudo.system.hostfile-entries; - in mkForce { - text = '' - 127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost - 127.0.0.2 ${hostname} localhost - ::1 ${hostname}.${domain-name} ${hostname} localhost - ${concatStringsSep "\n" host-entries} - ''; - user = "root"; - group = "root"; - mode = "0444"; - }; - - machine-id = mkIf (host-cfg.machine-id != null) { - text = host-cfg.machine-id; - user = "root"; - group = "root"; - mode = "0444"; - }; - - current-system-packages.text = with builtins; let - packages = map (p: "${p.name}") - config.environment.systemPackages; - sorted-unique = sort lessThan (unique packages); - in concatStringsSep "\n" sorted-unique; - - build-timestamp.text = toString config.instance.build-timestamp; - build-seed-hash.source = - generate-string-hash "build-seed" config.instance.build-seed; - }; - - systemPackages = with pkgs; - mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ]; - }; - - time.timeZone = site.timezone; - - krb5.libdefaults.default_realm = domain.gssapi-realm; - - services = { - cron.mailto = domain.admin-email; - fail2ban.ignoreIP = config.instance.local-networks; - }; - - virtualisation.docker = mkIf (host-cfg.docker-server) { - enable = true; - enableOnBoot = true; - autoPrune.enable = true; - }; - - programs.adb.enable = host-cfg.android-dev; - users.groups.adbusers = mkIf host-cfg.android-dev { - members = config.instance.local-admins; - }; - - boot.tmpOnTmpfs = host-cfg.tmp-on-tmpfs; - }; -} diff --git a/lib/fudo/hosts/local-network.nix b/lib/fudo/hosts/local-network.nix deleted file mode 100644 index f2de116..0000000 --- a/lib/fudo/hosts/local-network.nix +++ /dev/null @@ -1,143 +0,0 @@ -# THROW THIS AWAY, NOT USED - -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.hosts.local-network; - - # FIXME: this isn't used, is it? - gatewayServerOpts = { ... }: { - options = { - enable = mkEnableOption "Turn this host into a network gateway."; - - internal-interfaces = mkOption { - type = with types; listOf str; - description = - "List of internal interfaces from which to forward traffic."; - default = [ ]; - }; - - external-interface = mkOption { - type = types.str; - description = - "Interface facing public internet, to which traffic is forwarded."; - }; - - external-tcp-ports = mkOption { - type = with types; listOf port; - description = "List of TCP ports to open to the outside world."; - default = [ ]; - }; - - external-udp-ports = mkOption { - type = with types; listOf port; - description = "List of UDP ports to open to the outside world."; - default = [ ]; - }; - }; - }; - - dnsOverHttpsProxy = { - options = { - enable = mkEnableOption "Enable a DNS-over-HTTPS proxy server."; - - listen-port = mkOption { - type = types.port; - description = "Port on which to listen for DNS requests."; - default = 53; - }; - - upstream-dns = mkOption { - type = with types; listOf str; - description = "List of DoH DNS servers to use for recursion."; - default = [ ]; - }; - - bootstrap-dns = mkOption { - type = types.str; - description = "DNS server used to bootstrap the proxy server."; - default = "1.1.1.1"; - }; - }; - }; - - networkDhcpServerOpts = mkOption { - options = { - enable = mkEnableOption "Enable local DHCP server."; - - dns-servers = mkOption { - type = with types; listOf str; - description = "List of DNS servers for clients to use."; - default = [ ]; - }; - - listen-interfaces = mkOption { - type = with types; listOf str; - description = "List of interfaces on which to serve DHCP requests."; - default = [ ]; - }; - - server-ip = mkOption { - type = types.str; - description = "IP address of the server host."; - }; - }; - }; - - networkServerOpts = { - options = { - enable = mkEnableOption "Enable local networking server (DNS & DHCP)."; - - domain = mkOption { - type = types.str; - description = "Local network domain which this host will serve."; - }; - - dns-listen-addrs = mkOption { - type = with types; listOf str; - description = "List of IP addresses on which to listen for requests."; - default = [ ]; - }; - - dhcp = mkOption { - type = types.submodule networkDhcpServerOpts; - description = "Local DHCP server options."; - }; - }; - }; - -in { - options.fudo.hosts.local-network = with types; { - recursive-resolvers = mkOption { - type = listOf str; - description = "DNS server to use for recursive lookups."; - example = "1.2.3.4 port 53"; - }; - - gateway-server = mkOption { - type = submodule gatewayServerOpts; - description = "Gateway server options."; - }; - - dns-over-https-proxy = mkOption { - type = submodule dnsOverHttpsProxy; - description = "DNS-over-HTTPS proxy server."; - }; - - networkServerOpts = mkOption { - type = submodule networkServerOpts; - description = "Networking (DNS & DHCP) server for a local network."; - }; - }; - - config = { - fudo.secure-dns-proxy = mkIf cfg.dns-over-https-proxy.enable { - enable = true; - port = cfg.dns-over-https-proxy.listen-port; - upstream-dns = cfg.dns-over-https-proxy.upstream-dns; - bootstrap-dns = cfg.dns-over-https-proxy.bootstrap-dns; - listen-ips = cfg.dns-over-https-proxy.listen-ips; - }; - }; -} diff --git a/lib/fudo/include/rainloop.nix b/lib/fudo/include/rainloop.nix deleted file mode 100644 index 3172e6e..0000000 --- a/lib/fudo/include/rainloop.nix +++ /dev/null @@ -1,117 +0,0 @@ -lib: site: config: version: -with lib; -let - db-config = optionalString (config.database != null) - '' - type = "${config.database.type}" - pdo_dsn = "${config.database.type}:host=${config.database.hostname};port=${toString config.database.port};dbname=${config.database.name}" - pdo_user = "${config.database.user}" - pdo_password = "${fileContents config.database.password-file}" - ''; - -in '' - [webmail] - title = "${config.title}" - loading_description = "${config.title}" - favicon_url = "https://${site}/favicon.ico" - theme = "${config.theme}" - allow_themes = On - allow_user_background = Off - language = "en" - language_admin = "en" - allow_languages_on_settings = On - allow_additional_accounts = On - allow_additional_identities = On - messages_per_page = ${toString config.messages-per-page} - attachment_size_limit = ${toString config.max-upload-size} - - [interface] - show_attachment_thumbnail = On - new_move_to_folder_button = On - - [branding] - - [contacts] - enable = On - allow_sync = On - sync_interval = 20 - suggestions_limit = 10 - ${db-config} - - [security] - csrf_protection = On - custom_server_signature = "RainLoop" - x_frame_options_header = "" - openpgp = On - - admin_login = "admin" - admin_password = "" - allow_admin_panel = Off - allow_two_factor_auth = On - force_two_factor_auth = Off - hide_x_mailer_header = Off - admin_panel_host = "" - admin_panel_key = "admin" - content_security_policy = "" - core_install_access_domain = "" - - [login] - default_domain = "${config.domain}" - allow_languages_on_login = On - determine_user_language = On - determine_user_domain = Off - welcome_page = Off - hide_submit_button = On - - [plugins] - enable = Off - - [defaults] - view_editor_type = "${config.edit-mode}" - view_layout = ${if (config.layout-mode == "bottom") then "2" else "1"} - contacts_autosave = On - mail_use_threads = ${if config.enable-threading then "On" else "Off"} - allow_draft_autosave = On - mail_reply_same_folder = Off - show_images = On - - [logs] - enable = ${if config.debug then "On" else "Off"} - - [debug] - enable = ${if config.debug then "On" else "Off"} - hide_passwords = On - filename = "log-{date:Y-m-d}.txt" - - [social] - google_enable = Off - fb_enable = Off - twitter_enable = Off - dropbox_enable = Off - - [cache] - enable = On - index = "v1" - fast_cache_driver = "files" - fast_cache_index = "v1" - http = On - http_expires = 3600 - server_uids = On - - [labs] - allow_mobile_version = ${if config.enable-mobile then "On" else "Off"} - check_new_password_strength = On - allow_gravatar = On - allow_prefetch = On - allow_smart_html_links = On - cache_system_data = On - date_from_headers = On - autocreate_system_folders = On - allow_ctrl_enter_on_compose = On - favicon_status = On - use_local_proxy_for_external_images = On - detect_image_exif_orientation = On - - [version] - current = "${version}" -'' diff --git a/lib/fudo/initrd-network.nix b/lib/fudo/initrd-network.nix deleted file mode 100644 index a3c8c35..0000000 --- a/lib/fudo/initrd-network.nix +++ /dev/null @@ -1,87 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - initrd-cfg = config.fudo.hosts.${hostname}.initrd-network; - - read-lines = filename: splitString "\n" (fileContents filename); - - concatLists = lsts: concatMap (i: i) lsts; - - gen-sshfp-records-pkg = hostname: pubkey: let - pubkey-file = builtins.toFile "${hostname}-initrd-ssh-pubkey" pubkey; - in pkgs.stdenv.mkDerivation { - name = "${hostname}-initrd-ssh-firngerprint"; - - phases = [ "installPhase" ]; - - buildInputs = with pkgs; [ openssh ]; - - installPhase = '' - mkdir $out - ssh-keygen -r REMOVEME -f "${pubkey-file}" | sed 's/^REMOVEME IN SSHFP //' >> $out/initrd-ssh-pubkey.sshfp - ''; - }; - - gen-sshfp-records = hostname: pubkey: let - sshfp-record-pkg = gen-sshfp-records-pkg hostname pubkey; - in read-lines "${sshfp-record-pkg}/initrd-ssh-pubkey.sshfp"; - -in { - config = { - boot = mkIf (initrd-cfg != null) { - kernelParams = let - site = config.fudo.sites.${config.instance.local-site}; - site-gateway = site.gateway-v4; - netmask = - pkgs.lib.fudo.ip.maskFromV32Network site.network; - in [ - "ip=${initrd-cfg.ip}:${site-gateway}:${netmask}:${hostname}:${initrd-cfg.interface}" - ]; - initrd = { - network = { - enable = true; - - ssh = let - admin-ssh-keys = - concatMap (admin: config.fudo.users.${admin}.ssh-authorized-keys) - config.instance.local-admins; - in { - enable = true; - port = 22; - authorizedKeys = admin-ssh-keys; - hostKeys = [ - initrd-cfg.keypair.private-key-file - ]; - }; - }; - }; - }; - - fudo = { - local-network = let - initrd-network-hosts = - filterAttrs - (hostname: hostOpts: hostOpts.initrd-network != null) - config.instance.local-hosts; - in { - network-definition.hosts = mapAttrs' - (hostname: hostOpts: nameValuePair "${hostname}-recovery" - { - ipv4-address = hostOpts.initrd-network.ip; - description = "${hostname} initrd host"; - }) - initrd-network-hosts; - - extra-records = let - recs = (mapAttrsToList - (hostname: hostOpts: map - (sshfp: "${hostname} IN SSHFP ${sshfp}") - (gen-sshfp-records hostname hostOpts.initrd-network.keypair.public-key)) - initrd-network-hosts); - in concatLists recs; - }; - }; - }; -} diff --git a/lib/fudo/ipfs.nix b/lib/fudo/ipfs.nix deleted file mode 100644 index b60ac0c..0000000 --- a/lib/fudo/ipfs.nix +++ /dev/null @@ -1,66 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.ipfs; - - user-group-entry = group: user: - nameValuePair user { extraGroups = [ group ]; }; - -in { - options.fudo.ipfs = with types; { - enable = mkEnableOption "Fudo IPFS"; - - users = mkOption { - type = listOf str; - description = "List of users with IPFS access."; - default = [ ]; - }; - - user = mkOption { - type = str; - description = "User as which to run IPFS user."; - default = "ipfs"; - }; - - group = mkOption { - type = str; - description = "Group as which to run IPFS user."; - default = "ipfs"; - }; - - api-address = mkOption { - type = str; - description = "Address on which to listen for requests."; - default = "/ip4/127.0.0.1/tcp/5001"; - }; - - automount = mkOption { - type = bool; - description = "Whether to automount /ipfs and /ipns on boot."; - default = true; - }; - - data-dir = mkOption { - type = str; - description = "Path to store data for IPFS."; - default = "/var/lib/ipfs"; - }; - }; - - config = mkIf cfg.enable { - - users.users = - mapAttrs user-group-entry config.instance.local-users; - - services.ipfs = { - enable = true; - apiAddress = cfg.api-address; - autoMount = cfg.automount; - enableGC = true; - user = cfg.user; - group = cfg.group; - dataDir = cfg.data-dir; - }; - }; -} diff --git a/lib/fudo/jabber.nix b/lib/fudo/jabber.nix deleted file mode 100644 index a048063..0000000 --- a/lib/fudo/jabber.nix +++ /dev/null @@ -1,236 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - - siteOpts = { ... }: with types; { - options = { - enableACME = mkOption { - type = bool; - description = "Use ACME to get SSL certificates for this site."; - default = true; - }; - - site-config = mkOption { - type = attrs; - description = "Site-specific configuration."; - }; - }; - }; - - concatMapAttrs = f: attrs: - foldr (a: b: a // b) {} (mapAttrs f attrs); - - concatMapAttrsToList = f: attr: - concatMap (i: i) (attrValues (mapAttrs f attr)); - - host-domains = config.fudo.acme.host-domains.${hostname}; - - siteCerts = site: let - cert-copy = host-domains.${site}.local-copies.ejabberd; - in [ - cert-copy.certificate - cert-copy.private-key - cert-copy.chain - ]; - - siteCertService = site: - host-domains.${site}.local-copies.ejabberd.service; - - config-file-template = let - jabber-config = { - loglevel = cfg.log-level; - - access_rules = { - c2s = { allow = "all"; }; - announce = { allow = "admin"; }; - configure = { allow = "admin"; }; - pubsub_createnode = { allow = "local"; }; - }; - - acl = { - admin = { - user = concatMap - (admin: map (site: "${admin}@${site}") - (attrNames cfg.sites)) - cfg.admins; - }; - }; - - hosts = attrNames cfg.sites; - - listen = map (ip: { - port = cfg.port; - module = "ejabberd_c2s"; - ip = ip; - starttls = true; - starttls_required = true; - }) cfg.listen-ips; - - certfiles = concatMapAttrsToList - (site: siteOpts: - if (siteOpts.enableACME) then - (siteCerts site) - else []) - cfg.sites; - - host_config = - mapAttrs (site: siteOpts: siteOpts.site-config) - cfg.sites; - }; - - config-file = builtins.toJSON jabber-config; - in pkgs.writeText "ejabberd.config.yml.template" config-file; - - enter-secrets = template: secrets: target: let - secret-readers = concatStringsSep "\n" - (mapAttrsToList - (secret: file: "${secret}=$(cat ${file})") - secrets); - secret-swappers = map - (secret: "sed s/${secret}/\$${secret}/g") - (attrNames secrets); - swapper = concatStringsSep " | " secret-swappers; - in pkgs.writeShellScript "ejabberd-generate-config.sh" '' - cat ${template} | ${swapper} > ${target} - ''; - - cfg = config.fudo.jabber; - -in { - options.fudo.jabber = with types; { - enable = mkEnableOption "Enable ejabberd server."; - - listen-ips = mkOption { - type = listOf str; - description = "IPs on which to listen for Jabber connections."; - }; - - port = mkOption { - type = port; - description = "Port on which to listen for Jabber connections."; - default = 5222; - }; - - user = mkOption { - type = str; - description = "User as which to run the ejabberd server."; - default = "ejabberd"; - }; - - group = mkOption { - type = str; - description = "Group as which to run the ejabberd server."; - default = "ejabberd"; - }; - - admins = mkOption { - type = listOf str; - description = "List of admin users for the server."; - default = []; - }; - - sites = mkOption { - type = attrsOf (submodule siteOpts); - description = "List of sites on which to listen for Jabber connections."; - }; - - secret-files = mkOption { - type = attrsOf str; - description = "Map of secret-name to file. File contents will be subbed for the name in the config."; - default = {}; - }; - - config-file = mkOption { - type = str; - description = "Location at which to generate the configuration file."; - default = "/run/ejabberd/ejabberd.yaml"; - }; - - log-level = mkOption { - type = int; - description = '' - Log level at which to run the server. - - See: https://docs.ejabberd.im/admin/guide/troubleshooting/ - ''; - default = 3; - }; - - environment = mkOption { - type = attrsOf str; - description = "Environment variables to set for the ejabberd daemon."; - default = {}; - }; - }; - - config = mkIf cfg.enable { - users = { - users.${cfg.user} = { - isSystemUser = true; - }; - - groups.${cfg.group} = { - members = [ cfg.user ]; - }; - }; - - fudo = { - acme.host-domains.${hostname} = mapAttrs (site: siteCfg: - mkIf siteCfg.enableACME { - local-copies.ejabberd = { - user = cfg.user; - group = cfg.group; - }; - }) cfg.sites; - - system = let - config-dir = dirOf cfg.config-file; - in { - ensure-directories.${config-dir} = { - user = cfg.user; - perms = "0700"; - }; - - services.ejabberd-config-generator = let - config-generator = - enter-secrets config-file-template cfg.secret-files cfg.config-file; - in { - script = "${config-generator}"; - readWritePaths = [ config-dir ]; - workingDirectory = config-dir; - user = cfg.user; - description = "Generate ejabberd config file with necessary passwords."; - postStart = '' - chown ${cfg.user} ${cfg.config-file} - chmod 0400 ${cfg.config-file} - ''; - }; - }; - }; - - systemd = { - tmpfiles.rules = [ - "D '${dirOf cfg.config-file}' 0550 ${cfg.user} ${cfg.group} - -" - ]; - - services = { - ejabberd = { - wants = map (site: siteCertService site) (attrNames cfg.sites); - requires = [ "ejabberd-config-generator.service" ]; - environment = cfg.environment; - }; - }; - }; - - services.ejabberd = { - enable = true; - - user = cfg.user; - group = cfg.group; - - configFile = cfg.config-file; - }; - }; -} diff --git a/lib/fudo/kdc.nix b/lib/fudo/kdc.nix deleted file mode 100644 index c093080..0000000 --- a/lib/fudo/kdc.nix +++ /dev/null @@ -1,532 +0,0 @@ -{ config, lib, pkgs, ... } @ toplevel: - -with lib; -let - cfg = config.fudo.auth.kdc; - - 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"; - iprop-log = "${state-directory}/iprop.log"; - - master-server = cfg.master-config != null; - slave-server = cfg.slave-config != null; - - get-fqdn = hostname: - "${hostname}.${config.fudo.hosts.${hostname}.domain}"; - - kdc-conf = generate-kdc-conf { - realm = cfg.realm; - db-file = database-file; - key-file = cfg.master-key-file; - acl-data = if master-server then cfg.master-config.acl else null; - }; - - initialize-db = - { realm, user, group, kdc-conf, key-file, db-name, max-lifetime, max-renewal, - primary-keytab, kadmin-keytab, kpasswd-keytab, ipropd-keytab, local-hostname }: let - - kadmin-cmd = "kadmin -l -c ${kdc-conf} --"; - - get-domain-hosts = domain: let - host-in-subdomain = host: hostOpts: - (builtins.match "(.+[.])?${domain}$" hostOpts.domain) != null; - in attrNames (filterAttrs host-in-subdomain config.fudo.hosts); - - get-host-principals = realm: hostname: let - host = config.fudo.hosts.${hostname}; - in map (service: "${service}/${hostname}.${host.domain}@${realm}") - host.kerberos-services; - - add-principal-str = principal: - "${kadmin-cmd} add --random-key --use-defaults ${principal}"; - - test-existence = principal: - "[[ $( ${kadmin-cmd} get ${principal} ) ]]"; - - exists-or-add = principal: '' - if ${test-existence principal}; then - echo "skipping ${principal}, already exists" - else - ${add-principal-str principal} - fi - ''; - - ensure-host-principals = realm: - concatStringsSep "\n" - (map exists-or-add - (concatMap (get-host-principals realm) - (get-domain-hosts (toLower realm)))); - - slave-hostnames = map get-fqdn cfg.master-config.slave-hosts; - - ensure-iprop-principals = concatStringsSep "\n" - (map (host: exists-or-add "iprop/${host}@${realm}") - [ local-hostname ] ++ slave-hostnames); - - copy-slave-principals-file = let - slave-principals = map - (host: "iprop/${hostname}@${cfg.realm}") - slave-hostnames; - slave-principals-file = pkgs.writeText "heimdal-slave-principals" - (concatStringsSep "\n" slave-principals); - in optionalString (slave-principals-file != null) '' - cp ${slave-principals-file} ${state-directory}/slaves - # Since it's copied from /nix/store, this is by default read-only, - # which causes updates to fail. - chmod u+w ${state-directory}/slaves - ''; - - in pkgs.writeShellScript "initialize-kdc-db.sh" '' - TMP=$(mktemp -d -t kdc-XXXXXXXX) - if [ ! -e ${database-file} ]; then - ## CHANGING HOW THIS WORKS - ## Now we expect the key to be provided - # kstash --key-file=${key-file} --random-key - ${kadmin-cmd} init --realm-max-ticket-life="${max-lifetime}" --realm-max-renewable-life="${max-renewal}" ${realm} - fi - - ${ensure-host-principals realm} - - ${ensure-iprop-principals} - - echo "*** BEGIN EXTRACTING KEYTABS" - echo "*** You can probably ignore the 'principal does not exist' errors that follow," - echo "*** they're just testing for principal existence before creating those that" - echo "*** don't already exist" - - ${kadmin-cmd} ext_keytab --keytab=$TMP/primary.keytab */${local-hostname}@${realm} - mv $TMP/primary.keytab ${primary-keytab} - ${kadmin-cmd} ext_keytab --keytab=$TMP/kadmin.keytab kadmin/admin@${realm} - mv $TMP/kadmin.keytab ${kadmin-keytab} - ${kadmin-cmd} ext_keytab --keytab=$TMP/kpasswd.keytab kadmin/changepw@${realm} - mv $TMP/kpasswd.keytab ${kpasswd-keytab} - ${kadmin-cmd} ext_keytab --keytab=$TMP/ipropd.keytab iprop/${local-hostname}@${realm} - mv $TMP/ipropd.keytab ${ipropd-keytab} - - echo "*** END EXTRACTING KEYTABS" - - ${copy-slave-principals-file} - ''; - - generate-kdc-conf = { realm, db-file, key-file, acl-data }: - pkgs.writeText "kdc.conf" '' - [kdc] - database = { - dbname = sqlite:${db-file} - realm = ${realm} - mkey_file = ${key-file} - ${optionalString (acl-data != null) - "acl_file = ${generate-acl-file acl-data}"} - log_file = ${iprop-log} - } - - [realms] - ${realm} = { - enable-http = false - } - - [logging] - kdc = FILE:${state-directory}/kerberos.log - default = FILE:${state-directory}/kerberos.log - ''; - - aclEntry = { principal, ... }: { - options = with types; { - perms = let - perms = [ - "change-password" - "add" - "list" - "delete" - "modify" - "get" - "get-keys" - "all" - ]; - in mkOption { - type = listOf (enum perms); - description = "List of permissions."; - default = [ ]; - }; - - target = mkOption { - type = nullOr str; - description = "Target principals."; - default = null; - example = "hosts/*@REALM.COM"; - }; - }; - }; - - generate-acl-file = acl-entries: let - perms-to-permstring = perms: concatStringsSep "," perms; - in - pkgs.writeText "kdc.acl" (concatStringsSep "\n" (mapAttrsToList - (principal: opts: - "${principal} ${perms-to-permstring opts.perms}${ - optionalString (opts.target != null) " ${opts.target}" }") - acl-entries)); - - kadmin-local = kdc-conf: - pkgs.writeShellScriptBin "kadmin.local" '' - ${pkgs.heimdalFull}/bin/kadmin -l -c ${kdc-conf} $@ - ''; - - masterOpts = { ... }: { - options = with types; { - acl = mkOption { - type = attrsOf (submodule aclEntry); - description = "Mapping of pricipals to a list of permissions."; - default = { "*/admin" = [ "all" ]; }; - example = { - "*/root" = [ "all" ]; - "admin-user" = [ "add" "list" "modify" ]; - }; - }; - - kadmin-keytab = mkOption { - type = str; - description = "Location at which to store keytab for kadmind."; - default = "${state-directory}/kadmind.keytab"; - }; - - kpasswdd-keytab = mkOption { - type = str; - description = "Location at which to store keytab for kpasswdd."; - default = "${state-directory}/kpasswdd.keytab"; - }; - - ipropd-keytab = mkOption { - type = str; - description = "Location at which to store keytab for ipropd master."; - default = "${state-directory}/ipropd.keytab"; - }; - - slave-hosts = mkOption { - type = listOf str; - description = '' - A list of host to which the database should be propagated. - - Must exist in the Fudo Host database. - ''; - default = [ ]; - }; - }; - }; - - slaveOpts = { ... }: { - options = with types; { - master-host = mkOption { - type = str; - description = '' - Host from which to recieve database updates. - - Must exist in the Fudo Host database. - ''; - }; - - ipropd-keytab = mkOption { - type = str; - description = "Location at which to find keytab for ipropd slave."; - default = "${state-directory}/ipropd.keytab"; - }; - }; - }; - -in { - - options.fudo.auth.kdc = with types; { - enable = mkEnableOption "Fudo KDC"; - - realm = mkOption { - type = str; - description = "The realm for which we are the acting KDC."; - }; - - bind-addresses = mkOption { - type = listOf str; - description = "A list of IP addresses on which to bind."; - default = host-ips; - }; - - user = mkOption { - type = str; - description = "User as which to run Heimdal servers."; - default = "kerberos"; - }; - - group = mkOption { - type = str; - description = "Group as which to run Heimdal servers."; - default = "kerberos"; - }; - - state-directory = mkOption { - type = str; - description = "Path at which to store kerberos database."; - default = "/var/lib/kerberos"; - }; - - master-key-file = mkOption { - type = str; - description = '' - File containing the master key for the realm. - - Must be provided! - ''; - }; - - primary-keytab = mkOption { - type = str; - description = "Location of host master keytab."; - default = "${state-directory}/host.keytab"; - }; - - master-config = mkOption { - type = nullOr (submodule masterOpts); - description = "Configuration for the master KDC server."; - default = null; - }; - - slave-config = mkOption { - type = nullOr (submodule slaveOpts); - description = "Configuration for slave KDC servers."; - default = null; - }; - - max-ticket-lifetime = mkOption { - type = str; - description = "Maximum lifetime of a single ticket in this realm."; - default = "1d"; - }; - - max-ticket-renewal = mkOption { - type = str; - description = "Maximum time a ticket may be renewed in this realm."; - default = "7d"; - }; - }; - - config = mkIf cfg.enable { - - assertions = [ - { - assertion = master-server || slave-server; - message = - "For the KDC to be enabled, a master OR slave config must be provided."; - } - { - assertion = !(master-server && slave-server); - message = - "Only one of master-config and slave-config may be provided."; - } - ]; - - users = { - users.${cfg.user} = { - isSystemUser = true; - home = state-directory; - group = cfg.group; - }; - - groups.${cfg.group} = { members = [ cfg.user ]; }; - }; - - krb5 = { - libdefaults = { - # Stick to ~/.k5login - # k5login_directory = cfg.k5login-directory; - ticket_lifetime = cfg.max-ticket-lifetime; - renew_lifetime = cfg.max-ticket-renewal; - }; - # Sorry, port 80 isn't available! - realms.${cfg.realm}.enable-http = false; - extraConfig = '' - default = FILE:${state-directory}/kerberos.log - ''; - }; - - environment = { - systemPackages = [ pkgs.heimdalFull (kadmin-local kdc-conf) ]; - - ## This shouldn't be necessary...every host gets a krb5.keytab - # etc = { - # "krb5.keytab" = { - # user = "root"; - # group = "root"; - # mode = "0400"; - # source = cfg.primary-keytab; - # }; - # }; - }; - - fudo.system = { - ensure-directories = { - "${state-directory}" = { - user = cfg.user; - group = cfg.group; - perms = "0740"; - }; - }; - - services = if master-server then { - - heimdal-kdc = let - listen-addrs = concatStringsSep " " - (map (addr: "--addresses=${addr}") cfg.bind-addresses); - in { - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - description = - "Heimdal Kerberos Key Distribution Center (ticket server)."; - execStart = "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; - user = cfg.user; - group = cfg.group; - workingDirectory = state-directory; - privateNetwork = false; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; - }; - - heimdal-kdc-init = let - init-cmd = initialize-db { - realm = cfg.realm; - user = cfg.user; - group = cfg.group; - kdc-conf = kdc-conf; - key-file = cfg.master-key-file; - db-name = database-file; - max-lifetime = cfg.max-ticket-lifetime; - max-renewal = cfg.max-ticket-renewal; - primary-keytab = cfg.primary-keytab; - kadmin-keytab = cfg.master-config.kadmin-keytab; - kpasswd-keytab = cfg.master-config.kpasswdd-keytab; - ipropd-keytab = cfg.master-config.ipropd-keytab; - local-hostname = - "${config.instance.hostname}.${config.instance.local-domain}"; - }; - in { - requires = [ "heimdal-kdc.service" ]; - wantedBy = [ "multi-user.target" ]; - description = "Initialization script for Heimdal KDC."; - type = "oneshot"; - execStart = "${init-cmd}"; - user = cfg.user; - group = cfg.group; - path = with pkgs; [ heimdalFull ]; - protectSystem = "full"; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - workingDirectory = state-directory; - environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; - }; - - heimdal-ipropd-master = mkIf (length cfg.master-config.slave-hosts > 0) { - requires = [ "heimdal-kdc.service" ]; - wantedBy = [ "multi-user.target" ]; - description = "Propagate changes to the master KDC DB to all slaves."; - path = with pkgs; [ heimdalFull ]; - execStart = "${pkgs.heimdalFull}/libexec/heimdal/ipropd-master -c ${kdc-conf} -k ${cfg.master.ipropd-keytab}"; - user = cfg.user; - group = cfg.group; - workingDirectory = state-directory; - privateNetwork = false; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; - }; - - } else { - - heimdal-kdc-slave = let - listen-addrs = concatStringsSep " " - (map (addr: "--addresses=${addr}") cfg.bind-addresses); - command = - "${pkgs.heimdalFull}/libexec/heimdal/kdc -c ${kdc-conf} --ports=88 ${listen-addrs}"; - in { - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - description = - "Heimdal Slave Kerberos Key Distribution Center (ticket server)."; - execStart = command; - 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-ipropd-slave = { - wantedBy = [ "multi-user.target" ]; - description = "Receive changes propagated from the KDC master server."; - path = with pkgs; [ heimdalFull ]; - execStart = concatStringsSep " " [ - "${pkgs.heimdalFull}/libexec/heimdal/ipropd-slave" - "--config-file=${kdc-conf}" - "--keytab=${cfg.slave-config.ipropd-keytab}" - "--realm=${cfg.realm}" - "--hostname=${get-fqdn hostname}" - "--port=2121" - "--verbose" - (get-fqdn cfg.slave-config.master-host) - ]; - user = cfg.user; - group = cfg.group; - workingDirectory = state-directory; - privateNetwork = false; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - requiredCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - environment = { KRB5_CONFIG = "/etc/krb5.conf"; }; - }; - }; - }; - - services.xinetd = mkIf master-server { - enable = true; - - services = [ - { - name = "kerberos-adm"; - user = cfg.user; - server = "${pkgs.heimdalFull}/libexec/heimdal/kadmind"; - protocol = "tcp"; - serverArgs = - "--config-file=${kdc-conf} --keytab=${cfg.master-config.kadmin-keytab}"; - } - { - name = "kpasswd"; - user = cfg.user; - server = "${pkgs.heimdalFull}/libexec/heimdal/kpasswdd"; - protocol = "udp"; - serverArgs = - "--config-file=${kdc-conf} --keytab=${cfg.master-config.kpasswdd-keytab}"; - } - ]; - }; - - networking = { - firewall = { - allowedTCPPorts = [ 88 ] ++ - (optionals master-server [ 749 ]) ++ - (optionals slave-server [ 2121 ]); - allowedUDPPorts = [ 88 ] ++ - (optionals master-server [ 464 ]) ++ - (optionals slave-server [ 2121 ]); - }; - }; - }; -} diff --git a/lib/fudo/ldap.nix b/lib/fudo/ldap.nix deleted file mode 100644 index ebb4bab..0000000 --- a/lib/fudo/ldap.nix +++ /dev/null @@ -1,460 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - - cfg = config.fudo.auth.ldap-server; - - user-type = import ../types/user.nix { inherit lib; }; - - 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}" - else - "/home/${user-opts.primary-group}/${username}"; - - userLdif = base: name: group-map: opts: '' - dn: uid=${name},ou=members,${base} - uid: ${name} - objectClass: account - objectClass: shadowAccount - objectClass: posixAccount - cn: ${opts.common-name} - uidNumber: ${toString (opts.uid)} - gidNumber: ${toString (getUserGidNumber opts group-map)} - homeDirectory: ${mkHomeDir name opts} - description: ${opts.description} - shadowLastChange: 12230 - shadowMax: 99999 - shadowWarning: 7 - userPassword: ${opts.ldap-hashed-passwd} - ''; - - systemUserLdif = base: name: opts: '' - dn: cn=${name},${base} - objectClass: organizationalRole - objectClass: simpleSecurityObject - cn: ${name} - description: ${opts.description} - userPassword: ${opts.ldap-hashed-password} - ''; - - toMemberList = userList: - stringJoin "\n" (map (username: "memberUid: ${username}") userList); - - groupLdif = base: name: opts: '' - dn: cn=${name},ou=groups,${base} - objectClass: posixGroup - cn: ${name} - gidNumber: ${toString (opts.gid)} - description: ${opts.description} - ${toMemberList opts.members} - ''; - - systemUsersLdif = base: user-map: - stringJoin "\n" - (mapAttrsToList (name: opts: systemUserLdif base name opts) user-map); - - groupsLdif = base: group-map: - stringJoin "\n" - (mapAttrsToList (name: opts: groupLdif base name opts) group-map); - - usersLdif = base: group-map: user-map: - stringJoin "\n" - (mapAttrsToList (name: opts: userLdif base name group-map opts) user-map); - -in { - - options = with types; { - fudo = { - auth = { - ldap-server = { - enable = mkEnableOption "Fudo Authentication"; - - kerberos-host = mkOption { - type = str; - description = '' - The name of the host to use for Kerberos authentication. - ''; - }; - - kerberos-keytab = mkOption { - type = str; - description = '' - The path to a keytab for the LDAP server, containing a principal for ldap/. - ''; - }; - - ssl-certificate = mkOption { - type = str; - description = '' - The path to the SSL certificate to use for the server. - ''; - }; - - 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 = '' - The path to the SSL key to use for the server. - ''; - }; - - ssl-ca-certificate = mkOption { - type = nullOr str; - description = '' - The path to the SSL CA cert used to sign the certificate. - ''; - default = null; - }; - - organization = mkOption { - type = str; - description = '' - The name to use for the organization. - ''; - }; - - base = mkOption { - type = str; - description = "The base dn of the LDAP server."; - example = "dc=fudo,dc=org"; - }; - - rootpw-file = mkOption { - default = ""; - type = str; - description = '' - The path to a file containing the root password for this database. - ''; - }; - - listen-uris = mkOption { - type = listOf str; - description = '' - A list of URIs on which the ldap server should listen. - ''; - example = [ "ldap://auth.fudo.org" "ldaps://auth.fudo.org" ]; - }; - - users = mkOption { - type = attrsOf (submodule user-type.userOpts); - example = { - tester = { - uid = 10099; - common-name = "Joe Blow"; - hashed-password = ""; - }; - }; - description = '' - Users to be added to the Fudo LDAP database. - ''; - default = { }; - }; - - groups = mkOption { - default = { }; - type = attrsOf (submodule user-type.groupOpts); - example = { - admin = { - gid = 1099; - members = [ "tester" ]; - }; - }; - description = '' - Groups to be added to the Fudo LDAP database. - ''; - }; - - system-users = mkOption { - default = { }; - type = attrsOf (submodule user-type.systemUserOpts); - example = { - replicator = { - description = "System user for database sync"; - ldap-hashed-password = ""; - }; - }; - description = "System users to be added to the Fudo LDAP database."; - }; - - state-directory = mkOption { - type = str; - 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 = [ ]; - }; - }; - }; - }; - }; - - config = mkIf cfg.enable { - - environment = { - etc = { - "openldap/sasl2/slapd.conf" = { - mode = "0400"; - user = config.services.openldap.user; - group = config.services.openldap.group; - text = '' - mech_list: gssapi external - keytab: ${cfg.kerberos-keytab} - ''; - }; - }; - }; - - 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; - }; - }; - }; - - services.openldap = { - enable = true; - urlList = cfg.listen-uris; - - settings = let - makePermEntry = dn: perm: "by ${dn} ${perm}"; - - makeAccessLine = target: perm-map: let - perm-entries = mapAttrsToList makePermEntry perm-map; - in "to ${target} ${concatStringsSep " " perm-entries}"; - - makeAccess = access-map: let - access-lines = mapAttrsToList makeAccessLine; - numbered-access-lines = imap0 (i: line: "{${toString i}}${line}"); - in numbered-access-lines (access-lines access-map); - - in { - attrs = { - cn = "config"; - objectClass = "olcGlobal"; - olcPidFile = "/run/slapd/slapd.pid"; - olcTLSCertificateFile = cfg.ssl-certificate; - olcTLSCertificateKeyFile = cfg.ssl-private-key; - olcTLSCACertificateFile = ca-path; - olcSaslSecProps = "noplain,noanonymous"; - olcAuthzRegexp = let - authz-regex-entry = i: { regex, target }: - "{${toString i}}\"${regex}\" \"${target}\""; - in imap0 authz-regex-entry [ - { - regex = "^uid=auth/([^.]+).fudo.org,cn=fudo.org,cn=gssapi,cn=auth$"; - target = "cn=$1,ou=hosts,dc=fudo,dc=org"; - } - { - regex = "^uid=[^,/]+/root,cn=fudo.org,cn=gssapi,cn=auth$"; - target = "cn=admin,dc=fudo,dc=org"; - } - { - regex = "^uid=([^,/]+),cn=fudo.org,cn=gssapi,cn=auth$"; - target = "uid=$1,ou=members,dc=fudo,dc=org"; - } - { - regex = "^uid=host/([^,/]+),cn=fudo.org,cn=gssapi,cn=auth$"; - target = "cn=$1,ou=hosts,dc=fudo,dc=org"; - } - { - regex = "^gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth$"; - target = "cn=admin,dc=fudo,dc=org"; - } - ]; - }; - children = { - "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"; - "*" = "none"; - }; - }; - }; - }; - "olcDatabase={0}config" = { - attrs = { - objectClass = [ "olcDatabaseConfig" ]; - olcDatabase = "{0}config"; - olcAccess = makeAccess { - "*" = { - "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "*" = "none"; - }; - }; - }; - }; - "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.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"; - "dn.exact=cn=auth_reader,${cfg.base}" = "read"; - "dn.exact=cn=replicator,${cfg.base}" = "read"; - "self" = "write"; - "*" = "auth"; - }; - "dn=cn=admin,ou=groups,${cfg.base}" = { - "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "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.regex=cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${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=user_db_reader,${cfg.base}" = "read"; - "users" = "read"; - "*" = "none"; - }; - "*" = { - "dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" = "manage"; - "users" = "read"; - "*" = "none"; - }; - }; - }; - }; - }; - }; - - declarativeContents = { - "dc=fudo,dc=org" = '' - dn: ${cfg.base} - objectClass: top - objectClass: dcObject - objectClass: organization - o: ${cfg.organization} - - dn: ou=groups,${cfg.base} - objectClass: organizationalUnit - description: ${cfg.organization} groups - - dn: ou=members,${cfg.base} - objectClass: organizationalUnit - description: ${cfg.organization} members - - dn: cn=admin,${cfg.base} - objectClass: organizationalRole - cn: admin - description: "Admin User" - - ${systemUsersLdif cfg.base cfg.system-users} - ${groupsLdif cfg.base cfg.groups} - ${usersLdif cfg.base cfg.groups cfg.users} - ''; - }; - }; - }; -} diff --git a/lib/fudo/local-network.nix b/lib/fudo/local-network.nix deleted file mode 100644 index cbb7f44..0000000 --- a/lib/fudo/local-network.nix +++ /dev/null @@ -1,238 +0,0 @@ -{ lib, config, pkgs, ... }: - -with lib; -let - cfg = config.fudo.local-network; - - join-lines = concatStringsSep "\n"; - - traceout = out: builtins.trace out out; - -in { - - options.fudo.local-network = with types; { - - enable = mkEnableOption "Enable local network configuration (DHCP & DNS)."; - - domain = mkOption { - type = str; - description = "The domain to use for the local network."; - }; - - dns-servers = mkOption { - type = listOf str; - description = "A list of domain name servers to pass to local clients."; - }; - - dhcp-interfaces = mkOption { - type = listOf str; - description = "A list of interfaces on which to serve DHCP."; - }; - - dns-listen-ips = mkOption { - type = listOf str; - description = "A list of IPs on which to server DNS queries."; - }; - - gateway = mkOption { - type = str; - description = "The gateway to use for the local network."; - }; - - network = mkOption { - type = str; - description = "Network to treat as local."; - example = "10.0.0.0/16"; - }; - - dhcp-dynamic-network = mkOption { - type = str; - description = '' - The network from which to dynamically allocate IPs via DHCP. - - Must be a subnet of . - ''; - example = "10.0.1.0/24"; - }; - - enable-reverse-mappings = mkOption { - type = bool; - description = "Genereate PTR reverse lookup records."; - default = false; - }; - - recursive-resolver = mkOption { - type = str; - description = "DNS nameserver to use for recursive resolution."; - default = "1.1.1.1 port 53"; - }; - - search-domains = mkOption { - type = listOf str; - description = "A list of domains which clients should consider local."; - example = [ "my-domain.com" "other-domain.com" ]; - default = [ ]; - }; - - network-definition = let - networkOpts = import ../types/network-definition.nix { inherit lib; }; - in mkOption { - type = submodule networkOpts; - description = "Definition of network to be served by local server."; - default = { }; - }; - - extra-records = mkOption { - type = listOf str; - description = "Extra records to add to the local zone."; - default = [ ]; - }; - }; - - config = mkIf cfg.enable { - - fudo.system.hostfile-entries = let - other-hosts = filterAttrs - (hostname: hostOpts: hostname != config.instance.hostname) - cfg.network-definition.hosts; - in mapAttrs' (hostname: hostOpts: - nameValuePair hostOpts.ipv4-address ["${hostname}.${cfg.domain}" hostname]) - other-hosts; - - services.dhcpd4 = let network = cfg.network-definition; - in { - enable = true; - - machines = mapAttrsToList (hostname: hostOpts: { - ethernetAddress = hostOpts.mac-address; - hostName = hostname; - ipAddress = hostOpts.ipv4-address; - }) (filterAttrs (host: hostOpts: - hostOpts.mac-address != null && hostOpts.ipv4-address != null) - network.hosts); - - interfaces = cfg.dhcp-interfaces; - - extraConfig = '' - subnet ${pkgs.lib.fudo.ip.getNetworkBase cfg.network} netmask ${ - pkgs.lib.fudo.ip.maskFromV32Network cfg.network - } { - authoritative; - option subnet-mask ${pkgs.lib.fudo.ip.maskFromV32Network cfg.network}; - option broadcast-address ${pkgs.lib.fudo.ip.networkMaxIp cfg.network}; - option routers ${cfg.gateway}; - option domain-name-servers ${concatStringsSep " " cfg.dns-servers}; - option domain-name "${cfg.domain}"; - option domain-search "${ - concatStringsSep " " ([ cfg.domain ] ++ cfg.search-domains) - }"; - range ${pkgs.lib.fudo.ip.networkMinIp cfg.dhcp-dynamic-network} ${ - pkgs.lib.fudo.ip.networkMaxButOneIp cfg.dhcp-dynamic-network - }; - } - ''; - }; - - services.bind = let - blockHostsToZone = block: hosts-data: { - master = true; - name = "${block}.in-addr.arpa"; - file = let - # We should add these...but need a domain to assign them to. - # ip-last-el = ip: toInt (last (splitString "." ip)); - # used-els = map (host-data: ip-last-el host-data.ipv4-address) hosts-data; - # unused-els = subtractLists used-els (map toString (range 1 255)); - - in pkgs.writeText "db.${block}-zone" '' - $ORIGIN ${block}.in-addr.arpa. - $TTL 1h - - @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. ( - ${toString config.instance.build-timestamp} - 1800 - 900 - 604800 - 1800) - - @ IN NS ns1.${cfg.domain}. - - ${join-lines (map hostPtrRecord hosts-data)} - ''; - }; - - ipToBlock = ip: - concatStringsSep "." (reverseList (take 3 (splitString "." ip))); - compactHosts = - mapAttrsToList (host: data: data // { host = host; }) network.hosts; - hostsByBlock = - groupBy (host-data: ipToBlock host-data.ipv4-address) compactHosts; - hostPtrRecord = host-data: - "${ - last (splitString "." host-data.ipv4-address) - } IN PTR ${host-data.host}.${cfg.domain}."; - - blockZones = mapAttrsToList blockHostsToZone hostsByBlock; - - hostARecord = host: data: "${host} IN A ${data.ipv4-address}"; - hostSshFpRecords = host: data: - let - ssh-fingerprints = if (hasAttr host known-hosts) then - known-hosts.${host}.ssh-fingerprints - else - [ ]; - in join-lines - (map (sshfp: "${host} IN SSHFP ${sshfp}") ssh-fingerprints); - cnameRecord = alias: host: "${alias} IN CNAME ${host}"; - - network = cfg.network-definition; - - known-hosts = config.fudo.hosts; - - in { - enable = true; - cacheNetworks = [ cfg.network "localhost" "localnets" ]; - forwarders = [ cfg.recursive-resolver ]; - listenOn = cfg.dns-listen-ips; - extraOptions = concatStringsSep "\n" [ - "dnssec-enable yes;" - "dnssec-validation yes;" - "auth-nxdomain no;" - "recursion yes;" - "allow-recursion { any; };" - ]; - zones = [{ - master = true; - name = cfg.domain; - file = pkgs.writeText "${cfg.domain}-zone" '' - @ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. ( - ${toString config.instance.build-timestamp} - 5m - 2m - 6w - 5m) - - $TTL 1h - - @ IN NS ns1.${cfg.domain}. - - $ORIGIN ${cfg.domain}. - - $TTL 30m - - ${optionalString (network.gssapi-realm != null) - ''_kerberos IN TXT "${network.gssapi-realm}"''} - - ${join-lines - (imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}") - cfg.dns-servers)} - ${join-lines (mapAttrsToList hostARecord network.hosts)} - ${join-lines (mapAttrsToList hostSshFpRecords network.hosts)} - ${join-lines (mapAttrsToList cnameRecord network.aliases)} - ${join-lines network.verbatim-dns-records} - ${pkgs.lib.fudo.dns.srvRecordsToBindZone network.srv-records} - ${join-lines cfg.extra-records} - ''; - }] ++ blockZones; - }; - }; -} diff --git a/lib/fudo/mail-container.nix b/lib/fudo/mail-container.nix deleted file mode 100644 index 8a890ab..0000000 --- a/lib/fudo/mail-container.nix +++ /dev/null @@ -1,221 +0,0 @@ -{ pkgs, lib, config, ... }: -with lib; -let - hostname = config.instance.hostname; - cfg = config.fudo.mail-server; - container-maildir = "/var/lib/mail"; - container-statedir = "/var/lib/mail-state"; - - # Don't bother with group-id, nixos doesn't seem to use it anyway - container-mail-user = "mailer"; - container-mail-user-id = 542; - container-mail-group = "mailer"; - - build-timestamp = config.instance.build-timestamp; - build-seed = config.instance.build-seed; - site = config.instance.local-site; - domain = cfg.domain; - - local-networks = config.instance.local-networks; - -in rec { - config = mkIf (cfg.enableContainer) { - # Disable postfix on this host--it'll be run in the container instead - services.postfix.enable = false; - - services.nginx = mkIf cfg.monitoring { - enable = true; - - virtualHosts = let - proxy-headers = '' - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - ''; - trusted-network-string = - optionalString ((length local-networks) > 0) - (concatStringsSep "\n" - (map (network: "allow ${network};") - local-networks)) + '' - - deny all;''; - - in { - "${cfg.mail-hostname}" = { - enableACME = true; - forceSSL = true; - - locations."/metrics/postfix" = { - proxyPass = "http://127.0.0.1:9154/metrics"; - - extraConfig = '' - ${proxy-headers} - - ${trusted-network-string} - ''; - }; - - locations."/metrics/dovecot" = { - proxyPass = "http://127.0.0.1:9166/metrics"; - - extraConfig = '' - ${proxy-headers} - - ${trusted-network-string} - ''; - }; - - locations."/metrics/rspamd" = { - proxyPass = "http://127.0.0.1:7980/metrics"; - - extraConfig = '' - ${proxy-headers} - - ${trusted-network-string} - ''; - }; - }; - }; - }; - - containers.mail-server = { - - autoStart = true; - - bindMounts = { - "${container-maildir}" = { - hostPath = cfg.mail-directory; - isReadOnly = false; - }; - - "${container-statedir}" = { - hostPath = cfg.state-directory; - isReadOnly = false; - }; - - "/run/mail/certs/postfix/cert.pem" = { - hostPath = cfg.ssl.certificate; - isReadOnly = true; - }; - - "/run/mail/certs/postfix/key.pem" = { - hostPath = cfg.ssl.private-key; - isReadOnly = true; - }; - - "/run/mail/certs/dovecot/cert.pem" = { - hostPath = cfg.ssl.certificate; - isReadOnly = true; - }; - - "/run/mail/certs/dovecot/key.pem" = { - hostPath = cfg.ssl.private-key; - isReadOnly = true; - }; - - "/run/mail/passwords/dovecot/ldap-reader.passwd" = { - hostPath = cfg.dovecot.ldap.reader-password-file; - isReadOnly = true; - }; - }; - - config = { config, pkgs, ... }: { - - imports = let - initialize-host = import ../../initialize.nix; - profile = "container"; - in [ - ./mail.nix - - (initialize-host { - inherit - lib - pkgs - build-timestamp - site - domain - profile; - hostname = "mail-container"; - }) - ]; - - instance.build-seed = build-seed; - - environment.etc = { - "mail-server/postfix/cert.pem" = { - source = "/run/mail/certs/postfix/cert.pem"; - user = config.services.postfix.user; - mode = "0444"; - }; - "mail-server/postfix/key.pem" = { - source = "/run/mail/certs/postfix/key.pem"; - user = config.services.postfix.user; - mode = "0400"; - }; - "mail-server/dovecot/cert.pem" = { - source = "/run/mail/certs/dovecot/cert.pem"; - user = config.services.dovecot2.user; - mode = "0444"; - }; - "mail-server/dovecot/key.pem" = { - source = "/run/mail/certs/dovecot/key.pem"; - user = config.services.dovecot2.user; - mode = "0400"; - }; - - ## The pre-script runs as root anyway... - # "mail-server/dovecot/ldap-reader.passwd" = { - # source = "/run/mail/passwords/dovecot/ldap-reader.passwd"; - # user = config.services.dovecot2.user; - # mode = "0400"; - # }; - }; - - fudo = { - - mail-server = { - enable = true; - mail-hostname = cfg.mail-hostname; - domain = cfg.domain; - - debug = cfg.debug; - monitoring = cfg.monitoring; - - state-directory = container-statedir; - mail-directory = container-maildir; - - postfix = { - ssl-certificate = "/etc/mail-server/postfix/cert.pem"; - ssl-private-key = "/etc/mail-server/postfix/key.pem"; - }; - - dovecot = { - ssl-certificate = "/etc/mail-server/dovecot/cert.pem"; - ssl-private-key = "/etc/mail-server/dovecot/key.pem"; - ldap = { - server-urls = cfg.dovecot.ldap.server-urls; - reader-dn = cfg.dovecot.ldap.reader-dn; - reader-password-file = "/run/mail/passwords/dovecot/ldap-reader.passwd"; - }; - }; - - local-domains = cfg.local-domains; - - alias-users = cfg.alias-users; - user-aliases = cfg.user-aliases; - sender-blacklist = cfg.sender-blacklist; - recipient-blacklist = cfg.recipient-blacklist; - trusted-networks = cfg.trusted-networks; - - mail-user = container-mail-user; - mail-user-id = container-mail-user-id; - mail-group = container-mail-group; - - clamav.enable = cfg.clamav.enable; - - dkim.signing = cfg.dkim.signing; - }; - }; - }; - }; - }; -} diff --git a/lib/fudo/mail.nix b/lib/fudo/mail.nix deleted file mode 100644 index e31d948..0000000 --- a/lib/fudo/mail.nix +++ /dev/null @@ -1,225 +0,0 @@ -{ config, lib, pkgs, environment, ... }: - -with lib; -let - inherit (lib.strings) concatStringsSep; - cfg = config.fudo.mail-server; - -in { - - options.fudo.mail-server = with types; { - enable = mkEnableOption "Fudo Email Server"; - - enableContainer = mkEnableOption '' - Run the mail server in a container. - - Mutually exclusive with mail-server.enable. - ''; - - domain = mkOption { - type = str; - description = "The main and default domain name for this email server."; - }; - - mail-hostname = mkOption { - type = str; - description = "The domain name to use for the mail server."; - }; - - ldap-url = mkOption { - type = str; - description = "URL of the LDAP server to use for authentication."; - example = "ldaps://auth.fudo.org/"; - }; - - monitoring = mkEnableOption "Enable monitoring for the mail server."; - - mail-user = mkOption { - type = str; - description = "User to use for mail delivery."; - default = "mailuser"; - }; - - # No group id, because NixOS doesn't seem to use it - mail-group = mkOption { - type = str; - description = "Group to use for mail delivery."; - default = "mailgroup"; - }; - - mail-user-id = mkOption { - type = int; - description = "UID of mail-user."; - }; - - local-domains = mkOption { - type = listOf str; - description = "A list of domains for which we accept mail."; - default = ["localhost" "localhost.localdomain"]; - example = [ - "localhost" - "localhost.localdomain" - "somedomain.com" - "otherdomain.org" - ]; - }; - - mail-directory = mkOption { - type = str; - description = "Path to use for mail storage."; - }; - - state-directory = mkOption { - type = str; - description = "Path to use for state data."; - }; - - trusted-networks = mkOption { - type = listOf str; - description = "A list of trusted networks, for which we will happily relay without auth."; - example = [ - "10.0.0.0/16" - "192.168.0.0/24" - ]; - }; - - sender-blacklist = mkOption { - type = listOf str; - description = "A list of email addresses for whom we will not send email."; - default = []; - example = [ - "baduser@test.com" - "change-pw@test.com" - ]; - }; - - recipient-blacklist = mkOption { - type = listOf str; - description = "A list of email addresses for whom we will not accept email."; - default = []; - example = [ - "baduser@test.com" - "change-pw@test.com" - ]; - }; - - message-size-limit = mkOption { - type = int; - description = "Size of max email in megabytes."; - default = 30; - }; - - user-aliases = mkOption { - type = attrsOf (listOf str); - description = "A map of real user to list of alias emails."; - default = {}; - example = { - someuser = ["alias0" "alias1"]; - }; - }; - - alias-users = mkOption { - type = attrsOf (listOf str); - description = "A map of email alias to a list of users."; - example = { - alias = ["realuser0" "realuser1"]; - }; - }; - - mailboxes = mkOption { - description = '' - The mailboxes for dovecot. - - Depending on the mail client used it might be necessary to change some mailbox's name. - ''; - default = { - Trash = { - auto = "create"; - specialUse = "Trash"; - autoexpunge = "30d"; - }; - Junk = { - auto = "create"; - specialUse = "Junk"; - autoexpunge = "60d"; - }; - Drafts = { - auto = "create"; - specialUse = "Drafts"; - autoexpunge = "60d"; - }; - Sent = { - auto = "subscribe"; - specialUse = "Sent"; - }; - Archive = { - auto = "no"; - specialUse = "Archive"; - }; - Flagged = { - auto = "no"; - specialUse = "Flagged"; - }; - }; - }; - - debug = mkOption { - description = "Enable debugging on mailservers."; - type = bool; - default = false; - }; - - max-user-connections = mkOption { - description = "Max simultaneous connections per user."; - type = int; - default = 20; - }; - - ssl = { - certificate = mkOption { - type = str; - description = "Path to the ssl certificate for the mail server to use."; - }; - - private-key = mkOption { - type = str; - description = "Path to the ssl private key for the mail server to use."; - }; - }; - }; - - imports = [ - ./mail/dkim.nix - ./mail/dovecot.nix - ./mail/postfix.nix - ./mail/rspamd.nix - ./mail/clamav.nix - ]; - - config = mkIf cfg.enable { - systemd.tmpfiles.rules = [ - "d ${cfg.mail-directory} 775 ${cfg.mail-user} ${cfg.mail-group} - -" - "d ${cfg.state-directory} 775 root ${cfg.mail-group} - -" - ]; - - networking.firewall = { - allowedTCPPorts = [ 25 110 143 587 993 995 ]; - }; - - users = { - users = { - ${cfg.mail-user} = { - isSystemUser = true; - uid = cfg.mail-user-id; - group = cfg.mail-group; - }; - }; - - groups = { - ${cfg.mail-group} = { - members = [ cfg.mail-user ]; - }; - }; - }; - }; -} diff --git a/lib/fudo/mail/clamav.nix b/lib/fudo/mail/clamav.nix deleted file mode 100644 index 455548c..0000000 --- a/lib/fudo/mail/clamav.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let cfg = config.fudo.mail-server; - -in { - options.fudo.mail-server.clamav = { - enable = mkOption { - description = "Enable virus scanning with ClamAV."; - type = types.bool; - default = true; - }; - }; - - config = mkIf (cfg.enable && cfg.clamav.enable) { - - services.clamav = { - daemon = { - enable = true; - settings = { PhishingScanURLs = "no"; }; - }; - updater.enable = true; - }; - }; -} diff --git a/lib/fudo/mail/dkim.nix b/lib/fudo/mail/dkim.nix deleted file mode 100644 index 3e0cb48..0000000 --- a/lib/fudo/mail/dkim.nix +++ /dev/null @@ -1,114 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.fudo.mail-server; - - createDomainDkimCert = dom: - let - dkim_key = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key"; - dkim_txt = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.txt"; - in - '' - if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] - then - ${cfg.dkim.package}/bin/opendkim-genkey -s "${cfg.dkim.selector}" \ - -d "${dom}" \ - --bits="${toString cfg.dkim.key-bits}" \ - --directory="${cfg.dkim.key-directory}" - mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.private" "${dkim_key}" - mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.txt" "${dkim_txt}" - echo "Generated key for domain ${dom} selector ${cfg.dkim.selector}" - fi - ''; - - createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.local-domains); - - keyTable = pkgs.writeText "opendkim-KeyTable" - (lib.concatStringsSep "\n" (lib.flip map cfg.local-domains - (dom: "${dom} ${dom}:${cfg.dkim.selector}:${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key"))); - signingTable = pkgs.writeText "opendkim-SigningTable" - (lib.concatStringsSep "\n" (lib.flip map cfg.local-domains (dom: "${dom} ${dom}"))); - - dkim = config.services.opendkim; - args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ]; -in -{ - - options.fudo.mail-server.dkim = { - signing = mkOption { - type = types.bool; - default = true; - description = "Enable dkim signatures for mail."; - }; - - key-directory = mkOption { - type = types.str; - default = "/var/dkim"; - description = "Path to use to store DKIM keys."; - }; - - selector = mkOption { - type = types.str; - default = "mail"; - description = "Name to use for mail-signing keys."; - }; - - key-bits = mkOption { - type = types.int; - default = 2048; - description = '' - How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. - - If you have already deployed a key with a different number of bits than specified - here, then you should use a different selector (dkimSelector). In order to get - this package to generate a key with the new number of bits, you will either have to - change the selector or delete the old key file. - ''; - }; - - package = mkOption { - type = types.package; - default = pkgs.opendkim; - description = "OpenDKIM package to use."; - }; - }; - - config = mkIf (cfg.dkim.signing && cfg.enable) { - services.opendkim = { - enable = true; - selector = cfg.dkim.selector; - domains = "csl:${builtins.concatStringsSep "," cfg.local-domains}"; - configFile = pkgs.writeText "opendkim.conf" ('' - Canonicalization relaxed/simple - UMask 0002 - Socket ${dkim.socket} - KeyTable file:${keyTable} - SigningTable file:${signingTable} - '' + (lib.optionalString cfg.debug '' - Syslog yes - SyslogSuccess yes - LogWhy yes - '')); - }; - - users.users = { - "${config.services.postfix.user}" = { - extraGroups = [ "${config.services.opendkim.group}" ]; - }; - }; - - systemd.services.opendkim = { - preStart = lib.mkForce createAllCerts; - serviceConfig = { - ExecStart = lib.mkForce "${cfg.dkim.package}/bin/opendkim ${escapeShellArgs args}"; - PermissionsStartOnly = lib.mkForce false; - }; - }; - - systemd.tmpfiles.rules = [ - "d '${cfg.dkim.key-directory}' - ${config.services.opendkim.user} ${config.services.opendkim.group} - -" - ]; - }; -} diff --git a/lib/fudo/mail/dovecot.nix b/lib/fudo/mail/dovecot.nix deleted file mode 100644 index e33050e..0000000 --- a/lib/fudo/mail/dovecot.nix +++ /dev/null @@ -1,314 +0,0 @@ -{ config, lib, pkgs, environment, ... }: - -with lib; -let - cfg = config.fudo.mail-server; - - sieve-path = "${cfg.state-directory}/dovecot/imap_sieve"; - - pipe-bin = pkgs.stdenv.mkDerivation { - name = "pipe_bin"; - src = ./dovecot/pipe_bin; - buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ]; - buildCommand = '' - mkdir -p $out/pipe/bin - cp $src/* $out/pipe/bin/ - chmod a+x $out/pipe/bin/* - patchShebangs $out/pipe/bin - - for file in $out/pipe/bin/*; do - wrapProgram $file \ - --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" - done - ''; - }; - - ldap-conf-template = ldap-cfg: - let - ssl-config = if (ldap-cfg.ca == null) then '' - tls = no - tls_require_cert = try - '' else '' - tls_ca_cert_file = ${ldap-cfg.ca} - tls = yes - tls_require_cert = try - ''; - in - pkgs.writeText "dovecot2-ldap-config.conf.template" '' - uris = ${concatStringsSep " " ldap-cfg.server-urls} - ldap_version = 3 - dn = ${ldap-cfg.reader-dn} - dnpass = __LDAP_READER_PASSWORD__ - auth_bind = yes - auth_bind_userdn = uid=%u,ou=members,dc=fudo,dc=org - base = dc=fudo,dc=org - ${ssl-config} - ''; - - ldap-conf-generator = ldap-cfg: let - template = ldap-conf-template ldap-cfg; - target-dir = dirOf ldap-cfg.generated-ldap-config; - target = ldap-cfg.generated-ldap-config; - in pkgs.writeScript "dovecot2-ldap-password-swapper.sh" '' - mkdir -p ${target-dir} - touch ${target} - chmod 600 ${target} - chown ${config.services.dovecot2.user} ${target} - LDAP_READER_PASSWORD=$( cat "${ldap-cfg.reader-password-file}" ) - sed 's/__LDAP_READER_PASSWORD__/$LDAP_READER_PASSWORD/' '${template}' > ${target} - ''; - - ldap-passwd-entry = ldap-config: '' - passdb { - driver = ldap - args = ${ldap-conf "ldap-passdb.conf" ldap-config} - } - ''; - - ldapOpts = { - options = with types; { - ca = mkOption { - type = nullOr str; - description = "The path to the CA cert used to sign the LDAP server certificate."; - default = null; - }; - - base = mkOption { - type = str; - description = "Base of the LDAP server database."; - example = "dc=fudo,dc=org"; - }; - - server-urls = mkOption { - type = listOf str; - description = "A list of LDAP server URLs used for authentication."; - }; - - reader-dn = mkOption { - type = str; - description = '' - DN to use for reading user information. Needs access to homeDirectory, - uidNumber, gidNumber, and uid, but not password attributes. - ''; - }; - - reader-password-file = mkOption { - type = str; - description = "Password for the user specified in ldap-reader-dn."; - }; - - generated-ldap-config = mkOption { - type = str; - description = "Path at which to store the generated LDAP config file, including password."; - default = "/run/dovecot2/config/ldap.conf"; - }; - }; - }; - - dovecot-user = config.services.dovecot2.user; - -in { - options.fudo.mail-server.dovecot = with types; { - ssl-private-key = mkOption { - type = str; - description = "Location of the server SSL private key."; - }; - - ssl-certificate = mkOption { - type = str; - description = "Location of the server SSL certificate."; - }; - - ldap = mkOption { - type = nullOr (submodule ldapOpts); - default = null; - description = '' - LDAP auth server configuration. If omitted, the server will use local authentication. - ''; - }; - }; - - config = mkIf cfg.enable { - - services.prometheus.exporters.dovecot = mkIf cfg.monitoring { - enable = true; - scopes = ["user" "global"]; - listenAddress = "127.0.0.1"; - port = 9166; - socketPath = "/var/run/dovecot2/old-stats"; - }; - - services.dovecot2 = { - enable = true; - enableImap = true; - enableLmtp = true; - enablePop3 = true; - enablePAM = cfg.dovecot.ldap == null; - - createMailUser = true; - - mailUser = cfg.mail-user; - mailGroup = cfg.mail-group; - mailLocation = "maildir:${cfg.mail-directory}/%u/"; - - sslServerCert = cfg.dovecot.ssl-certificate; - sslServerKey = cfg.dovecot.ssl-private-key; - - modules = [ pkgs.dovecot_pigeonhole ]; - protocols = [ "sieve" ]; - - sieveScripts = { - after = builtins.toFile "spam.sieve" '' - require "fileinto"; - - if header :is "X-Spam" "Yes" { - fileinto "Junk"; - stop; - } - ''; - }; - - mailboxes = cfg.mailboxes; - - extraConfig = '' - #Extra Config - - ${optionalString cfg.monitoring '' - # The prometheus exporter still expects an older style of metrics - mail_plugins = $mail_plugins old_stats - service old-stats { - unix_listener old-stats { - user = dovecot-exporter - group = dovecot-exporter - } - } - ''} - - ${lib.optionalString cfg.debug '' - mail_debug = yes - auth_debug = yes - verbose_ssl = yes - ''} - - protocol imap { - mail_max_userip_connections = ${toString cfg.max-user-connections} - mail_plugins = $mail_plugins imap_sieve - } - - protocol pop3 { - mail_max_userip_connections = ${toString cfg.max-user-connections} - } - - protocol lmtp { - mail_plugins = $mail_plugins sieve - } - - mail_access_groups = ${cfg.mail-group} - ssl = required - - # When looking up usernames, just use the name, not the full address - auth_username_format = %n - - service lmtp { - # Enable logging in debug mode - ${optionalString cfg.debug "executable = lmtp -L"} - - # Unix socket for postfix to deliver messages via lmtp - unix_listener dovecot-lmtp { - user = "postfix" - group = ${cfg.mail-group} - mode = 0600 - } - - # Drop privs, since all mail is owned by one user - # user = ${cfg.mail-user} - # group = ${cfg.mail-group} - user = root - } - - auth_mechanisms = login plain - - ${optionalString (cfg.dovecot.ldap != null) '' - passdb { - driver = ldap - args = ${cfg.dovecot.ldap.generated-ldap-config} - } - ''} - userdb { - driver = static - args = uid=${toString cfg.mail-user-id} home=${cfg.mail-directory}/%u - } - - # Used by postfix to authorize users - service auth { - unix_listener auth { - mode = 0660 - user = "${config.services.postfix.user}" - group = ${cfg.mail-group} - } - - unix_listener auth-userdb { - mode = 0660 - user = "${config.services.postfix.user}" - group = ${cfg.mail-group} - } - } - - service auth-worker { - user = root - } - - service imap { - vsz_limit = 1024M - } - - namespace inbox { - separator = "/" - inbox = yes - } - - plugin { - sieve_plugins = sieve_imapsieve sieve_extprograms - sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve - sieve_default = file:/var/sieve/%u/default.sieve - sieve_default_name = default - # From elsewhere to Spam folder - imapsieve_mailbox1_name = Junk - imapsieve_mailbox1_causes = COPY - imapsieve_mailbox1_before = file:${sieve-path}/report-spam.sieve - # From Spam folder to elsewhere - imapsieve_mailbox2_name = * - imapsieve_mailbox2_from = Junk - imapsieve_mailbox2_causes = COPY - imapsieve_mailbox2_before = file:${sieve-path}/report-ham.sieve - sieve_pipe_bin_dir = ${pipe-bin}/pipe/bin - sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment - } - - recipient_delimiter = + - - lmtp_save_to_detail_mailbox = yes - - lda_mailbox_autosubscribe = yes - lda_mailbox_autocreate = yes - ''; - }; - - systemd = { - tmpfiles.rules = [ - "d ${sieve-path} 750 ${dovecot-user} ${cfg.mail-group} - -" - ]; - - services.dovecot2.preStart = '' - rm -f ${sieve-path}/* - cp -p ${./dovecot/imap_sieve}/*.sieve ${sieve-path} - for k in ${sieve-path}/*.sieve ; do - ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" - done - - ${optionalString (cfg.dovecot.ldap != null) - (ldap-conf-generator cfg.dovecot.ldap)} - ''; - }; - }; -} diff --git a/lib/fudo/mail/dovecot/imap_sieve/report-ham.sieve b/lib/fudo/mail/dovecot/imap_sieve/report-ham.sieve deleted file mode 100644 index a9d30cf..0000000 --- a/lib/fudo/mail/dovecot/imap_sieve/report-ham.sieve +++ /dev/null @@ -1,15 +0,0 @@ -require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; - -if environment :matches "imap.mailbox" "*" { - set "mailbox" "${1}"; -} - -if string "${mailbox}" "Trash" { - stop; -} - -if environment :matches "imap.user" "*" { - set "username" "${1}"; -} - -pipe :copy "sa-learn-ham.sh" [ "${username}" ]; diff --git a/lib/fudo/mail/dovecot/imap_sieve/report-spam.sieve b/lib/fudo/mail/dovecot/imap_sieve/report-spam.sieve deleted file mode 100644 index 4024b7a..0000000 --- a/lib/fudo/mail/dovecot/imap_sieve/report-spam.sieve +++ /dev/null @@ -1,7 +0,0 @@ -require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; - -if environment :matches "imap.user" "*" { - set "username" "${1}"; -} - -pipe :copy "sa-learn-spam.sh" [ "${username}" ]; \ No newline at end of file diff --git a/lib/fudo/mail/dovecot/pipe_bin/sa-learn-ham.sh b/lib/fudo/mail/dovecot/pipe_bin/sa-learn-ham.sh deleted file mode 100755 index 76fc4ed..0000000 --- a/lib/fudo/mail/dovecot/pipe_bin/sa-learn-ham.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -o errexit -exec rspamc -h /run/rspamd/worker-controller.sock learn_ham \ No newline at end of file diff --git a/lib/fudo/mail/dovecot/pipe_bin/sa-learn-spam.sh b/lib/fudo/mail/dovecot/pipe_bin/sa-learn-spam.sh deleted file mode 100755 index 2a2f766..0000000 --- a/lib/fudo/mail/dovecot/pipe_bin/sa-learn-spam.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -o errexit -exec rspamc -h /run/rspamd/worker-controller.sock learn_spam \ No newline at end of file diff --git a/lib/fudo/mail/postfix.nix b/lib/fudo/mail/postfix.nix deleted file mode 100644 index 7525f4d..0000000 --- a/lib/fudo/mail/postfix.nix +++ /dev/null @@ -1,319 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - inherit (lib.strings) concatStringsSep; - - cfg = config.fudo.mail-server; - - # The final newline is important - write-entries = filename: entries: - let - entries-string = (concatStringsSep "\n" entries); - in builtins.toFile filename '' - ${entries-string} - ''; - - make-user-aliases = entries: - concatStringsSep "\n" - (mapAttrsToList (user: aliases: - concatStringsSep "\n" - (map (alias: "${alias} ${user}") aliases)) - entries); - - make-alias-users = domains: entries: - concatStringsSep "\n" - (flatten - (mapAttrsToList (alias: users: - (map (domain: - "${alias}@${domain} ${concatStringsSep "," users}") - domains)) - entries)); - - policyd-spf = pkgs.writeText "policyd-spf.conf" ( - cfg.postfix.policy-spf-extra-config - + (lib.optionalString cfg.debug '' - debugLevel = 4 - '')); - - submission-header-cleanup-rules = pkgs.writeText "submission_header_cleanup_rules" ('' - # Removes sensitive headers from mails handed in via the submission port. - # See https://thomas-leister.de/mailserver-debian-stretch/ - # Uses "pcre" style regex. - - /^Received:/ IGNORE - /^X-Originating-IP:/ IGNORE - /^X-Mailer:/ IGNORE - /^User-Agent:/ IGNORE - /^X-Enigmail:/ IGNORE - ''); - blacklist-postfix-entry = sender: "${sender} REJECT"; - blacklist-postfix-file = filename: entries: - write-entries filename entries; - sender-blacklist-file = blacklist-postfix-file "reject_senders" - (map blacklist-postfix-entry cfg.sender-blacklist); - recipient-blacklist-file = blacklist-postfix-file "reject_recipients" - (map blacklist-postfix-entry cfg.recipient-blacklist); - - # A list of domains for which we accept mail - virtual-mailbox-map-file = write-entries "virtual_mailbox_map" - (map (domain: "@${domain} OK") (cfg.local-domains ++ [cfg.domain])); - - sender-login-map-file = let - escapeDot = (str: replaceStrings ["."] ["\\."] str); - in write-entries "sender_login_maps" - (map (domain: "/^(.*)@${escapeDot domain}$/ \${1}") (cfg.local-domains ++ [cfg.domain])); - - mapped-file = name: "hash:/var/lib/postfix/conf/${name}"; - - pcre-file = name: "pcre:/var/lib/postfix/conf/${name}"; - -in { - - options.fudo.mail-server.postfix = { - - ssl-private-key = mkOption { - type = types.str; - description = "Location of the server SSL private key."; - }; - - ssl-certificate = mkOption { - type = types.str; - description = "Location of the server SSL certificate."; - }; - - policy-spf-extra-config = mkOption { - type = types.lines; - default = ""; - example = '' - skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 - ''; - description = '' - Extra configuration options for policyd-spf. This can be use to among - other things skip spf checking for some IP addresses. - ''; - }; - }; - - config = mkIf cfg.enable { - - services.prometheus.exporters.postfix = mkIf cfg.monitoring { - enable = true; - systemd.enable = true; - showqPath = "/var/lib/postfix/queue/public/showq"; - user = config.services.postfix.user; - group = config.services.postfix.group; - }; - - services.postfix = { - enable = true; - domain = cfg.domain; - origin = cfg.domain; - hostname = cfg.mail-hostname; - destination = ["localhost" "localhost.localdomain"]; - # destination = ["localhost" "localhost.localdomain" cfg.hostname] ++ - # cfg.local-domains;; - - enableHeaderChecks = true; - enableSmtp = true; - enableSubmission = true; - - mapFiles."reject_senders" = sender-blacklist-file; - mapFiles."reject_recipients" = recipient-blacklist-file; - mapFiles."virtual_mailbox_map" = virtual-mailbox-map-file; - mapFiles."sender_login_map" = sender-login-map-file; - - # TODO: enable! - # headerChecks = [ { action = "REDIRECT spam@example.com"; pattern = "/^X-Spam-Flag:/"; } ]; - networks = cfg.trusted-networks; - - virtual = '' - ${make-user-aliases cfg.user-aliases} - - ${make-alias-users ([cfg.domain] ++ cfg.local-domains) cfg.alias-users} - ''; - - sslCert = cfg.postfix.ssl-certificate; - sslKey = cfg.postfix.ssl-private-key; - - config = { - virtual_mailbox_domains = cfg.local-domains ++ [cfg.domain]; - # virtual_mailbox_base = "${cfg.mail-directory}/"; - virtual_mailbox_maps = mapped-file "virtual_mailbox_map"; - - virtual_uid_maps = "static:${toString cfg.mail-user-id}"; - virtual_gid_maps = "static:${toString config.users.groups."${cfg.mail-group}".gid}"; - - virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; - - # NOTE: it's important that this ends with /, to indicate Maildir format! - # mail_spool_directory = "${cfg.mail-directory}/"; - message_size_limit = toString(cfg.message-size-limit * 1024 * 1024); - - smtpd_banner = "${cfg.mail-hostname} ESMTP NO UCE"; - - tls_eecdh_strong_curve = "prime256v1"; - tls_eecdh_ultra_curve = "secp384r1"; - - policy-spf_time_limit = "3600s"; - - smtp_host_lookup = "dns, native"; - - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_auth_enable = "yes"; - smtpd_sasl_local_domain = "fudo.org"; - - smtpd_sasl_security_options = "noanonymous"; - smtpd_sasl_tls_security_options = "noanonymous"; - - smtpd_sender_login_maps = (pcre-file "sender_login_map"); - - disable_vrfy_command = "yes"; - - recipient_delimiter = "+"; - - milter_protocol = "6"; - milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; - - smtpd_milters = [ - "unix:/run/rspamd/rspamd-milter.sock" - "unix:/var/run/opendkim/opendkim.sock" - ]; - - non_smtpd_milters = [ - "unix:/run/rspamd/rspamd-milter.sock" - "unix:/var/run/opendkim/opendkim.sock" - ]; - - smtpd_relay_restrictions = [ - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unauth_destination" - "reject_unauth_pipelining" - "reject_unauth_destination" - "reject_unknown_sender_domain" - ]; - - smtpd_sender_restrictions = [ - "check_sender_access ${mapped-file "reject_senders"}" - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unknown_sender_domain" - ]; - - smtpd_recipient_restrictions = [ - "check_sender_access ${mapped-file "reject_recipients"}" - "permit_mynetworks" - "permit_sasl_authenticated" - "check_policy_service unix:private/policy-spf" - "reject_unknown_recipient_domain" - "reject_unauth_pipelining" - "reject_unauth_destination" - "reject_invalid_hostname" - "reject_non_fqdn_hostname" - "reject_non_fqdn_sender" - "reject_non_fqdn_recipient" - ]; - - smtpd_helo_restrictions = [ - "permit_mynetworks" - "reject_invalid_hostname" - "permit" - ]; - - # Handled by submission - smtpd_tls_security_level = "may"; - - smtpd_tls_eecdh_grade = "ultra"; - - # Disable obselete protocols - smtpd_tls_protocols = [ - "TLSv1.2" - "TLSv1.1" - "!TLSv1" - "!SSLv2" - "!SSLv3" - ]; - smtp_tls_protocols = [ - "TLSv1.2" - "TLSv1.1" - "!TLSv1" - "!SSLv2" - "!SSLv3" - ]; - smtpd_tls_mandatory_protocols = [ - "TLSv1.2" - "TLSv1.1" - "!TLSv1" - "!SSLv2" - "!SSLv3" - ]; - smtp_tls_mandatory_protocols = [ - "TLSv1.2" - "TLSv1.1" - "!TLSv1" - "!SSLv2" - "!SSLv3" - ]; - - smtp_tls_ciphers = "high"; - smtpd_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; - smtpd_tls_mandatory_ciphers = "high"; - - smtpd_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"]; - smtpd_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"]; - smtp_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"]; - smtp_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"]; - - tls_preempt_cipherlist = "yes"; - - smtpd_tls_auth_only = "yes"; - - smtpd_tls_loglevel = "1"; - - tls_random_source = "dev:/dev/urandom"; - }; - - submissionOptions = { - smtpd_tls_security_level = "encrypt"; - smtpd_sasl_auth_enable = "yes"; - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_security_options = "noanonymous"; - smtpd_sasl_local_domain = cfg.domain; - smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_restrictions = "reject_sender_login_mismatch,reject_unknown_sender_domain"; - smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; - cleanup_service_name = "submission-header-cleanup"; - }; - - masterConfig = { - "policy-spf" = { - type = "unix"; - privileged = true; - chroot = false; - command = "spawn"; - args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"]; - }; - "submission-header-cleanup" = { - type = "unix"; - private = false; - chroot = false; - maxproc = 0; - command = "cleanup"; - args = ["-o" "header_checks=pcre:${submission-header-cleanup-rules}"]; - }; - }; - }; - - # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work - systemd.services.postfix = { - after = [ "dovecot2.service" ] - ++ (lib.optional cfg.dkim.signing "opendkim.service"); - requires = [ "dovecot2.service" ] - ++ (lib.optional cfg.dkim.signing "opendkim.service"); - }; - }; -} diff --git a/lib/fudo/mail/rspamd.nix b/lib/fudo/mail/rspamd.nix deleted file mode 100644 index 4bc9324..0000000 --- a/lib/fudo/mail/rspamd.nix +++ /dev/null @@ -1,88 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; -let - cfg = config.fudo.mail-server; - -in { - config = mkIf cfg.enable { - services.prometheus.exporters.rspamd.enable = true; - - services.rspamd = { - - enable = true; - - locals = { - "milter_headers.conf" = { - text = '' - extended_spam_headers = yes; - ''; - }; - - "antivirus.conf" = { - text = '' - clamav { - action = "reject"; - symbol = "CLAM_VIRUS"; - type = "clamav"; - log_clean = true; - servers = "/run/clamav/clamd.ctl"; - scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all - } - ''; - }; - }; - - overrides = { - "milter_headers.conf" = { - text = '' - extended_spam_headers = true; - ''; - }; - }; - - workers.rspamd_proxy = { - type = "rspamd_proxy"; - bindSockets = [{ - socket = "/run/rspamd/rspamd-milter.sock"; - mode = "0664"; - }]; - count = 1; # Do not spawn too many processes of this type - extraConfig = '' - milter = yes; # Enable milter mode - timeout = 120s; # Needed for Milter usually - - upstream "local" { - default = yes; # Self-scan upstreams are always default - self_scan = yes; # Enable self-scan - } - ''; - }; - - workers.controller = { - type = "controller"; - count = 1; - bindSockets = [ - "localhost:11334" - { - socket = "/run/rspamd/worker-controller.sock"; - mode = "0666"; - } - ]; - includes = []; - }; - }; - - systemd.services.rspamd = { - requires = (optional cfg.clamav.enable "clamav-daemon.service"); - after = (optional cfg.clamav.enable "clamav-daemon.service"); - }; - - systemd.services.postfix = { - after = [ "rspamd.service" ]; - requires = [ "rspamd.service" ]; - }; - - users.extraUsers.${config.services.postfix.user}.extraGroups = [ config.services.rspamd.group ]; - }; -} diff --git a/lib/fudo/minecraft-server.nix b/lib/fudo/minecraft-server.nix deleted file mode 100644 index 9ee5474..0000000 --- a/lib/fudo/minecraft-server.nix +++ /dev/null @@ -1,64 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.minecraft-server; - -in { - options.fudo.minecraft-server = { - enable = mkEnableOption "Start a minecraft server."; - - package = mkOption { - type = types.package; - description = "Minecraft package to use."; - default = pkgs.minecraft-server_1_15_1; - }; - - data-dir = mkOption { - type = types.path; - description = "Path at which to store minecraft data."; - }; - - world-name = mkOption { - type = types.str; - description = "Name of the server world (used in saves etc)."; - }; - - motd = mkOption { - type = types.str; - description = "Welcome message for newcomers."; - }; - - game-mode = mkOption { - type = types.enum ["survival" "creative" "adventure" "spectator"]; - description = "Game mode of the server."; - default = "survival"; - }; - - difficulty = mkOption { - type = types.int; - description = "Difficulty level, where 0 is peaceful and 3 is hard."; - default = 2; - }; - }; - - config = mkIf cfg.enable { - environment.systemPackages = [ - cfg.package - ]; - - services.minecraft-server = { - enable = true; - package = cfg.package; - dataDir = cfg.data-dir; - eula = true; - declarative = true; - serverProperties = { - level-name = cfg.world-name; - motd = cfg.motd; - difficulty = cfg.difficulty; - gamemode = cfg.game-mode; - }; - }; - }; -} diff --git a/lib/fudo/netinfo-email.nix b/lib/fudo/netinfo-email.nix deleted file mode 100644 index 4dcc3a2..0000000 --- a/lib/fudo/netinfo-email.nix +++ /dev/null @@ -1,93 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.netinfo-email; - - make-script = server: port: target: pkgs.writeText "netinfo-script.rb" '' - #!${pkgs.ruby}/bin/ruby - - require 'net/smtp' - - raise RuntimeError.new("NETINFO_SMTP_USERNAME not set!") if not ENV['NETINFO_SMTP_USERNAME'] - user = ENV['NETINFO_SMTP_USERNAME'] - - raise RuntimeError.new("NETINFO_SMTP_PASSWD not set!") if not ENV['NETINFO_SMTP_PASSWD'] - passwd = ENV['NETINFO_SMTP_PASSWD'] - - hostname = `${pkgs.inetutils}/bin/hostname -f`.strip - date = `${pkgs.coreutils}/bin/date +%Y-%m-%d`.strip - email_date = `${pkgs.coreutils}/bin/date` - ipinfo = `${pkgs.iproute}/bin/ip addr` - - message = < 0; - }; - - mkZoneFileName = name: if name == "." then "root" else name; - - # replaces include: directives for keys with fake keys for nsd-checkconf - injectFakeKeys = keys: concatStrings - (mapAttrsToList - (keyName: keyOptions: '' - fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")" - sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf - '') - keys); - - nsdEnv = pkgs.buildEnv { - name = "nsd-env"; - - paths = [ configFile ] - ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs; - - postBuild = '' - echo "checking zone files" - cd $out/zones - for zoneFile in *; do - echo "|- checking zone '$out/zones/$zoneFile'" - ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || { - if grep -q \\\\\\$ "$zoneFile"; then - echo zone "$zoneFile" contains escaped dollar signs \\\$ - echo Escaping them is not needed any more. Please make sure \ - to unescape them where they prefix a variable name. - fi - exit 1 - } - done - echo "checking configuration file" - # Save original config file including key references... - cp $out/nsd.conf{,.orig} - # ...inject mock keys into config - ${injectFakeKeys cfg.keys} - # ...do the checkconf - ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf - # ... and restore original config file. - mv $out/nsd.conf{.orig,} - ''; - }; - - writeZoneData = name: text: pkgs.writeTextFile { - name = "nsd-zone-${mkZoneFileName name}"; - inherit text; - destination = "/zones/${mkZoneFileName name}"; - }; - - - # options are ordered alphanumerically by the nixos option name - configFile = pkgs.writeTextDir "nsd.conf" '' - server: - chroot: "${stateDir}" - username: ${username} - # The directory for zonefile: files. The daemon chdirs here. - zonesdir: "${stateDir}" - # the list of dynamically added zones. - database: "${stateDir}/var/nsd.db" - pidfile: "${pidFile}" - xfrdfile: "${stateDir}/var/xfrd.state" - xfrdir: "${stateDir}/tmp" - zonelistfile: "${stateDir}/var/zone.list" - # interfaces - ${forEach " ip-address: " cfg.interfaces} - ip-freebind: ${yesOrNo cfg.ipFreebind} - hide-version: ${yesOrNo cfg.hideVersion} - identity: "${cfg.identity}" - ip-transparent: ${yesOrNo cfg.ipTransparent} - do-ip4: ${yesOrNo cfg.ipv4} - ipv4-edns-size: ${toString cfg.ipv4EDNSSize} - do-ip6: ${yesOrNo cfg.ipv6} - ipv6-edns-size: ${toString cfg.ipv6EDNSSize} - log-time-ascii: ${yesOrNo cfg.logTimeAscii} - ${maybeString "nsid: " cfg.nsid} - port: ${toString cfg.port} - reuseport: ${yesOrNo cfg.reuseport} - round-robin: ${yesOrNo cfg.roundRobin} - server-count: ${toString cfg.serverCount} - ${maybeToString "statistics: " cfg.statistics} - tcp-count: ${toString cfg.tcpCount} - tcp-query-count: ${toString cfg.tcpQueryCount} - tcp-timeout: ${toString cfg.tcpTimeout} - verbosity: ${toString cfg.verbosity} - ${maybeString "version: " cfg.version} - xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout} - zonefiles-check: ${yesOrNo cfg.zonefilesCheck} - ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength} - ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength} - rrl-ratelimit: ${toString cfg.ratelimit.ratelimit} - ${maybeString "rrl-slip: " cfg.ratelimit.slip} - rrl-size: ${toString cfg.ratelimit.size} - rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit} - ${keyConfigFile} - remote-control: - control-enable: ${yesOrNo cfg.remoteControl.enable} - control-key-file: "${cfg.remoteControl.controlKeyFile}" - control-cert-file: "${cfg.remoteControl.controlCertFile}" - ${forEach " control-interface: " cfg.remoteControl.interfaces} - control-port: ${toString cfg.remoteControl.port} - server-key-file: "${cfg.remoteControl.serverKeyFile}" - server-cert-file: "${cfg.remoteControl.serverCertFile}" - ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)} - ${cfg.extraConfig} - ''; - - yesOrNo = b: if b then "yes" else "no"; - maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"''; - maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}''; - forEach = pre: l: concatMapStrings (x: pre + x + "\n") l; - - - keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: '' - key: - name: "${keyName}" - algorithm: "${keyOptions.algorithm}" - include: "${stateDir}/private/${keyName}" - '') cfg.keys); - - copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: '' - secret=$(cat "${keyOptions.keyFile}") - dest="${stateDir}/private/${keyName}" - echo " secret: \"$secret\"" > "$dest" - chown ${username}:${username} "$dest" - chmod 0400 "$dest" - '') cfg.keys); - - - # options are ordered alphanumerically by the nixos option name - zoneConfigFile = name: zone: '' - zone: - name: "${name}" - zonefile: "${stateDir}/zones/${mkZoneFileName name}" - ${maybeString "outgoing-interface: " zone.outgoingInterface} - ${forEach " rrl-whitelist: " zone.rrlWhitelist} - ${maybeString "zonestats: " zone.zoneStats} - ${maybeToString "max-refresh-time: " zone.maxRefreshSecs} - ${maybeToString "min-refresh-time: " zone.minRefreshSecs} - ${maybeToString "max-retry-time: " zone.maxRetrySecs} - ${maybeToString "min-retry-time: " zone.minRetrySecs} - allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback} - ${forEach " allow-notify: " zone.allowNotify} - ${forEach " request-xfr: " zone.requestXFR} - ${forEach " notify: " zone.notify} - notify-retry: ${toString zone.notifyRetry} - ${forEach " provide-xfr: " zone.provideXFR} - ''; - - zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; }; - - zoneConfigs' = parent: name: zone: - if !(zone ? children) || zone.children == null || zone.children == { } - # leaf -> actual zone - then listToAttrs [ (nameValuePair name (parent // zone)) ] - - # fork -> pattern - else zipAttrsWith (name: head) ( - mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child) - zone.children - ); - - # fighting infinite recursion - zoneOptions = zoneOptionsRaw // childConfig zoneOptions1 true; - zoneOptions1 = zoneOptionsRaw // childConfig zoneOptions2 false; - zoneOptions2 = zoneOptionsRaw // childConfig zoneOptions3 false; - zoneOptions3 = zoneOptionsRaw // childConfig zoneOptions4 false; - zoneOptions4 = zoneOptionsRaw // childConfig zoneOptions5 false; - zoneOptions5 = zoneOptionsRaw // childConfig zoneOptions6 false; - zoneOptions6 = zoneOptionsRaw // childConfig null false; - - childConfig = x: v: { options.children = { type = types.attrsOf x; visible = v; }; }; - - # options are ordered alphanumerically - zoneOptionsRaw = types.submodule { - options = { - - allowAXFRFallback = mkOption { - type = types.bool; - default = true; - description = '' - If NSD as secondary server should be allowed to AXFR if the primary - server does not allow IXFR. - ''; - }; - - allowNotify = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name" - "10.0.3.4&255.255.0.0 BLOCKED" - ]; - description = '' - Listed primary servers are allowed to notify this secondary server. - - either a plain IPv4/IPv6 address or range. Valid patters for ranges: - * 10.0.0.0/24 # via subnet size - * 10.0.0.0&255.255.255.0 # via subnet mask - * 10.0.0.1-10.0.0.254 # via range - A optional port number could be added with a '@': - * 2001:1234::1@1234 - - * will use the specified TSIG key - * NOKEY no TSIG signature is required - * BLOCKED notifies from non-listed or blocked IPs will be ignored - * ]]> - ''; - }; - - children = mkOption { - default = {}; - description = '' - Children zones inherit all options of their parents. Attributes - defined in a child will overwrite the ones of its parent. Only - leaf zones will be actually served. This way it's possible to - define maybe zones which share most attributes without - duplicating everything. This mechanism replaces nsd's patterns - in a save and functional way. - ''; - }; - - data = mkOption { - type = types.lines; - default = ""; - example = ""; - description = '' - The actual zone data. This is the content of your zone file. - Use imports or pkgs.lib.readFile if you don't want this data in your config file. - ''; - }; - - dnssec = mkEnableOption "DNSSEC"; - - dnssecPolicy = { - algorithm = mkOption { - type = types.str; - default = "RSASHA256"; - description = "Which algorithm to use for DNSSEC"; - }; - keyttl = mkOption { - type = types.str; - default = "1h"; - description = "TTL for dnssec records"; - }; - coverage = mkOption { - type = types.str; - default = "1y"; - description = '' - The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time. - ''; - }; - zsk = mkOption { - type = keyPolicy; - default = { keySize = 2048; - prePublish = "1w"; - postPublish = "1w"; - rollPeriod = "1mo"; - }; - description = "Key policy for zone signing keys"; - }; - ksk = mkOption { - type = keyPolicy; - default = { keySize = 4096; - prePublish = "1mo"; - postPublish = "1mo"; - rollPeriod = "0"; - }; - description = "Key policy for key signing keys"; - }; - }; - - maxRefreshSecs = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Limit refresh time for secondary zones. This is the timer which - checks to see if the zone has to be refetched when it expires. - Normally the value from the SOA record is used, but this option - restricts that value. - ''; - }; - - minRefreshSecs = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Limit refresh time for secondary zones. - ''; - }; - - maxRetrySecs = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Limit retry time for secondary zones. This is the timeout after - a failed fetch attempt for the zone. Normally the value from - the SOA record is used, but this option restricts that value. - ''; - }; - - minRetrySecs = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Limit retry time for secondary zones. - ''; - }; - - - notify = mkOption { - type = types.listOf types.str; - default = []; - example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ]; - description = '' - This primary server will notify all given secondary servers about - zone changes. - - a plain IPv4/IPv6 address with on optional port number (ip@port) - - * sign notifies with the specified key - * NOKEY don't sign notifies - ]]> - ''; - }; - - notifyRetry = mkOption { - type = types.int; - default = 5; - description = '' - Specifies the number of retries for failed notifies. Set this along with notify. - ''; - }; - - outgoingInterface = mkOption { - type = types.nullOr types.str; - default = null; - example = "2000::1@1234"; - description = '' - This address will be used for zone-transfere requests if configured - as a secondary server or notifications in case of a primary server. - Supply either a plain IPv4 or IPv6 address with an optional port - number (ip@port). - ''; - }; - - provideXFR = mkOption { - type = types.listOf types.str; - default = []; - example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ]; - description = '' - Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED - address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40 - ''; - }; - - requestXFR = mkOption { - type = types.listOf types.str; - default = []; - example = []; - description = '' - Format: [AXFR|UDP] <ip-address> <key-name | NOKEY> - ''; - }; - - rrlWhitelist = mkOption { - type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]); - default = []; - description = '' - Whitelists the given rrl-types. - ''; - }; - - zoneStats = mkOption { - type = types.nullOr types.str; - default = null; - example = "%s"; - description = '' - When set to something distinct to null NSD is able to collect - statistics per zone. All statistics of this zone(s) will be added - to the group specified by this given name. Use "%s" to use the zones - name as the group. The groups are output from nsd-control stats - and stats_noreset. - ''; - }; - }; - }; - - keyPolicy = types.submodule { - options = { - keySize = mkOption { - type = types.int; - description = "Key size in bits"; - }; - prePublish = mkOption { - type = types.str; - description = "How long in advance to publish new keys"; - }; - postPublish = mkOption { - type = types.str; - description = "How long after deactivation to keep a key in the zone"; - }; - rollPeriod = mkOption { - type = types.str; - description = "How frequently to change keys"; - }; - }; - }; - - dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs); - - dnssec = dnssecZones != {}; - - dnssecTools = pkgs.bind.override { enablePython = true; }; - - signZones = optionalString dnssec '' - mkdir -p ${stateDir}/dnssec - chown ${username}:${username} ${stateDir}/dnssec - chmod 0600 ${stateDir}/dnssec - ${concatStrings (mapAttrsToList signZone dnssecZones)} - ''; - signZone = name: zone: '' - ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name} - ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name} - ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name} - ''; - policyFile = name: policy: pkgs.writeText "${name}.policy" '' - zone ${name} { - algorithm ${policy.algorithm}; - key-size zsk ${toString policy.zsk.keySize}; - key-size ksk ${toString policy.ksk.keySize}; - keyttl ${policy.keyttl}; - pre-publish zsk ${policy.zsk.prePublish}; - pre-publish ksk ${policy.ksk.prePublish}; - post-publish zsk ${policy.zsk.postPublish}; - post-publish ksk ${policy.ksk.postPublish}; - roll-period zsk ${policy.zsk.rollPeriod}; - roll-period ksk ${policy.ksk.rollPeriod}; - coverage ${policy.coverage}; - }; - ''; -in -{ - # options are ordered alphanumerically - options.fudo.nsd = { - - enable = mkEnableOption "NSD authoritative DNS server"; - - bind8Stats = mkEnableOption "BIND8 like statistics"; - - dnssecInterval = mkOption { - type = types.str; - default = "1h"; - description = '' - How often to check whether dnssec key rollover is required - ''; - }; - - extraConfig = mkOption { - type = types.lines; - default = ""; - description = '' - Extra nsd config. - ''; - }; - - hideVersion = mkOption { - type = types.bool; - default = true; - description = '' - Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries. - ''; - }; - - identity = mkOption { - type = types.str; - default = "unidentified server"; - description = '' - Identify the server (CH TXT ID.SERVER entry). - ''; - }; - - interfaces = mkOption { - type = types.listOf types.str; - default = [ "127.0.0.0" "::1" ]; - description = '' - What addresses the server should listen to. - ''; - }; - - ipFreebind = mkOption { - type = types.bool; - default = false; - description = '' - Whether to bind to nonlocal addresses and interfaces that are down. - Similar to ip-transparent. - ''; - }; - - ipTransparent = mkOption { - type = types.bool; - default = false; - description = '' - Allow binding to non local addresses. - ''; - }; - - ipv4 = mkOption { - type = types.bool; - default = true; - description = '' - Whether to listen on IPv4 connections. - ''; - }; - - ipv4EDNSSize = mkOption { - type = types.int; - default = 4096; - description = '' - Preferred EDNS buffer size for IPv4. - ''; - }; - - ipv6 = mkOption { - type = types.bool; - default = true; - description = '' - Whether to listen on IPv6 connections. - ''; - }; - - ipv6EDNSSize = mkOption { - type = types.int; - default = 4096; - description = '' - Preferred EDNS buffer size for IPv6. - ''; - }; - - logTimeAscii = mkOption { - type = types.bool; - default = true; - description = '' - Log time in ascii, if false then in unix epoch seconds. - ''; - }; - - nsid = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - NSID identity (hex string, or "ascii_somestring"). - ''; - }; - - port = mkOption { - type = types.int; - default = 53; - description = '' - Port the service should bind do. - ''; - }; - - reuseport = mkOption { - type = types.bool; - default = pkgs.stdenv.isLinux; - description = '' - Whether to enable SO_REUSEPORT on all used sockets. This lets multiple - processes bind to the same port. This speeds up operation especially - if the server count is greater than one and makes fast restarts less - prone to fail - ''; - }; - - rootServer = mkOption { - type = types.bool; - default = false; - description = '' - Whether this server will be a root server (a DNS root server, you - usually don't want that). - ''; - }; - - roundRobin = mkEnableOption "round robin rotation of records"; - - serverCount = mkOption { - type = types.int; - default = 1; - description = '' - Number of NSD servers to fork. Put the number of CPUs to use here. - ''; - }; - - - stateDir = mkOption { - type = types.str; - description = "Directory at which to store NSD state data."; - default = "/var/lib/nsd"; - }; - - statistics = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Statistics are produced every number of seconds. Prints to log. - If null no statistics are logged. - ''; - }; - - tcpCount = mkOption { - type = types.int; - default = 100; - description = '' - Maximum number of concurrent TCP connections per server. - ''; - }; - - tcpQueryCount = mkOption { - type = types.int; - default = 0; - description = '' - Maximum number of queries served on a single TCP connection. - 0 means no maximum. - ''; - }; - - tcpTimeout = mkOption { - type = types.int; - default = 120; - description = '' - TCP timeout in seconds. - ''; - }; - - verbosity = mkOption { - type = types.int; - default = 0; - description = '' - Verbosity level. - ''; - }; - - version = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - The version string replied for CH TXT version.server and version.bind - queries. Will use the compiled package version on null. - See hideVersion for enabling/disabling this responses. - ''; - }; - - xfrdReloadTimeout = mkOption { - type = types.int; - default = 1; - description = '' - Number of seconds between reloads triggered by xfrd. - ''; - }; - - zonefilesCheck = mkOption { - type = types.bool; - default = true; - description = '' - Whether to check mtime of all zone files on start and sighup. - ''; - }; - - - keys = mkOption { - type = types.attrsOf (types.submodule { - options = { - - algorithm = mkOption { - type = types.str; - default = "hmac-sha256"; - description = '' - Authentication algorithm for this key. - ''; - }; - - keyFile = mkOption { - type = types.path; - description = '' - Path to the file which contains the actual base64 encoded - key. The key will be copied into "${stateDir}/private" before - NSD starts. The copied file is only accessibly by the NSD - user. - ''; - }; - - }; - }); - default = {}; - example = literalExample '' - { "tsig.example.org" = { - algorithm = "hmac-md5"; - keyFile = "/path/to/my/key"; - }; - } - ''; - description = '' - Define your TSIG keys here. - ''; - }; - - - ratelimit = { - - enable = mkEnableOption "ratelimit capabilities"; - - ipv4PrefixLength = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - IPv4 prefix length. Addresses are grouped by netblock. - ''; - }; - - ipv6PrefixLength = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - IPv6 prefix length. Addresses are grouped by netblock. - ''; - }; - - ratelimit = mkOption { - type = types.int; - default = 200; - description = '' - Max qps allowed from any query source. - 0 means unlimited. With an verbosity of 2 blocked and - unblocked subnets will be logged. - ''; - }; - - slip = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Number of packets that get discarded before replying a SLIP response. - 0 disables SLIP responses. 1 will make every response a SLIP response. - ''; - }; - - size = mkOption { - type = types.int; - default = 1000000; - description = '' - Size of the hashtable. More buckets use more memory but lower - the chance of hash hash collisions. - ''; - }; - - whitelistRatelimit = mkOption { - type = types.int; - default = 2000; - description = '' - Max qps allowed from whitelisted sources. - 0 means unlimited. Set the rrl-whitelist option for specific - queries to apply this limit instead of the default to them. - ''; - }; - - }; - - - remoteControl = { - - enable = mkEnableOption "remote control via nsd-control"; - - controlCertFile = mkOption { - type = types.path; - default = "/etc/nsd/nsd_control.pem"; - description = '' - Path to the client certificate signed with the server certificate. - This file is used by nsd-control and generated by nsd-control-setup. - ''; - }; - - controlKeyFile = mkOption { - type = types.path; - default = "/etc/nsd/nsd_control.key"; - description = '' - Path to the client private key, which is used by nsd-control - but not by the server. This file is generated by nsd-control-setup. - ''; - }; - - interfaces = mkOption { - type = types.listOf types.str; - default = [ "127.0.0.1" "::1" ]; - description = '' - Which interfaces NSD should bind to for remote control. - ''; - }; - - port = mkOption { - type = types.int; - default = 8952; - description = '' - Port number for remote control operations (uses TLS over TCP). - ''; - }; - - serverCertFile = mkOption { - type = types.path; - default = "/etc/nsd/nsd_server.pem"; - description = '' - Path to the server self signed certificate, which is used by the server - but and by nsd-control. This file is generated by nsd-control-setup. - ''; - }; - - serverKeyFile = mkOption { - type = types.path; - default = "/etc/nsd/nsd_server.key"; - description = '' - Path to the server private key, which is used by the server - but not by nsd-control. This file is generated by nsd-control-setup. - ''; - }; - - }; - - zones = mkOption { - type = types.attrsOf zoneOptions; - default = {}; - example = literalExample '' - { "serverGroup1" = { - provideXFR = [ "10.1.2.3 NOKEY" ]; - children = { - "example.com." = { - data = ''' - $ORIGIN example.com. - $TTL 86400 - @ IN SOA a.ns.example.com. admin.example.com. ( - ... - '''; - }; - "example.org." = { - data = ''' - $ORIGIN example.org. - $TTL 86400 - @ IN SOA a.ns.example.com. admin.example.com. ( - ... - '''; - }; - }; - }; - "example.net." = { - provideXFR = [ "10.3.2.1 NOKEY" ]; - data = ''' - ... - '''; - }; - } - ''; - description = '' - Define your zones here. Zones can cascade other zones and therefore - inherit settings from parent zones. Look at the definition of - children to learn about inheritance and child zones. - The given example will define 3 zones (example.(com|org|net).). Both - example.com. and example.org. inherit their configuration from - serverGroup1. - ''; - }; - }; - - config = mkIf cfg.enable { - - assertions = singleton { - assertion = zoneConfigs ? "." -> cfg.rootServer; - message = "You have a root zone configured. If this is really what you " - + "want, please enable 'services.nsd.rootServer'."; - }; - - environment = { - systemPackages = [ nsdPkg ]; - etc."nsd/nsd.conf".source = "${configFile}/nsd.conf"; - }; - - users.groups.${username}.gid = config.ids.gids.nsd; - - users.users.${username} = { - description = "NSD service user"; - home = stateDir; - createHome = true; - uid = config.ids.uids.nsd; - group = username; - }; - - systemd.services.nsd = { - description = "NSD authoritative only domain name service"; - - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - - startLimitBurst = 4; - startLimitIntervalSec = 5 * 60; # 5 mins - serviceConfig = { - ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf"; - StandardError = "null"; - PIDFile = pidFile; - Restart = "always"; - RestartSec = "4s"; - }; - - preStart = '' - rm -Rf "${stateDir}/private/" - rm -Rf "${stateDir}/tmp/" - mkdir -m 0700 -p "${stateDir}/private" - mkdir -m 0700 -p "${stateDir}/tmp" - mkdir -m 0700 -p "${stateDir}/var" - cat > "${stateDir}/don't touch anything in here" << EOF - Everything in this directory except NSD's state in var and dnssec - is automatically generated and will be purged and redeployed by - the nsd.service pre-start script. - EOF - chown ${username}:${username} -R "${stateDir}/private" - chown ${username}:${username} -R "${stateDir}/tmp" - chown ${username}:${username} -R "${stateDir}/var" - rm -rf "${stateDir}/zones" - cp -rL "${nsdEnv}/zones" "${stateDir}/zones" - ${copyKeys} - ''; - }; - - systemd.timers.nsd-dnssec = mkIf dnssec { - description = "Automatic DNSSEC key rollover"; - - wantedBy = [ "nsd.service" ]; - - timerConfig = { - OnActiveSec = cfg.dnssecInterval; - OnUnitActiveSec = cfg.dnssecInterval; - }; - }; - - systemd.services.nsd-dnssec = mkIf dnssec { - description = "DNSSEC key rollover"; - - wantedBy = [ "nsd.service" ]; - before = [ "nsd.service" ]; - - script = signZones; - - postStop = '' - /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service - ''; - }; - - }; -} diff --git a/lib/fudo/password.nix b/lib/fudo/password.nix deleted file mode 100644 index 2c7d0e4..0000000 --- a/lib/fudo/password.nix +++ /dev/null @@ -1,116 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.password; - - genOpts = { - options = { - file = mkOption { - type = types.str; - description = "Password file in which to store a generated password."; - }; - - user = mkOption { - type = types.str; - description = "User to which the file should belong."; - }; - - group = mkOption { - type = with types; nullOr str; - description = "Group to which the file should belong."; - default = "nogroup"; - }; - - restart-services = mkOption { - type = with types; listOf str; - description = "List of services to restart when the password file is generated."; - default = []; - }; - }; - }; - - generate-passwd-file = file: user: group: pkgs.writeShellScriptBin "generate-passwd-file.sh" '' - mkdir -p $(dirname ${file}) - - if touch ${file}; then - chown ${user}${optionalString (group != null) ":${group}"} ${file} - if [ $? -ne 0 ]; then - rm ${file} - echo "failed to set permissions on ${file}" - exit 4 - fi - ${pkgs.pwgen}/bin/pwgen 30 1 > ${file} - else - echo "cannot write to ${file}" - exit 2 - fi - - if [ ! -f ${file} ]; then - echo "Failed to create file ${file}" - exit 3 - fi - - ${if (group != null) then - "chmod 640 ${file}" - else - "chmod 600 ${file}"} - - echo "created password file ${file}" - exit 0 - ''; - - restart-script = service-name: '' - SYSCTL=${pkgs.systemd}/bin/systemctl - JOBTYPE=$(${pkgs.systemd}/bin/systemctl show ${service-name} -p Type) - if $SYSCTL is-active --quiet ${service-name} || - [ $JOBTYPE == "Type=simple" ] || - [ $JOBTYPE == "Type=oneshot" ] ; then - echo "restarting service ${service-name} because password has changed." - $SYSCTL restart ${service-name} - fi - ''; - - filterForRestarts = filterAttrs (name: opts: opts.restart-services != []); - -in { - options.fudo.password = { - file-generator = mkOption { - type = with types; attrsOf (submodule genOpts); - description = "List of password files to generate."; - default = {}; - }; - }; - - config = { - systemd.targets.fudo-passwords = { - description = "Target indicating that all Fudo passwords have been generated."; - wantedBy = [ "default.target" ]; - }; - - systemd.services = fold (a: b: a // b) {} (mapAttrsToList (name: opts: { - "file-generator-${name}" = { - enable = true; - partOf = [ "fudo-passwords.target" ]; - serviceConfig.Type = "oneshot"; - description = "Generate password file for ${name}."; - script = "${generate-passwd-file opts.file opts.user opts.group}/bin/generate-passwd-file.sh"; - reloadIfChanged = true; - }; - - "file-generator-watcher-${name}" = mkIf (! (opts.restart-services == [])) { - description = "Restart services upon regenerating password for ${name}"; - after = [ "file-generator-${name}.service" ]; - partOf = [ "fudo-passwords.target" ]; - serviceConfig.Type = "oneshot"; - script = concatStringsSep "\n" (map restart-script opts.restart-services); - }; - }) cfg.file-generator); - - systemd.paths = mapAttrs' (name: opts: - nameValuePair "file-generator-watcher-${name}" { - partOf = [ "fudo-passwords.target"]; - pathConfig.PathChanged = opts.file; - }) (filterForRestarts cfg.file-generator); - }; -} diff --git a/lib/fudo/postgres.nix b/lib/fudo/postgres.nix deleted file mode 100644 index 8ab72cc..0000000 --- a/lib/fudo/postgres.nix +++ /dev/null @@ -1,370 +0,0 @@ -{ config, lib, pkgs, environment, ... }: - -with lib; -let - cfg = config.fudo.postgresql; - - hostname = config.instance.hostname; - domain-name = config.instance.local-domain; - - gssapi-realm = config.fudo.domains.${domain-name}.gssapi-realm; - - join-lines = lib.concatStringsSep "\n"; - - strip-ext = filename: - head (builtins.match "^(.+)[.][^.]+$" filename); - - userDatabaseOpts = { database, ... }: { - options = { - access = mkOption { - type = types.str; - description = "Privileges for user on this database."; - default = "CONNECT"; - }; - - entity-access = mkOption { - type = with types; attrsOf str; - description = - "A list of entities mapped to the access this user should have."; - default = { }; - example = { - "TABLE users" = "SELECT,DELETE"; - "ALL SEQUENCES IN public" = "SELECT"; - }; - }; - }; - }; - - userOpts = { username, ... }: { - options = with types; { - password-file = mkOption { - type = nullOr str; - description = "A file containing the user's (plaintext) password."; - default = null; - }; - - databases = mkOption { - type = attrsOf (submodule userDatabaseOpts); - description = "Map of databases to required database/table perms."; - default = { }; - example = { - my_database = { - access = "ALL PRIVILEGES"; - entity-access = { "ALL TABLES" = "SELECT"; }; - }; - }; - }; - }; - }; - - databaseOpts = { dbname, ... }: { - options = with types; { - users = mkOption { - type = listOf str; - description = - "A list of users who should have full access to this database."; - default = [ ]; - }; - }; - }; - - filterPasswordedUsers = filterAttrs (user: opts: opts.password-file != null); - - password-setter-script = user: password-file: sql-file: '' - unset PASSWORD - if [ ! -f ${password-file} ]; then - echo "file does not exist: ${password-file}" - exit 1 - fi - PASSWORD=$(cat ${password-file}) - echo "setting password for user ${user}" - echo "ALTER USER ${user} ENCRYPTED PASSWORD '$PASSWORD';" >> ${sql-file} - ''; - - passwords-setter-script = users: - pkgs.writeScript "postgres-set-passwords.sh" '' - if [ $# -ne 1 ]; then - echo "usage: $0 output-file.sql" - exit 1 - fi - - OUTPUT_FILE=$1 - - if [ ! -f $OUTPUT_FILE ]; then - echo "file doesn't exist: $OUTPUT_FILE" - exit 2 - fi - - ${join-lines (mapAttrsToList (user: opts: - password-setter-script user opts.password-file "$OUTPUT_FILE") - (filterPasswordedUsers users))} - ''; - - userDatabaseAccess = user: databases: - mapAttrs' (database: databaseOpts: - nameValuePair "DATABASE ${database}" databaseOpts.access) databases; - - makeEntry = nw: - "host all all ${nw} gss include_realm=0 krb_realm=${gssapi-realm}"; - - makeNetworksEntry = networks: join-lines (map makeEntry networks); - - makeLocalUserPasswordEntries = users: - join-lines (mapAttrsToList (user: opts: - join-lines (map (db: '' - local ${db} ${user} md5 - host ${db} ${user} 127.0.0.1/16 md5 - host ${db} ${user} ::1/128 md5 - '') (attrNames opts.databases))) (filterPasswordedUsers users)); - - userTableAccessSql = user: entity: access: - "GRANT ${access} ON ${entity} TO ${user};"; - userDatabaseAccessSql = user: database: dbOpts: '' - \c ${database} - ${join-lines - (mapAttrsToList (userTableAccessSql user) dbOpts.entity-access)} - ''; - userAccessSql = user: userOpts: - join-lines (mapAttrsToList (userDatabaseAccessSql user) userOpts.databases); - usersAccessSql = users: join-lines (mapAttrsToList userAccessSql users); - -in { - - options.fudo.postgresql = with types; { - enable = mkEnableOption "Fudo PostgreSQL Server"; - - ssl-private-key = mkOption { - type = str; - description = "Location of the server SSL private key."; - }; - - ssl-certificate = mkOption { - type = str; - description = "Location of the server SSL certificate."; - }; - - keytab = mkOption { - type = str; - description = "Location of the server Kerberos keytab."; - }; - - local-networks = mkOption { - type = listOf str; - description = "A list of networks from which to accept connections."; - example = [ "10.0.0.1/16" ]; - default = [ ]; - }; - - users = mkOption { - type = attrsOf (submodule userOpts); - description = "A map of users to user attributes."; - example = { - sampleUser = { - password-file = "/path/to/password/file"; - databases = { - some_database = { - access = "CONNECT"; - entity-access = { "TABLE some_table" = "SELECT,UPDATE"; }; - }; - }; - }; - }; - default = { }; - }; - - databases = mkOption { - type = attrsOf (submodule databaseOpts); - description = "A map of databases to database options."; - default = { }; - }; - - socket-directory = mkOption { - type = str; - description = "Directory in which to place unix sockets."; - default = "/run/postgresql"; - }; - - socket-group = mkOption { - type = str; - description = "Group for accessing sockets."; - default = "postgres_local"; - }; - - local-users = mkOption { - type = listOf str; - description = "Users able to access the server via local socket."; - default = [ ]; - }; - - required-services = mkOption { - 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 { - - environment = { - systemPackages = with pkgs; [ postgresql_11_gssapi ]; - - # 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/private/postgres.keytab" = { - # mode = "0400"; - # user = "postgres"; - # group = "postgres"; - # source = cfg.keytab; - # }; - # }; - }; - - users.groups = { - ${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; }; - }; - - services.postgresql = { - enable = true; - package = pkgs.postgresql_11_gssapi; - enableTCPIP = true; - ensureDatabases = mapAttrsToList (name: value: name) cfg.databases; - ensureUsers = ((mapAttrsToList (username: attrs: { - name = username; - ensurePermissions = userDatabaseAccess username attrs.databases; - }) cfg.users) ++ (flatten (mapAttrsToList (database: opts: - (map (username: { - name = username; - ensurePermissions = { "DATABASE ${database}" = "ALL PRIVILEGES"; }; - }) opts.users)) cfg.databases))); - - settings = { - krb_server_keyfile = cfg.keytab; - - ssl = true; - 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; - unix_socket_permissions = "0777"; - }; - - authentication = lib.mkForce '' - ${makeLocalUserPasswordEntries cfg.users} - - local all all ident - - # host-local - host all all 127.0.0.1/32 gss include_realm=0 krb_realm=${gssapi-realm} - host all all ::1/128 gss include_realm=0 krb_realm=${gssapi-realm} - - # local networks - ${makeNetworksEntry cfg.local-networks} - ''; - - dataDir = mkIf (cfg.state-directory != null) cfg.state-directory; - }; - - systemd = { - - 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 = - pkgs.writeScript "password-script-wrapper.sh" '' - TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t postgres-XXXXXXXXXX) - echo "using temp dir $TMPDIR" - PASSWORD_SQL_FILE=$TMPDIR/user-passwords.sql - echo "password file $PASSWORD_SQL_FILE" - touch $PASSWORD_SQL_FILE - chown ${config.services.postgresql.superUser} $PASSWORD_SQL_FILE - chmod go-rwx $PASSWORD_SQL_FILE - ${passwords-script} $PASSWORD_SQL_FILE - echo "executing $PASSWORD_SQL_FILE" - ${pkgs.postgresql}/bin/psql --port ${ - toString config.services.postgresql.port - } -d postgres -f $PASSWORD_SQL_FILE - echo rm $PASSWORD_SQL_FILE - echo "Postgresql user passwords set."; - exit 0 - ''; - - in { - description = - "A service to set postgresql user passwords after the server has started."; - after = [ "postgresql.service" ] ++ cfg.required-services; - requires = [ "postgresql.service" ] ++ cfg.required-services; - serviceConfig = { - Type = "oneshot"; - User = config.services.postgresql.superUser; - }; - partOf = [ cfg.systemd-target ]; - script = "${password-wrapper-script}"; - }; - - postgresql = { - requires = cfg.required-services; - after = cfg.required-services; - partOf = [ cfg.systemd-target ]; - - 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))} - ${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* - ''; - - postStop = concatStringsSep "\n" cfg.cleanup-tasks; - }; - }; - }; - }; -} diff --git a/lib/fudo/prometheus.nix b/lib/fudo/prometheus.nix deleted file mode 100644 index 450baaf..0000000 --- a/lib/fudo/prometheus.nix +++ /dev/null @@ -1,207 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - inherit (lib.strings) concatStringsSep; - cfg = config.fudo.prometheus; - -in { - - options.fudo.prometheus = { - enable = mkEnableOption "Fudo Prometheus Data-Gathering Server"; - - service-discovery-dns = mkOption { - type = with types; attrsOf (listOf str); - description = '' - A map of exporter type to a list of domains to use for service discovery. - ''; - example = { - node = [ "node._metrics._tcp.my-domain.com" ]; - postfix = [ "postfix._metrics._tcp.my-domain.com" ]; - }; - default = { - dovecot = []; - node = []; - postfix = []; - rspamd = []; - }; - }; - - static-targets = mkOption { - type = with types; attrsOf (listOf str); - description = '' - A map of exporter type to a list of host:ports from which to collect metrics. - ''; - example = { - node = [ "my-host.my-domain:1111" ]; - }; - default = { - dovecot = []; - node = []; - postfix = []; - rspamd = []; - }; - }; - - docker-hosts = mkOption { - type = with types; listOf str; - description = '' - A list of explicit docker targets from which to gather node data. - ''; - default = []; - }; - - push-url = mkOption { - type = with types; nullOr str; - description = '' - The that services can use to manually push data. - ''; - default = null; - }; - - push-address = mkOption { - type = with types; nullOr str; - description = '' - The address on which to listen for incoming data. - ''; - default = null; - }; - - hostname = mkOption { - type = with types; str; - description = "The hostname upon which Prometheus will serve."; - example = "my-metrics-server.fudo.org"; - }; - }; - - config = mkIf cfg.enable { - services.nginx = { - enable = true; - - virtualHosts = { - "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; - - locations."/" = { - proxyPass = "http://127.0.0.1:9090"; - - extraConfig = let - local-networks = config.instance.local-networks; - in '' - 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; - - ${optionalString ((length local-networks) > 0) - (concatStringsSep "\n" (map (network: "allow ${network};") local-networks)) + "\ndeny all;"} - ''; - }; - }; - }; - }; - - services.prometheus = { - - enable = true; - - webExternalUrl = "https://${cfg.hostname}"; - - listenAddress = "127.0.0.1"; - port = 9090; - - scrapeConfigs = [ - { - job_name = "docker"; - honor_labels = false; - static_configs = [ - { - targets = cfg.docker-hosts; - } - ]; - } - - { - job_name = "node"; - scheme = "https"; - metrics_path = "/metrics/node"; - honor_labels = false; - dns_sd_configs = [ - { - names = cfg.service-discovery-dns.node; - } - ]; - static_configs = [ - { - targets = cfg.static-targets.node; - } - ]; - } - - { - job_name = "dovecot"; - scheme = "https"; - metrics_path = "/metrics/dovecot"; - honor_labels = false; - dns_sd_configs = [ - { - names = cfg.service-discovery-dns.dovecot; - } - ]; - static_configs = [ - { - targets = cfg.static-targets.dovecot; - } - ]; - } - - { - job_name = "postfix"; - scheme = "https"; - metrics_path = "/metrics/postfix"; - honor_labels = false; - dns_sd_configs = [ - { - names = cfg.service-discovery-dns.postfix; - } - ]; - static_configs = [ - { - targets = cfg.static-targets.postfix; - } - ]; - } - - { - job_name = "rspamd"; - scheme = "https"; - metrics_path = "/metrics/rspamd"; - honor_labels = false; - dns_sd_configs = [ - { - names = cfg.service-discovery-dns.rspamd; - } - ]; - static_configs = [ - { - targets = cfg.static-targets.rspamd; - } - ]; - } - ]; - - pushgateway = { - enable = if (cfg.push-url != null) then true else false; - web = { - external-url = if cfg.push-url == null then - cfg.push-address - else - cfg.push-url; - listen-address = cfg.push-address; - }; - }; - }; - }; -} diff --git a/lib/fudo/secrets.nix b/lib/fudo/secrets.nix deleted file mode 100644 index 344371a..0000000 --- a/lib/fudo/secrets.nix +++ /dev/null @@ -1,221 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.secrets; - - encrypt-on-disk = { secret-name, target-host, target-pubkey, source-file }: - pkgs.stdenv.mkDerivation { - name = "${target-host}-${secret-name}-secret"; - phases = "installPhase"; - buildInputs = [ pkgs.age ]; - installPhase = '' - age -a -r "${target-pubkey}" -o $out ${source-file} - ''; - }; - - decrypt-script = { secret-name, source-file, target-host, target-file - , host-master-key, user, group, permissions }: - pkgs.writeShellScript "decrypt-fudo-secret-${target-host}-${secret-name}.sh" '' - rm -f ${target-file} - touch ${target-file} - chown ${user}:${group} ${target-file} - chmod ${permissions} ${target-file} - # NOTE: silly hack because sometimes age leaves a blank line - # Only include lines with at least one non-space character - SRC=$(mktemp fudo-secret-${target-host}-${secret-name}.XXXXXXXX) - cat ${encrypt-on-disk { - inherit secret-name source-file target-host; - target-pubkey = host-master-key.public-key; - }} | grep "[^ ]" > $SRC - age -d -i ${host-master-key.key-path} -o ${target-file} $SRC - rm -f $SRC - ''; - - secret-service = target-host: secret-name: - { source-file, target-file, user, group, permissions, ... }: { - description = "decrypt secret ${secret-name} for ${target-host}."; - wantedBy = [ "default.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = let - host-master-key = config.fudo.hosts.${target-host}.master-key; - in decrypt-script { - inherit secret-name source-file target-host target-file host-master-key - user group permissions; - }; - }; - path = [ pkgs.age ]; - }; - - secretOpts = { name, ... }: { - options = with types; { - source-file = mkOption { - type = path; # CAREFUL: this will copy the file to nixstore...keep on deploy host - description = "File from which to load the secret. If unspecified, a random new password will be generated."; - default = "${generate-secret name}/passwd"; - }; - - target-file = mkOption { - type = str; - description = - "Target file on the host; the secret will be decrypted to this file."; - }; - - user = mkOption { - type = str; - description = "User (on target host) to which the file will belong."; - }; - - group = mkOption { - type = str; - description = "Group (on target host) to which the file will belong."; - default = "nogroup"; - }; - - permissions = mkOption { - type = str; - description = "Permissions to set on the target file."; - default = "0400"; - }; - - metadata = mkOption { - type = attrsOf anything; - description = "Arbitrary metadata associated with this secret."; - default = {}; - }; - }; - }; - - nix-build-users = let usernames = attrNames config.users.users; - in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null) - usernames; - - generate-secret = name: pkgs.stdenv.mkDerivation { - name = "${name}-generated-passwd"; - - phases = [ "installPhase" ]; - - buildInputs = with pkgs; [ pwgen ]; - - buildPhase = '' - echo "${name}-${config.instance.build-timestamp}" >> file.txt - pwgen --secure --symbols --num-passwords=1 --sha1=file.txt 40 > passwd - rm -f file.txt - ''; - - installPhase = '' - mkdir $out - mv passwd $out/passwd - ''; - }; - -in { - options.fudo.secrets = with types; { - enable = mkOption { - type = bool; - description = "Include secrets in the build (disable when secrets are unavailable)"; - default = true; - }; - - host-secrets = mkOption { - type = attrsOf (attrsOf (submodule secretOpts)); - description = "Map of hosts to host secrets"; - default = { }; - }; - - host-deep-secrets = mkOption { - type = attrsOf (attrsOf (submodule secretOpts)); - description = '' - Secrets that are only passed during deployment. - - These secrets will be passed as nixops deployment secrets, - _unlike_ regular secrets that are passed to hosts as part of - the nixops store, but encrypted with the host SSH key. Regular - secrets are kept secret from normal users. These secrets will - be kept secret from _everybody_. However, they won't be - available on the host at boot until a new deployment occurs. - ''; - default = { }; - }; - - secret-users = mkOption { - type = listOf str; - description = "List of users with read-access to secrets."; - default = [ ]; - }; - - secret-group = mkOption { - type = str; - description = "Group to which secrets will belong."; - default = "nixops-secrets"; - }; - - secret-paths = mkOption { - type = listOf str; - description = - "Paths which contain (only) secrets. The contents will be reabable by the secret-group."; - default = [ ]; - }; - }; - - config = mkIf cfg.enable { - users.groups = { - ${cfg.secret-group} = { - members = cfg.secret-users ++ nix-build-users; - }; - }; - - systemd = let - hostname = config.instance.hostname; - - host-secrets = if (hasAttr hostname cfg.host-secrets) then - cfg.host-secrets.${hostname} - else - { }; - - host-secret-services = mapAttrs' (secret: secretOpts: - (nameValuePair "fudo-secret-${hostname}-${secret}" - (secret-service hostname secret secretOpts))) host-secrets; - - trace-all = obj: builtins.trace obj obj; - - host-secret-paths = mapAttrsToList - (secret: secretOpts: - let perms = if secretOpts.group != "nobody" then "550" else "500"; - in "d ${dirOf secretOpts.target-file} ${perms} ${secretOpts.user} ${secretOpts.group} - -") - host-secrets; - - build-secret-paths = - map (path: "d '${path}' - root ${cfg.secret-group} - -") - cfg.secret-paths; - - in { - tmpfiles.rules = host-secret-paths ++ build-secret-paths; - - services = host-secret-services // { - fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) { - wantedBy = [ "default.target" ]; - description = - "Ensure access for group ${cfg.secret-group} to fudo secret paths."; - serviceConfig = { - ExecStart = pkgs.writeShellScript "fudo-secrets-watcher.sh" - (concatStringsSep "\n" (map (path: '' - chown -R root:${cfg.secret-group} ${path} - chmod -R u=rwX,g=rX,o= ${path} - '') cfg.secret-paths)); - }; - }; - }; - - paths.fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) { - wantedBy = [ "default.target" ]; - description = "Watch fudo secret paths, and correct perms on changes."; - pathConfig = { - PathChanged = cfg.secret-paths; - Unit = "fudo-secrets-watcher.service"; - }; - }; - }; - }; -} diff --git a/lib/fudo/secure-dns-proxy.nix b/lib/fudo/secure-dns-proxy.nix deleted file mode 100644 index d0afd24..0000000 --- a/lib/fudo/secure-dns-proxy.nix +++ /dev/null @@ -1,103 +0,0 @@ -{ lib, pkgs, config, ... }: - -with lib; -let - cfg = config.fudo.secure-dns-proxy; - - fudo-lib = import ../fudo-lib.nix { lib = lib; }; - -in { - options.fudo.secure-dns-proxy = with types; { - enable = - mkEnableOption "Enable a DNS server using an encrypted upstream source."; - - listen-port = mkOption { - type = port; - description = "Port on which to listen for DNS queries."; - default = 53; - }; - - upstream-dns = mkOption { - type = listOf str; - description = '' - The upstream DNS services to use, in a format useable by dnsproxy. - - See: https://github.com/AdguardTeam/dnsproxy - ''; - default = [ "https://cloudflare-dns.com/dns-query" ]; - }; - - bootstrap-dns = mkOption { - type = str; - description = - "A simple DNS server from which HTTPS DNS can be bootstrapped, if necessary."; - default = "1.1.1.1"; - }; - - listen-ips = mkOption { - type = listOf str; - description = "A list of local IP addresses on which to listen."; - default = [ "0.0.0.0" ]; - }; - - allowed-networks = mkOption { - type = nullOr (listOf str); - description = - "List of networks with which this job is allowed to communicate."; - default = null; - }; - - user = mkOption { - type = str; - description = "User as which to run secure DNS proxy."; - default = "secure-dns-proxy"; - }; - - group = mkOption { - type = str; - description = "Group as which to run secure DNS proxy."; - default = "secure-dns-proxy"; - }; - }; - - config = mkIf cfg.enable (let - upgrade-perms = cfg.listen-port <= 1024; - in { - users = mkIf upgrade-perms { - users = { - ${cfg.user} = { - isSystemUser = true; - group = cfg.group; - }; - }; - - groups = { - ${cfg.group} = { - members = [ cfg.user ]; - }; - }; - }; - - fudo.system.services.secure-dns-proxy = { - description = "DNS Proxy for secure DNS-over-HTTPS lookups."; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - privateNetwork = false; - requiredCapabilities = mkIf upgrade-perms [ "CAP_NET_BIND_SERVICE" ]; - restartWhen = "always"; - addressFamilies = [ "AF_INET" "AF_INET6" ]; - networkWhitelist = cfg.allowed-networks; - user = mkIf upgrade-perms cfg.user; - group = mkIf upgrade-perms cfg.group; - - execStart = let - upstreams = map (upstream: "-u ${upstream}") cfg.upstream-dns; - upstream-line = concatStringsSep " " upstreams; - listen-line = - concatStringsSep " " (map (listen: "-l ${listen}") cfg.listen-ips); - in "${pkgs.dnsproxy}/bin/dnsproxy -p ${ - toString cfg.listen-port - } ${upstream-line} ${listen-line} -b ${cfg.bootstrap-dns}"; - }; - }); -} diff --git a/lib/fudo/sites.nix b/lib/fudo/sites.nix deleted file mode 100644 index 384203f..0000000 --- a/lib/fudo/sites.nix +++ /dev/null @@ -1,240 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - site-name = config.fudo.hosts.${hostname}.site; - site-cfg = config.fudo.sites.${site-name}; - - site-hosts = filterAttrs (hostname: hostOpts: hostOpts.site == site-name) - config.fudo.hosts; - - siteOpts = { site, ... }: { - options = with types; { - site = mkOption { - type = str; - description = "Site name."; - default = site; - }; - - network = mkOption { - type = str; - description = "Network to be treated as local."; - }; - - dynamic-network = mkOption { - type = nullOr str; - description = "Network to be allocated by DHCP."; - default = null; - }; - - gateway-v4 = mkOption { - type = nullOr str; - description = "Gateway to use for public ipv4 internet access."; - default = null; - }; - - gateway-v6 = mkOption { - type = nullOr str; - description = "Gateway to use for public ipv6 internet access."; - default = null; - }; - - local-groups = mkOption { - type = listOf str; - description = "List of groups which should exist at this site."; - default = [ ]; - }; - - local-users = mkOption { - type = listOf str; - description = - "List of users which should exist on all hosts at this site."; - default = [ ]; - }; - - local-admins = mkOption { - type = listOf str; - description = - "List of admin users which should exist on all hosts at this site."; - default = [ ]; - }; - - enable-monitoring = - mkEnableOption "Enable site-wide monitoring with prometheus."; - - nameservers = mkOption { - type = listOf str; - description = "List of nameservers to be used by hosts at this site."; - default = [ ]; - }; - - timezone = mkOption { - type = str; - description = "Timezone of the site."; - example = "America/Winnipeg"; - }; - - deploy-pubkeys = mkOption { - type = nullOr (listOf str); - description = "SSH pubkey of site deploy key. Used by dropbear daemon."; - default = null; - }; - - enable-ssh-backdoor = mkOption { - type = bool; - description = - "Enable a backup SSH server in case of failures of the primary."; - default = true; - }; - - dropbear-rsa-key-path = mkOption { - type = str; - description = "Location of Dropbear RSA key."; - default = "/etc/dropbear/host_rsa_key"; - }; - - dropbear-ecdsa-key-path = mkOption { - type = str; - description = "Location of Dropbear ECDSA key."; - default = "/etc/dropbear/host_ecdsa_key"; - }; - - dropbear-ssh-port = mkOption { - type = port; - description = "Port to be used for the backup SSH server."; - default = 2112; - }; - - enable-distributed-builds = - mkEnableOption "Enable distributed builds for the site."; - - build-servers = mkOption { - type = attrsOf (submodule buildServerOpts); - description = - "List of hosts to be used as build servers for the local site."; - default = { }; - example = { - my-build-host = { - port = 22; - systems = [ "i686-linux" "x86_64-linux" ]; - build-user = "my-builder"; - }; - }; - }; - - local-networks = mkOption { - type = listOf str; - description = "List of networks to consider local at this site."; - default = [ ]; - }; - - mail-server = mkOption { - type = str; - description = "Hostname of the mail server to use for this site."; - }; - }; - }; - - buildServerOpts = { hostname, ... }: { - options = with types; { - port = mkOption { - type = port; - description = "SSH port at which to contact the server."; - default = 22; - }; - - systems = mkOption { - type = listOf str; - description = - "A list of systems for which this build server can build."; - default = [ "i686-linux" "x86_64-linux" ]; - }; - - max-jobs = mkOption { - type = int; - description = "Max build allowed per-system."; - default = 1; - }; - - speed-factor = mkOption { - type = int; - description = "Weight to give this server, i.e. it's relative speed."; - default = 1; - }; - - supported-features = mkOption { - type = listOf str; - description = "List of features supported by this server."; - default = [ ]; - }; - - build-user = mkOption { - type = str; - description = "User as which to run distributed builds."; - default = "nix-site-builder"; - }; - }; - }; - -in { - options.fudo.sites = mkOption { - type = with types; attrsOf (submodule siteOpts); - description = "Site configurations for all sites known to the system."; - default = { }; - }; - - config = { - networking.firewall.allowedTCPPorts = - mkIf site-cfg.enable-ssh-backdoor [ site-cfg.dropbear-ssh-port ]; - - systemd = mkIf site-cfg.enable-ssh-backdoor { - sockets = { - dropbear-deploy = { - wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = "0.0.0.0:${toString site-cfg.dropbear-ssh-port}"; - Accept = true; - }; - unitConfig = { restartIfChanged = true; }; - }; - }; - - services = { - dropbear-deploy-init = { - wantedBy = [ "multi-user.target" ]; - script = '' - if [ ! -d /etc/dropbear ]; then - mkdir /etc/dropbear - chmod 700 /etc/dropbear - fi - - if [ ! -f ${site-cfg.dropbear-rsa-key-path} ]; then - ${pkgs.dropbear}/bin/dropbearkey -t rsa -f ${site-cfg.dropbear-rsa-key-path} - ${pkgs.coreutils}/bin/chmod 0400 ${site-cfg.dropbear-rsa-key-path} - fi - - if [ ! -f ${site-cfg.dropbear-ecdsa-key-path} ]; then - ${pkgs.dropbear}/bin/dropbearkey -t ecdsa -f ${site-cfg.dropbear-ecdsa-key-path} - ${pkgs.coreutils}/bin/chmod 0400 ${site-cfg.dropbear-ecdsa-key-path} - fi - ''; - }; - - "dropbear-deploy@" = { - description = - "Per-connection service for deployment, using dropbear."; - requires = [ "dropbear-deploy-init.service" ]; - after = [ "network.target" ]; - serviceConfig = { - Type = "simple"; - ExecStart = - "${pkgs.dropbear}/bin/dropbear -F -i -w -m -j -k -r ${site-cfg.dropbear-rsa-key-path} -r ${site-cfg.dropbear-ecdsa-key-path}"; - ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID"; - StandardInput = "socket"; - }; - }; - }; - }; - }; -} diff --git a/lib/fudo/slynk.nix b/lib/fudo/slynk.nix deleted file mode 100644 index 8ae12ed..0000000 --- a/lib/fudo/slynk.nix +++ /dev/null @@ -1,70 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.slynk; - - initScript = port: load-paths: let - load-path-string = - concatStringsSep " " (map (path: "\"${path}\"") load-paths); - in pkgs.writeText "slynk.lisp" '' - (load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname))) - (ql:quickload :slynk) - (setf asdf:*central-registry* - (append asdf:*central-registry* - (list ${load-path-string}))) - (slynk:create-server :port ${toString port} :dont-close t) - (dolist (var '("LD_LIBRARY_PATH")) - (format t "~S: ~S~%" var (sb-unix::posix-getenv var))) - - (loop (sleep 60)) - ''; - - lisp-libs = with pkgs.lispPackages; [ - alexandria - asdf-package-system - asdf-system-connections - cl_plus_ssl - cl-ppcre - quicklisp - quri - uiop - usocket - ]; - -in { - options.fudo.slynk = { - enable = mkEnableOption "Enable Slynk emacs common lisp server."; - - port = mkOption { - type = types.int; - description = "Port on which to open a Slynk server."; - default = 4005; - }; - }; - - config = mkIf cfg.enable { - systemd.user.services.slynk = { - description = "Slynk Common Lisp server."; - - serviceConfig = let - load-paths = (map (pkg: "${pkg}/lib/common-lisp/") lisp-libs); - in { - ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init"; - ExecStart = "${pkgs.sbcl}/bin/sbcl --load ${initScript cfg.port load-paths}"; - Restart = "on-failure"; - PIDFile = "/run/slynk.$USERNAME.pid"; - }; - - path = with pkgs; [ - gcc - glibc # for getent - file - ]; - - environment = { - LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib"; - }; - }; - }; -} diff --git a/lib/fudo/ssh.nix b/lib/fudo/ssh.nix deleted file mode 100644 index 3f1f965..0000000 --- a/lib/fudo/ssh.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -{ - config = { - programs.ssh.knownHosts = let - keyed-hosts = - filterAttrs (h: o: o.ssh-pubkeys != []) - config.fudo.hosts; - - crossProduct = f: list0: list1: - concatMap (el0: map (el1: f el0 el1) list1) list0; - - all-hostnames = hostname: opts: - [ hostname ] ++ - (crossProduct (host: domain: "${host}.${domain}") - ([ hostname ] ++ opts.aliases) - ([ opts.domain ] ++ opts.extra-domains)); - - in mapAttrs (hostname: hostOpts: { - publicKeyFile = builtins.head hostOpts.ssh-pubkeys; - hostNames = all-hostnames hostname hostOpts; - }) keyed-hosts; - }; -} diff --git a/lib/fudo/system-networking.nix b/lib/fudo/system-networking.nix deleted file mode 100644 index 07c13c5..0000000 --- a/lib/fudo/system-networking.nix +++ /dev/null @@ -1,168 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.fudo.system; - - portMappingOpts = { name, ... }: { - options = with types; { - internal-port = mkOption { - type = port; - description = "Port on localhost to recieve traffic"; - }; - external-port = mkOption { - type = port; - description = "External port on which to listen for traffic."; - }; - protocols = mkOption { - type = listOf str; - description = - "Protocols for which to forward ports. Default is tcp-only."; - default = [ "tcp" ]; - }; - }; - }; - -in { - options.fudo.system = with types; { - internal-port-map = mkOption { - type = attrsOf (submodule portMappingOpts); - description = - "Sets of external ports to internal (i.e. localhost) ports to forward."; - default = { }; - example = { - sshmap = { - internal-port = 2222; - external-port = 22; - protocol = "udp"; - }; - }; - }; - - # DO THIS MANUALLY since NixOS sux at making a reasonable /etc/hosts - hostfile-entries = mkOption { - type = attrsOf (listOf str); - description = "Map of extra IP addresses to hostnames for /etc/hosts"; - default = {}; - example = { - "10.0.0.3" = [ "my-host" "my-host.my.domain" ]; - }; - }; - }; - - config = mkIf (cfg.internal-port-map != { }) { - # FIXME: FUCK ME THIS IS WAY HARDER THAN IT SHOULD BE - # boot.kernel.sysctl = mkIf (cfg.internal-port-map != { }) { - # "net.ipv4.conf.all.route_localnet" = "1"; - # }; - - # fudo.system.services.forward-internal-ports = let - # ip-line = op: src-port: target-port: protocol: '' - # ${ipt} -t nat -${op} PREROUTING -p ${protocol} --dport ${ - # toString src-port - # } -j REDIRECT --to-ports ${toString target-port} - # ${ipt} -t nat -${op} OUTPUT -p ${protocol} -s lo --dport ${ - # toString src-port - # } -j REDIRECT --to-ports ${toString target-port} - # ''; - - # ip-forward-line = ip-line "I"; - - # ip-unforward-line = ip-line "D"; - - # traceOut = obj: builtins.trace obj obj; - - # concatMapAttrsToList = f: attrs: concatLists (mapAttrsToList f attrs); - - # portmap-entries = concatMapAttrsToList (name: opts: - # map (protocol: { - # src = opts.external-port; - # target = opts.internal-port; - # protocol = protocol; - # }) opts.protocols) cfg.internal-port-map; - - # make-entries = f: { src, target, protocol, ... }: f src target protocol; - - # forward-entries = map (make-entries ip-forward-line) portmap-entries; - - # unforward-entries = map (make-entries ip-unforward-line) portmap-entries; - - # forward-ports-script = pkgs.writeShellScript "forward-internal-ports.sh" - # (concatStringsSep "\n" forward-entries); - - # unforward-ports-script = - # pkgs.writeShellScript "unforward-internal-ports.sh" - # (concatStringsSep "\n" - # (map (make-entries ip-unforward-line) portmap-entries)); - # in { - # wantedBy = [ "multi-user.target" ]; - # after = [ "firewall.service" "nat.service" ]; - # type = "oneshot"; - # description = "Rules for forwarding external ports to local ports."; - # execStart = "${forward-ports-script}"; - # execStop = "${unforward-ports-script}"; - # requiredCapabilities = - # [ "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ]; - # }; - - # networking.firewall = let - # iptables = "ip46tables"; - # ip-forward-line = protocols: internal: external: - # concatStringsSep "\n" (map (protocol: '' - # ${iptables} -t nat -I PREROUTING -p ${protocol} --dport ${ - # toString external - # } -j REDIRECT --to-ports ${toString internal} - # ${iptables} -t nat -I OUTPUT -s lo -p ${protocol} --dport ${ - # toString external - # } -j REDIRECT --to-ports ${toString internal} - # '') protocols); - - # ip-unforward-line = protocols: internal: external: - # concatStringsSep "\n" (map (protocol: '' - # ${iptables} -t nat -D PREROUTING -p ${protocol} --dport ${ - # toString external - # } -j REDIRECT --to-ports ${toString internal} - # ${iptables} -t nat -D OUTPUT -s lo -p ${protocol} --dport ${ - # toString external - # } -j REDIRECT --to-ports ${toString internal} - # '') protocols); - # in { - # enable = true; - - # extraCommands = concatStringsSep "\n" (mapAttrsToList (name: opts: - # ip-forward-line opts.protocols opts.internal-port opts.external-port) - # cfg.internal-port-map); - - # extraStopCommands = concatStringsSep "\n" (mapAttrsToList (name: opts: - # ip-unforward-line opts.protocols opts.internal-port opts.external-port) - # cfg.internal-port-map); - # }; - - # networking.nat.forwardPorts = - # let portmaps = (attrValues opts.external-port); - # in concatMap (opts: - # map (protocol: { - # destination = "127.0.0.1:${toString opts.internal-port}"; - # sourcePort = opts.external-port; - # proto = protocol; - # }) opts.protocols) (attrValues cfg.internal-port-map); - - # services.xinetd = mkIf ((length (attrNames cfg.internal-port-map)) > 0) { - # enable = true; - # services = let - # svcs = mapAttrsToList (name: opts: opts // { name = name; }) - # cfg.internal-port-map; - # svcs-protocols = concatMap - # (svc: map (protocol: svc // { protocol = protocol; }) svc.protocols) - # svcs; - # in map (opts: { - # name = opts.name; - # unlisted = true; - # port = opts.external-port; - # server = "${pkgs.coreutils}/bin/false"; - # extraConfig = "redirect = localhost ${toString opts.internal-port}"; - # protocol = opts.protocol; - # }) svcs-protocols; - # }; - }; -} diff --git a/lib/fudo/system.nix b/lib/fudo/system.nix deleted file mode 100644 index edd6844..0000000 --- a/lib/fudo/system.nix +++ /dev/null @@ -1,500 +0,0 @@ -{ pkgs, lib, config, ... }: - -with lib; -let - cfg = config.fudo.system; - - mkDisableOption = description: - mkOption { - type = types.bool; - default = true; - description = description; - }; - - isEmpty = lst: 0 == (length lst); - - serviceOpts = { name, ... }: - with types; { - options = { - after = mkOption { - type = listOf str; - description = "List of services to start before this one."; - default = [ ]; - }; - script = mkOption { - type = nullOr str; - description = "Simple shell script for the service to run."; - default = null; - }; - reloadScript = mkOption { - type = nullOr str; - description = "Script to run whenever the service is restarted."; - default = null; - }; - before = mkOption { - type = listOf str; - description = - "List of services before which this service should be started."; - default = [ ]; - }; - requires = mkOption { - type = listOf str; - description = - "List of services on which this service depends. If they fail to start, this service won't start."; - default = [ ]; - }; - preStart = mkOption { - type = nullOr str; - description = "Script to run prior to starting this service."; - default = null; - }; - postStart = mkOption { - type = nullOr str; - description = "Script to run after starting this service."; - default = null; - }; - preStop = mkOption { - type = nullOr str; - description = "Script to run prior to stopping this service."; - default = null; - }; - postStop = mkOption { - type = nullOr str; - description = "Script to run after stopping this service."; - default = null; - }; - requiredBy = mkOption { - type = listOf str; - description = - "List of services which require this service, and should fail without it."; - default = [ ]; - }; - wantedBy = mkOption { - type = listOf str; - default = [ ]; - description = - "List of services before which this service should be started."; - }; - environment = mkOption { - type = attrsOf str; - description = "Environment variables supplied to this service."; - default = { }; - }; - environment-file = mkOption { - type = nullOr str; - description = - "File containing environment variables supplied to this service."; - default = null; - }; - description = mkOption { - type = str; - description = "Description of the service."; - }; - path = mkOption { - type = listOf package; - description = - "A list of packages which should be in the service PATH."; - default = [ ]; - }; - restartIfChanged = - mkDisableOption "Restart the service if the definition changes."; - dynamicUser = mkDisableOption "Create a new user for this service."; - privateNetwork = mkDisableOption "Only allow access to localhost."; - privateUsers = - mkDisableOption "Don't allow access to system user list."; - privateDevices = mkDisableOption - "Restrict access to system devices other than basics."; - privateTmp = mkDisableOption "Limit service to a private tmp dir."; - protectControlGroups = - mkDisableOption "Don't allow service to modify control groups."; - protectClock = - mkDisableOption "Don't allow service to modify system clock."; - restrictSuidSgid = - mkDisableOption "Don't allow service to suid or sgid binaries."; - protectKernelTunables = - mkDisableOption "Don't allow service to modify kernel tunables."; - privateMounts = - mkDisableOption "Don't allow service to access mounted devices."; - protectKernelModules = mkDisableOption - "Don't allow service to load or evict kernel modules."; - protectHome = mkDisableOption "Limit access to home directories."; - protectHostname = - mkDisableOption "Don't allow service to modify hostname."; - protectKernelLogs = - mkDisableOption "Don't allow access to kernel logs."; - lockPersonality = mkDisableOption "Lock service 'personality'."; - restrictRealtime = - mkDisableOption "Restrict service from using realtime functionality."; - restrictNamespaces = - mkDisableOption "Restrict service from using namespaces."; - memoryDenyWriteExecute = mkDisableOption - "Restrict process from executing from writable memory."; - keyringMode = mkOption { - type = str; - default = "private"; - description = "Sharing state of process keyring."; - }; - requiredCapabilities = mkOption { - type = listOf (enum capabilities); - default = [ ]; - description = "List of capabilities granted to the service."; - }; - restartWhen = mkOption { - type = str; - default = "on-failure"; - description = "Conditions under which process should be restarted."; - }; - restartSec = mkOption { - type = int; - default = 10; - description = "Number of seconds to wait before restarting service."; - }; - execStart = mkOption { - type = nullOr str; - default = null; - description = "Command to run to launch the service."; - }; - execStop = mkOption { - type = nullOr str; - default = null; - description = "Command to run to launch the service."; - }; - protectSystem = mkOption { - type = enum [ "true" "false" "full" "strict" true false ]; - default = "full"; - description = - "Level of protection to apply to the system for this service."; - }; - addressFamilies = mkOption { - type = listOf (enum address-families); - default = [ ]; - description = "List of address families which the service can use."; - }; - workingDirectory = mkOption { - type = nullOr path; - default = null; - description = "Directory in which to launch the service."; - }; - user = mkOption { - type = nullOr str; - default = null; - description = "User as which to launch this service."; - }; - group = mkOption { - type = nullOr str; - default = null; - description = "Primary group as which to launch this service."; - }; - type = mkOption { - type = - enum [ "simple" "exec" "forking" "oneshot" "dbus" "notify" "idle" ]; - default = "simple"; - description = "Systemd service type of this service."; - }; - partOf = mkOption { - type = listOf str; - default = [ ]; - description = - "List of targets to which this service belongs (and with which it should be restarted)."; - }; - standardOutput = mkOption { - type = str; - default = "journal"; - description = "Destination of standard output for this service."; - }; - standardError = mkOption { - type = str; - default = "journal"; - description = "Destination of standard error for this service."; - }; - pidFile = mkOption { - type = nullOr str; - default = null; - description = "Service PID file."; - }; - networkWhitelist = mkOption { - type = nullOr (listOf str); - default = null; - description = - "A list of networks with which this process may communicate."; - }; - allowedSyscalls = mkOption { - type = listOf (enum syscalls); - default = [ ]; - description = "System calls which the service is permitted to make."; - }; - maximumUmask = mkOption { - type = str; - default = "0077"; - description = "Umask to apply to files created by the service."; - }; - startOnlyPerms = mkDisableOption "Disable perms after startup."; - onCalendar = mkOption { - type = nullOr str; - description = - "Schedule on which the job should be invoked. See: man systemd.time(7)."; - default = null; - }; - runtimeDirectory = mkOption { - type = nullOr str; - description = - "Directory created at runtime with perms for the service to read/write."; - default = null; - }; - readWritePaths = mkOption { - type = listOf str; - description = - "A list of paths to which the service will be allowed normal access, even if ProtectSystem=strict."; - default = [ ]; - }; - stateDirectory = mkOption { - type = nullOr str; - description = - "State directory for the service, available via STATE_DIRECTORY."; - default = null; - }; - cacheDirectory = mkOption { - type = nullOr str; - description = - "Cache directory for the service, available via CACHE_DIRECTORY."; - default = null; - }; - inaccessiblePaths = mkOption { - type = listOf str; - description = - "A list of paths which should be inaccessible to the service."; - default = [ "/home" "/root" ]; - }; - # noExecPaths = mkOption { - # type = listOf str; - # description = - # "A list of paths where the service will not be allowed to run executables."; - # default = [ "/home" "/root" "/tmp" "/var" ]; - # }; - readOnlyPaths = mkOption { - type = listOf str; - description = - "A list of paths to which will be read-only for the service."; - default = [ ]; - }; - execPaths = mkOption { - type = listOf str; - description = - "A list of paths where the service WILL be allowed to run executables."; - default = [ ]; - }; - }; - }; - - # See: man capabilities(7) - capabilities = [ - "CAP_AUDIT_CONTROL" - "CAP_AUDIT_READ" - "CAP_AUDIT_WRITE" - "CAP_BLOCK_SUSPEND" - "CAP_BPF" - "CAP_CHECKPOINT_RESTORE" - "CAP_CHOWN" - "CAP_DAC_OVERRIDE" - "CAP_DAC_READ_SEARCH" - "CAP_FOWNER" - "CAP_FSETID" - "CAP_IPC_LOCK" - "CAP_IPC_OWNER" - "CAP_KILL" - "CAP_LEASE" - "CAP_LINUX_IMMUTABLE" - "CAP_MAC_ADMIN" - "CAP_MAC_OVERRIDE" - "CAP_MKNOD" - "CAP_NET_ADMIN" - "CAP_NET_BIND_SERVICE" - "CAP_NET_BROADCAST" - "CAP_NET_RAW" - "CAP_PERFMON" - "CAP_SETGID" - "CAP_SETFCAP" - "CAP_SETPCAP" - "CAP_SETUID" - "CAP_SYS_ADMIN" - "CAP_SYS_BOOT" - "CAP_SYS_CHROOT" - "CAP_SYS_MODULE" - "CAP_SYS_NICE" - "CAP_SYS_PACCT" - "CAP_SYS_PTRACE" - "CAP_SYS_RAWIO" - "CAP_SYS_RESOURCE" - "CAP_SYS_TIME" - "CAP_SYS_TTY_CONFIG" - "CAP_SYSLOG" - "CAP_WAKE_ALARM" - ]; - - syscalls = [ - "@clock" - "@debug" - "@module" - "@mount" - "@raw-io" - "@reboot" - "@swap" - "@privileged" - "@resources" - "@cpu-emulation" - "@obsolete" - ]; - - address-families = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; - - restrict-capabilities = allowed: - if (allowed == [ ]) then - "~${concatStringsSep " " capabilities}" - else - concatStringsSep " " allowed; - - restrict-syscalls = allowed: - if (allowed == [ ]) then - "~${concatStringsSep " " syscalls}" - else - concatStringsSep " " allowed; - - restrict-address-families = allowed: - if (allowed == [ ]) then [ "~AF_INET" "~AF_INET6" ] else allowed; - - dirOpts = { path, ... }: { - options = with types; { - user = mkOption { - type = str; - description = "User by whom the directory will be owned."; - default = "nobody"; - }; - group = mkOption { - type = str; - description = "Group by which the directory will be owned."; - default = "nogroup"; - }; - perms = mkOption { - type = str; - description = "Permission bits to apply to the directory."; - default = "0770"; - }; - }; - }; - -in { - options.fudo.system = with types; { - services = mkOption { - type = attrsOf (submodule serviceOpts); - description = "Fudo system service definitions, with secure defaults."; - default = { }; - }; - - tmpOnTmpfs = mkOption { - type = bool; - description = "Put tmp filesystem on tmpfs (needs enough RAM)."; - default = true; - }; - - ensure-directories = mkOption { - type = attrsOf (submodule dirOpts); - description = "A map of required directories to directory properties."; - default = { }; - }; - }; - - config = { - - systemd.timers = mapAttrs (name: opts: { - enable = true; - description = opts.description; - partOf = [ "${name}.timer" ]; - wantedBy = [ "timers.target" ]; - timerConfig = { OnCalendar = opts.onCalendar; }; - }) (filterAttrs (name: opts: opts.onCalendar != null) cfg.services); - - systemd.tmpfiles.rules = mapAttrsToList - (path: opts: "d ${path} ${opts.perms} ${opts.user} ${opts.group} - -") - cfg.ensure-directories; - - systemd.targets.fudo-init = { wantedBy = [ "multi-user.target" ]; }; - - systemd.services = mapAttrs (name: opts: { - enable = true; - script = mkIf (opts.script != null) opts.script; - reload = mkIf (opts.reloadScript != null) opts.reloadScript; - after = opts.after ++ [ "fudo-init.target" ]; - before = opts.before; - requires = opts.requires; - wantedBy = opts.wantedBy; - preStart = mkIf (opts.preStart != null) opts.preStart; - postStart = mkIf (opts.postStart != null) opts.postStart; - postStop = mkIf (opts.postStop != null) opts.postStop; - preStop = mkIf (opts.preStop != null) opts.preStop; - partOf = opts.partOf; - requiredBy = opts.requiredBy; - environment = opts.environment; - description = opts.description; - restartIfChanged = opts.restartIfChanged; - path = opts.path; - serviceConfig = { - PrivateNetwork = opts.privateNetwork; - PrivateUsers = mkIf (opts.user == null) opts.privateUsers; - PrivateDevices = opts.privateDevices; - PrivateTmp = opts.privateTmp; - PrivateMounts = opts.privateMounts; - ProtectControlGroups = opts.protectControlGroups; - ProtectKernelTunables = opts.protectKernelTunables; - ProtectKernelModules = opts.protectKernelModules; - ProtectSystem = opts.protectSystem; - ProtectHostname = opts.protectHostname; - ProtectHome = opts.protectHome; - ProtectClock = opts.protectClock; - ProtectKernelLogs = opts.protectKernelLogs; - KeyringMode = opts.keyringMode; - EnvironmentFile = - mkIf (opts.environment-file != null) opts.environment-file; - - # This is more complicated than it looks... - # CapabilityBoundingSet = restrict-capabilities opts.requiredCapabilities; - AmbientCapabilities = concatStringsSep " " opts.requiredCapabilities; - SecureBits = mkIf ((length opts.requiredCapabilities) > 0) "keep-caps"; - - DynamicUser = mkIf (opts.user == null) opts.dynamicUser; - Restart = opts.restartWhen; - WorkingDirectory = - mkIf (opts.workingDirectory != null) opts.workingDirectory; - RestrictAddressFamilies = - restrict-address-families opts.addressFamilies; - RestrictNamespaces = opts.restrictNamespaces; - User = mkIf (opts.user != null) opts.user; - Group = mkIf (opts.group != null) opts.group; - Type = opts.type; - StandardOutput = opts.standardOutput; - PIDFile = mkIf (opts.pidFile != null) opts.pidFile; - LockPersonality = opts.lockPersonality; - RestrictRealtime = opts.restrictRealtime; - ExecStart = mkIf (opts.execStart != null) opts.execStart; - ExecStop = mkIf (opts.execStop != null) opts.execStop; - MemoryDenyWriteExecute = opts.memoryDenyWriteExecute; - SystemCallFilter = restrict-syscalls opts.allowedSyscalls; - UMask = opts.maximumUmask; - IpAddressAllow = - mkIf (opts.networkWhitelist != null) opts.networkWhitelist; - IpAddressDeny = mkIf (opts.networkWhitelist != null) "any"; - LimitNOFILE = "49152"; - PermissionsStartOnly = opts.startOnlyPerms; - RuntimeDirectory = - mkIf (opts.runtimeDirectory != null) opts.runtimeDirectory; - CacheDirectory = mkIf (opts.cacheDirectory != null) opts.cacheDirectory; - StateDirectory = mkIf (opts.stateDirectory != null) opts.stateDirectory; - ReadWritePaths = opts.readWritePaths; - ReadOnlyPaths = opts.readOnlyPaths; - InaccessiblePaths = opts.inaccessiblePaths; - # Apparently not supported yet? - # NoExecPaths = opts.noExecPaths; - ExecPaths = opts.execPaths; - }; - }) config.fudo.system.services; - }; -} diff --git a/lib/fudo/users.nix b/lib/fudo/users.nix deleted file mode 100644 index c95b6ed..0000000 --- a/lib/fudo/users.nix +++ /dev/null @@ -1,126 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - - user = import ../types/user.nix { inherit lib; }; - - list-includes = list: el: isNull (findFirst (this: this == el) null list); - - filterExistingUsers = users: group-members: - let user-list = attrNames users; - in filter (username: list-includes user-list username) group-members; - - hostname = config.instance.hostname; - host-cfg = config.fudo.hosts.${hostname}; - -in { - options = with types; { - fudo = { - users = mkOption { - type = attrsOf (submodule user.userOpts); - description = "Users"; - default = { }; - }; - - groups = mkOption { - type = attrsOf (submodule user.groupOpts); - description = "Groups"; - default = { }; - }; - - system-users = mkOption { - type = attrsOf (submodule user.systemUserOpts); - description = "System users (probably not what you're looking for!)"; - default = { }; - }; - }; - }; - - config = let - sys = config.instance; - in { - fudo.auth.ldap-server = { - users = filterAttrs - (username: userOpts: userOpts.ldap-hashed-passwd != null) - config.fudo.users; - - groups = config.fudo.groups; - - system-users = config.fudo.system-users; - }; - - programs.ssh.extraConfig = mkAfter '' - IdentityFile %h/.ssh/id_rsa - IdentityFile /etc/ssh/private_keys.d/%u.key - ''; - - environment.etc = mapAttrs' (username: userOpts: - nameValuePair - "ssh/private_keys.d/${username}" - { - text = concatStringsSep "\n" - (map (keypair: readFile keypair.public-key) - userOpts.ssh-keys); - }) - sys.local-users; - - users = { - users = mapAttrs (username: userOpts: { - isNormalUser = true; - uid = userOpts.uid; - createHome = true; - description = userOpts.common-name; - group = userOpts.primary-group; - home = if (userOpts.home-directory != null) then - userOpts.home-directory - else - "/home/${userOpts.primary-group}/${username}"; - hashedPassword = userOpts.login-hashed-passwd; - openssh.authorizedKeys.keys = userOpts.ssh-authorized-keys; - }) sys.local-users; - - groups = (mapAttrs (groupname: groupOpts: { - gid = groupOpts.gid; - members = filterExistingUsers sys.local-users groupOpts.members; - }) sys.local-groups) // { - wheel = { members = sys.local-admins; }; - docker = mkIf (host-cfg.docker-server) { members = sys.local-admins; }; - }; - }; - - services.nfs.idmapd.settings = let - local-domain = config.instance.local-domain; - local-admins = config.instance.local-admins; - local-users = config.instance.local-users; - local-realm = config.fudo.domains.${local-domain}.gssapi-realm; - in { - General = { - Verbosity = 10; - # Domain = local-domain; - "Local-Realms" = local-realm; - }; - Translation = { - GSS-Methods = "static"; - }; - Static = let - generate-admin-entry = admin: userOpts: - nameValuePair "${admin}/root@${local-realm}" "root"; - generate-user-entry = user: userOpts: - nameValuePair "${user}@${local-realm}" user; - - admin-entries = - mapAttrs' generate-admin-entry (getAttrs local-admins local-users); - user-entries = - mapAttrs' generate-user-entry local-users; - in admin-entries // user-entries; - }; - - # Group home directories have to exist, otherwise users can't log in - systemd.tmpfiles.rules = let - groups-with-members = attrNames - (filterAttrs (group: groupOpts: (length groupOpts.members) > 0) - sys.local-groups); - in map (group: "d /home/${group} 550 root ${group} - -") groups-with-members; - }; -} diff --git a/lib/fudo/vpn.nix b/lib/fudo/vpn.nix deleted file mode 100644 index 1ad39d2..0000000 --- a/lib/fudo/vpn.nix +++ /dev/null @@ -1,126 +0,0 @@ -{ pkgs, lib, config, ... }: - -with lib; -let - cfg = config.fudo.vpn; - - generate-pubkey-pkg = name: privkey: - pkgs.runCommand "wireguard-${name}-pubkey" { - WIREGUARD_PRIVATE_KEY = privkey; - } '' - mkdir $out - PUBKEY=$(echo $WIREGUARD_PRIVATE_KEY | ${pkgs.wireguard-tools}/bin/wg pubkey) - echo $PUBKEY > $out/pubkey.key - ''; - - generate-client-config = privkey-file: server-pubkey: network: server-ip: listen-port: dns-servers: '' - [Interface] - Address = ${ip.networkMinIp network} - PrivateKey = ${fileContents privkey-file} - ListenPort = ${toString listen-port} - DNS = ${concatStringsSep ", " dns-servers} - - [Peer] - PublicKey = ${server-pubkey} - Endpoint = ${server-ip}:${toString listen-port} - AllowedIps = 0.0.0.0/0, ::/0 - PersistentKeepalive = 25 - ''; - - generate-peer-entry = peer-name: peer-privkey-path: peer-allowed-ips: let - peer-pkg = generate-pubkey-pkg "client-${peer-name}" (fileContents peer-privkey-path); - pubkey-path = "${peer-pkg}/pubkey.key"; - in { - publicKey = fileContents pubkey-path; - allowedIPs = peer-allowed-ips; - }; - -in { - options.fudo.vpn = with types; { - enable = mkEnableOption "Enable Fudo VPN"; - - network = mkOption { - type = str; - description = "Network range to assign this interface."; - default = "10.100.0.0/16"; - }; - - private-key-file = mkOption { - type = str; - description = "Path to the secret key (generated with wg [genkey/pubkey])."; - example = "/path/to/secret.key"; - }; - - listen-port = mkOption { - type = port; - description = "Port on which to listen for incoming connections."; - default = 51820; - }; - - dns-servers = mkOption { - type = listOf str; - description = "A list of dns servers to pass to clients."; - default = ["1.1.1.1" "8.8.8.8"]; - }; - - server-ip = mkOption { - type = str; - description = "IP of this WireGuard server."; - }; - - peers = mkOption { - type = attrsOf str; - description = "A map of peers to shared private keys."; - default = {}; - example = { - peer0 = "/path/to/priv.key"; - }; - }; - }; - - config = mkIf cfg.enable { - environment.etc = let - peer-data = imap1 (i: peer:{ - name = peer.name; - privkey-path = peer.privkey-path; - network-range = let - base = ip.intToIpv4 - ((ip.ipv4ToInt (ip.getNetworkBase cfg.network)) + (i * 256)); - in "${base}/24"; - }) (mapAttrsToList (name: privkey-path: { - name = name; - privkey-path = privkey-path; - }) cfg.peers); - - server-pubkey-pkg = generate-pubkey-pkg "server-pubkey" (fileContents cfg.private-key-file); - - server-pubkey = fileContents "${server-pubkey-pkg}/pubkey.key"; - - in listToAttrs - (map (peer: nameValuePair "wireguard/clients/${peer.name}.conf" { - mode = "0400"; - user = "root"; - group = "root"; - text = generate-client-config - peer.privkey-path - server-pubkey - peer.network-range - cfg.server-ip - cfg.listen-port - cfg.dns-servers; - }) peer-data); - - networking.wireguard = { - enable = true; - interfaces.wgtun0 = { - generatePrivateKeyFile = false; - ips = [ cfg.network ]; - listenPort = cfg.listen-port; - peers = mapAttrsToList - (name: private-key: generate-peer-entry name private-key ["0.0.0.0/0" "::/0"]) - cfg.peers; - privateKeyFile = cfg.private-key-file; - }; - }; - }; -} diff --git a/lib/fudo/webmail.nix b/lib/fudo/webmail.nix deleted file mode 100644 index 240efeb..0000000 --- a/lib/fudo/webmail.nix +++ /dev/null @@ -1,385 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - hostname = config.instance.hostname; - - cfg = config.fudo.webmail; - - webmail-user = cfg.user; - webmail-group = cfg.group; - - base-data-path = "/run/rainloop"; - - concatMapAttrs = f: attrs: - foldr (a: b: a // b) {} (mapAttrsToList f attrs); - - fastcgi-conf = builtins.toFile "fastcgi.conf" '' - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - fastcgi_param REQUEST_SCHEME $scheme; - fastcgi_param HTTPS $https if_not_empty; - - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; - - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - - # PHP only, required if PHP was built with --enable-force-cgi-redirect - fastcgi_param REDIRECT_STATUS 200; - ''; - - site-packages = mapAttrs (site: site-cfg: - pkgs.rainloop-community.overrideAttrs (oldAttrs: { - # Not sure how to correctly specify this arg... - #dataPath = "${base-data-path}/${site}"; - - # Overwriting, to correctly create data dir - installPhase = '' - mkdir $out - cp -r rainloop/* $out - rm -rf $out/data - ln -s ${base-data-path}/${site} $out/data - ln -s ${site-cfg.favicon} $out/favicon.ico - ''; - })) cfg.sites; - - siteOpts = { site-host, ... }: with types; { - options = { - title = mkOption { - type = str; - description = "Webmail site title"; - example = "My Webmail"; - }; - - debug = mkOption { - type = bool; - description = "Turn debug logs on."; - default = false; - }; - - mail-server = mkOption { - type = str; - description = "Mail server from which to send & recieve email."; - default = "mail.fudo.org"; - }; - - favicon = mkOption { - type = str; - description = "URL of the site favicon"; - example = "https://www.somepage.com/fav.ico"; - }; - - messages-per-page = mkOption { - type = int; - description = "Default number of messages to show per page"; - default = 30; - }; - - max-upload-size = mkOption { - type = int; - description = "Size limit in MB for uploaded files"; - default = 30; - }; - - theme = mkOption { - type = str; - description = "Default theme to use for this webmail site."; - default = "Default"; - }; - - domain = mkOption { - type = str; - description = "Domain for which the server acts as webmail server"; - }; - - edit-mode = mkOption { - type = enum [ "Plain" "Html" "PlainForced" "HtmlForced" ]; - description = "Default text editing mode for email"; - default = "Html"; - }; - - layout-mode = mkOption { - type = enum [ "side" "bottom" ]; - description = "Layout mode to use for email preview."; - default = "side"; - }; - - enable-threading = mkOption { - type = bool; - description = "Whether to enable threading for email."; - default = true; - }; - - enable-mobile = mkOption { - type = bool; - description = "Whether to enable a mobile site view."; - default = true; - }; - - database = mkOption { - type = nullOr (submodule databaseOpts); - description = "Database configuration for storing contact data."; - example = { - name = "my_db"; - host = "db.domain.com"; - user = "my_user"; - password-file = /path/to/some/file.pw; - }; - default = null; - }; - - admin-email = mkOption { - type = str; - description = "Email of administrator of this site."; - default = "admin@fudo.org"; - }; - }; - }; - - databaseOpts = { ... }: with types; { - options = { - type = mkOption { - type = enum [ "pgsql" "mysql" ]; - description = "Driver to use when connecting to the database."; - default = "pgsql"; - }; - - hostname = mkOption { - type = str; - description = "Name of host running the database."; - example = "my-db.domain.com"; - }; - - port = mkOption { - type = int; - description = "Port on which the database server is listening."; - default = 5432; - }; - - name = mkOption { - type = str; - description = - "Name of the database containing contact info. must have access."; - default = "rainloop_webmail"; - }; - - user = mkOption { - type = str; - description = "User as which to connect to the database."; - default = "webmail"; - }; - - password-file = mkOption { - type = nullOr str; - description = '' - Password to use when connecting to the database. - - If unset, a random password will be generated. - ''; - }; - }; - }; - -in { - options.fudo.webmail = with types; { - enable = mkEnableOption "Enable a RainLoop webmail server."; - - sites = mkOption { - type = attrsOf (submodule siteOpts); - description = "A map of webmail sites to site configurations."; - example = { - "webmail.domain.com" = { - title = "My Awesome Webmail"; - layout-mode = "side"; - favicon = "/path/to/favicon.ico"; - admin-password = "shh-don't-tell"; - }; - }; - }; - - user = mkOption { - type = str; - description = "User as which webmail will run."; - default = "webmail-php"; - }; - - group = mkOption { - type = str; - description = "Group as which webmail will run."; - default = "webmail-php"; - }; - }; - - config = mkIf cfg.enable { - users = { - users = { - ${webmail-user} = { - isSystemUser = true; - description = "Webmail PHP FPM user"; - group = webmail-group; - }; - }; - groups = { - ${webmail-group} = { - members = [ webmail-user config.services.nginx.user ]; - }; - }; - }; - - security.acme.certs = mapAttrs - (site: site-cfg: { email = site-cfg.admin-email; }) - cfg.sites; - - services = { - phpfpm = { - pools.webmail = { - settings = { - "pm" = "dynamic"; - "pm.max_children" = 50; - "pm.start_servers" = 5; - "pm.min_spare_servers" = 1; - "pm.max_spare_servers" = 8; - }; - - phpOptions = '' - memory_limit = 500M - ''; - - # Not working....see chmod below - user = webmail-user; - group = webmail-group; - }; - }; - - nginx = { - enable = true; - - virtualHosts = mapAttrs (site: site-cfg: { - enableACME = true; - forceSSL = true; - - root = "${site-packages.${site}}"; - - locations = { - "/" = { index = "index.php"; }; - - "/data" = { - extraConfig = '' - deny all; - return 403; - ''; - }; - }; - - extraConfig = '' - location ~ \.php$ { - expires -1; - - include ${fastcgi-conf}; - fastcgi_index index.php; - fastcgi_pass unix:${config.services.phpfpm.pools.webmail.socket}; - } - ''; - }) cfg.sites; - }; - }; - - fudo.secrets.host-secrets.${hostname} = concatMapAttrs - (site: site-cfg: let - - site-config-file = builtins.toFile "${site}-rainloop.cfg" - (import ./include/rainloop.nix lib site site-cfg site-packages.${site}.version); - - domain-config-file = builtins.toFile "${site}-domain.cfg" '' - imap_host = "${site-cfg.mail-server}" - imap_port = 143 - imap_secure = "TLS" - imap_short_login = On - sieve_use = Off - sieve_allow_raw = Off - sieve_host = "" - sieve_port = 4190 - sieve_secure = "None" - smtp_host = "${site-cfg.mail-server}" - smtp_port = 587 - smtp_secure = "TLS" - smtp_short_login = On - smtp_auth = On - smtp_php_mail = Off - white_list = "" - ''; - in { - "${site}-site-config" = { - source-file = site-config-file; - target-file = "/var/run/webmail/rainloop/site-${site}-rainloop.cfg"; - user = cfg.user; - }; - - "${site}-domain-config" = { - source-file = domain-config-file; - target-file = "/var/run/webmail/rainloop/domain-${site}-rainloop.cfg"; - user = cfg.user; - }; - }) cfg.sites; - - # TODO: make this a fudo service - systemd.services = { - webmail-init = let - link-configs = concatStringsSep "\n" (mapAttrsToList (site: site-cfg: - let - cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-site-config".target-file; - domain-cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-domain-config".target-file; - in '' - ${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/configs - ${pkgs.coreutils}/bin/cp ${cfg-file} ${base-data-path}/${site}/_data_/_default_/configs/application.ini - - ${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/domains/ - ${pkgs.coreutils}/bin/cp ${domain-cfg-file} ${base-data-path}/${site}/_data_/_default_/domains/${site-cfg.domain}.ini - '') cfg.sites); - scriptPkg = (pkgs.writeScriptBin "webmail-init.sh" '' - #!${pkgs.bash}/bin/bash -e - ${link-configs} - ${pkgs.coreutils}/bin/chown -R ${webmail-user}:${webmail-group} ${base-data-path} - ${pkgs.coreutils}/bin/chmod -R u+w ${base-data-path} - ''); - in { - requiredBy = [ "nginx.service" ]; - description = - "Initialize webmail service directories prior to starting nginx."; - script = "${scriptPkg}/bin/webmail-init.sh"; - }; - - phpfpm-webmail-socket-perm = { - wantedBy = [ "multi-user.target" ]; - description = - "Change ownership of the phpfpm socket for webmail once it's started."; - requires = [ "phpfpm-webmail.service" ]; - after = [ "phpfpm.target" ]; - serviceConfig = { - ExecStart = '' - ${pkgs.coreutils}/bin/chown ${webmail-user}:${webmail-group} ${config.services.phpfpm.pools.webmail.socket} - ''; - }; - }; - - nginx = { - requires = - [ "webmail-init.service" "phpfpm-webmail-socket-perm.service" ]; - }; - }; - }; -} diff --git a/lib/fudo/wireless-networks.nix b/lib/fudo/wireless-networks.nix deleted file mode 100644 index 62ada27..0000000 --- a/lib/fudo/wireless-networks.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - networkOpts = { network, ... }: { - options = { - network = mkOption { - type = types.str; - description = "Name of wireless network."; - default = network; - }; - - key = mkOption { - type = types.str; - description = "Secret key for wireless network."; - }; - }; - }; - -in { - options.fudo.wireless-networks = mkOption { - type = with types; attrsOf (submodule networkOpts); - description = "A map of wireless networks to attributes (including key)."; - default = { }; - }; - - config = { - networking.wireless.networks = - mapAttrs (network: networkOpts: { psk = networkOpts.key; }) - config.fudo.wireless-networks; - }; -} diff --git a/lib/informis/cl-gemini.nix b/lib/informis/cl-gemini.nix deleted file mode 100644 index 71c0a09..0000000 --- a/lib/informis/cl-gemini.nix +++ /dev/null @@ -1,177 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - cfg = config.informis.cl-gemini; - - feedOpts = { ... }: with types; { - options = { - url = mkOption { - type = str; - description = "Base URI of the feed, i.e. the URI corresponding to the feed path."; - example = "gemini://my.server/path/to/feedfiles"; - }; - - title = mkOption { - type = str; - description = "Title of given feed."; - example = "My Fancy Feed"; - }; - - path = mkOption { - type = str; - description = "Path to Gemini files making up the feed."; - example = "/path/to/feed"; - }; - }; - }; - - ensure-certificates = hostname: user: key: cert: pkgs.writeShellScript "ensure-gemini-certificates.sh" '' - if [[ ! -e ${key} ]]; then - TARGET_CERT_DIR=$(${pkgs.coreutils}/bin/dirname ${cert}) - TARGET_KEY_DIR=$(${pkgs.coreutils}/bin/dirname ${key}) - if [[ ! -d $TARGET_CERT_DIR ]]; then mkdir -p $TARGET_CERT_DIR; fi - if [[ ! -d $TARGET_KEY_DIR ]]; then mkdir -p $TARGET_KEY_DIR; fi - ${pkgs.openssl}/bin/openssl req -new -subj "/CN=.${hostname}" -addext "subjectAltName = DNS:${hostname}, DNS:.${hostname}" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -nodes -out ${cert} -keyout ${key} - ${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${cert} - ${pkgs.coreutils}/bin/chown -R ${user}:nogroup ${key} - ${pkgs.coreutils}/bin/chmod 0444 ${cert} - ${pkgs.coreutils}/bin/chmod 0400 ${key} - fi - ''; - - generate-feeds = feeds: - let - feed-strings = mapAttrsToList (feed-name: opts: - "(cl-gemini:register-feed :name \"${feed-name}\" :title \"${opts.title}\" :path \"${opts.path}\" :base-uri \"${opts.url}\")") feeds; - in pkgs.writeText "gemini-local-feeds.lisp" (concatStringsSep "\n" feed-strings); - -in { - options.informis.cl-gemini = with types; { - enable = mkEnableOption "Enable the cl-gemini server."; - - port = mkOption { - type = port; - description = "Port on which to serve Gemini traffic."; - default = 1965; - }; - - hostname = mkOption { - type = str; - description = "Hostname at which the server is available (for generating the SSL certificate)."; - example = "my.hostname.com"; - }; - - user = mkOption { - type = str; - description = "User as which to run the cl-gemini server."; - default = "cl-gemini"; - }; - - server-ip = mkOption { - type = str; - description = "IP on which to serve Gemini traffic."; - example = "1.2.3.4"; - }; - - document-root = mkOption { - type = str; - description = "Root at which to look for gemini files."; - example = "/my/gemini/root"; - }; - - user-public = mkOption { - type = str; - description = "Subdirectory of user homes to check for gemini files."; - default = "gemini-public"; - }; - - ssl-private-key = mkOption { - type = str; - description = "Path to the pem-encoded server private key."; - example = "/path/to/secret/key.pem"; - default = "${config.users.users.cl-gemini.home}/private/server-key.pem"; - }; - - ssl-certificate = mkOption { - type = str; - description = "Path to the pem-encoded server public certificate."; - example = "/path/to/cert.pem"; - default = "${config.users.users.cl-gemini.home}/private/server-cert.pem"; - }; - - slynk-port = mkOption { - type = nullOr port; - description = "Port on which to open a slynk server, if any."; - default = null; - }; - - feeds = mkOption { - type = attrsOf (submodule feedOpts); - description = "Feeds to generate and make available (as eg. /feed/name.xml)."; - example = { - diary = { - title = "My Diary"; - path = "/path/to/my/gemfiles/"; - url = "gemini://my.host/blog-path/"; - }; - }; - default = {}; - }; - - textfiles-archive = mkOption { - type = str; - description = "A path containing only gemini & text files."; - example = "/path/to/textfiles/"; - }; - }; - - config = mkIf cfg.enable { - - networking.firewall.allowedTCPPorts = [ cfg.port ]; - - users.users = { - ${cfg.user} = { - isSystemUser = true; - group = "nogroup"; - createHome = true; - home = "/var/lib/${cfg.user}"; - }; - }; - - systemd.services = { - cl-gemini = { - description = "cl-gemini Gemini server (https://gemini.circumlunar.space/)"; - - serviceConfig = { - ExecStartPre = "${ensure-certificates cfg.hostname cfg.user cfg.ssl-private-key cfg.ssl-certificate}"; - ExecStart = "${pkgs.cl-gemini}/bin/launch-server.sh"; - Restart = "on-failure"; - PIDFile = "/run/cl-gemini.$USERNAME.uid"; - User = cfg.user; - }; - - environment = { - GEMINI_SLYNK_PORT = mkIf (cfg.slynk-port != null) (toString cfg.slynk-port); - GEMINI_LISTEN_IP = cfg.server-ip; - GEMINI_PRIVATE_KEY = cfg.ssl-private-key; - GEMINI_CERTIFICATE = cfg.ssl-certificate; - GEMINI_LISTEN_PORT = toString cfg.port; - GEMINI_DOCUMENT_ROOT = cfg.document-root; - GEMINI_TEXTFILES_ROOT = cfg.textfiles-archive; - GEMINI_FEEDS = "${generate-feeds cfg.feeds}"; - - CL_SOURCE_REGISTRY = "${pkgs.lib.fudo.lisp.lisp-source-registry pkgs.cl-gemini}"; - }; - - path = with pkgs; [ - gcc - file - getent - ]; - - wantedBy = [ "multi-user.target" ]; - }; - }; - }; -} diff --git a/lib/instance.nix b/lib/instance.nix deleted file mode 100644 index c70652b..0000000 --- a/lib/instance.nix +++ /dev/null @@ -1,122 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - user = import ./types/user.nix { inherit lib; }; - host = import ./types/host.nix { inherit lib; }; - -in { - options.instance = with types; { - hostname = mkOption { - type = str; - 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."; - }; - - local-domain = mkOption { - type = str; - description = "Domain name of the current local host."; - }; - - local-profile = mkOption { - type = str; - description = "Profile name of the current local host."; - }; - - local-site = mkOption { - type = str; - description = "Site name of the current local host."; - }; - - local-admins = mkOption { - type = listOf str; - description = "List of users who should have admin access to the local host."; - }; - - local-groups = mkOption { - type = attrsOf (submodule user.groupOpts); - description = "List of groups which should be created on the local host."; - }; - - local-hosts = mkOption { - type = attrsOf (submodule host.hostOpts); - description = "List of hosts that should be considered local to the current host."; - }; - - local-users = mkOption { - type = attrsOf (submodule user.userOpts); - description = "List of users who should have access to the local host"; - }; - - local-networks = mkOption { - type = listOf str; - description = "Networks which are considered local to this host, site, or domain."; - }; - - build-seed = mkOption { - type = str; - description = "Seed used to generate configuration."; - }; - }; - - config = let - local-host = config.instance.hostname; - local-domain = config.fudo.hosts.${local-host}.domain; - local-site = config.fudo.hosts.${local-host}.site; - - host = config.fudo.hosts.${local-host}; - - host-user-list = host.local-users; - domain-user-list = config.fudo.domains."${local-domain}".local-users; - site-user-list = config.fudo.sites."${local-site}".local-users; - local-users = - getAttrs (host-user-list ++ domain-user-list ++ site-user-list) config.fudo.users; - - host-admin-list = host.local-admins; - domain-admin-list = config.fudo.domains."${local-domain}".local-admins; - site-admin-list = config.fudo.sites."${local-site}".local-admins; - local-admins = host-admin-list ++ domain-admin-list ++ site-admin-list; - - host-group-list = host.local-groups; - domain-group-list = config.fudo.domains."${local-domain}".local-groups; - site-group-list = config.fudo.sites."${local-site}".local-groups; - local-groups = - getAttrs (host-group-list ++ domain-group-list ++ site-group-list) - config.fudo.groups; - - local-hosts = - filterAttrs (host: hostOpts: hostOpts.site == local-site) config.fudo.hosts; - - local-networks = - host.local-networks ++ - config.fudo.domains.${local-domain}.local-networks ++ - config.fudo.sites.${local-site}.local-networks; - - local-profile = host.profile; - - host-fqdn = "${config.instance.hostname}.${local-domain}"; - - in { - instance = { - inherit - host-fqdn - local-domain - local-site - local-users - local-admins - local-groups - local-hosts - local-profile - local-networks; - }; - }; -} diff --git a/lib/ip.nix b/lib/ip.nix deleted file mode 100644 index 33d477a..0000000 --- a/lib/ip.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ pkgs, ... }: - -with pkgs.lib; -let - pow = x: e: if (e == 0) then 1 else x * (pow x (e - 1)); - - generateNBits = n: - let - helper = n: c: - if (c == n) then pow 2 c else (pow 2 c) + (helper n (c + 1)); - in if (n <= 0) then - throw "Can't generate 0 or fewer bits" - else - helper (n - 1) 0; - - rightPadBits = int: bits: bitOr int (generateNBits bits); - - reverseIpv4 = ip: concatStringsSep "." (reverseList (splitString "." ip)); - - intToBinaryList = int: - let - helper = int: cur: - let curExp = pow 2 cur; - in if (curExp > int) then - [ ] - else - [ (if ((bitAnd curExp int) > 0) then 1 else 0) ] - ++ (helper int (cur + 1)); - in reverseList (helper int 0); - - leftShift = int: n: int * (pow 2 n); - - rightShift = int: n: int / (pow 2 n); - -in rec { - - ipv4ToInt = ip: - let els = map toInt (reverseList (splitString "." ip)); - in foldr (a: b: a + b) 0 (imap0 (i: el: (leftShift el (i * 8))) els); - - intToIpv4 = int: - concatStringsSep "." - (map (i: toString (bitAnd (rightShift int (i * 8)) 255)) [ 3 2 1 0 ]); - - maskFromV32Network = network: - let - fullMask = ipv4ToInt "255.255.255.255"; - insignificantBits = 32 - (getNetworkMask network); - in intToIpv4 - (leftShift (rightShift fullMask insignificantBits) insignificantBits); - - networkMinIp = network: intToIpv4 (1 + (ipv4ToInt (getNetworkBase network))); - - networkMaxIp = network: - intToIpv4 (rightPadBits (ipv4ToInt (getNetworkBase network)) - (32 - (getNetworkMask network))); - - # To avoid broadcast IP... - networkMaxButOneIp = network: - intToIpv4 ((rightPadBits (ipv4ToInt (getNetworkBase network)) - (32 - (getNetworkMask network))) - 1); - - ipv4OnNetwork = ip: network: - let - ip-int = ipv4ToInt ip; - net-min = networkMinIp network; - net-max = networkMaxIp network; - in (ip-int >= networkMinIp) && (ip-int <= networkMaxIp); - - getNetworkMask = network: toInt (elemAt (splitString "/" network) 1); - - getNetworkBase = network: - let - ip = elemAt (splitString "/" network) 0; - insignificantBits = 32 - (getNetworkMask network); - in intToIpv4 - (leftShift (rightShift (ipv4ToInt ip) insignificantBits) insignificantBits); -} diff --git a/lib/lisp.nix b/lib/lisp.nix deleted file mode 100644 index 1eea537..0000000 --- a/lib/lisp.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ pkgs, ... }: - -with pkgs.lib; -rec { - gather-dependencies = pkg: unique (pkg.propagatedBuildInputs ++ (concatMap gather-dependencies pkg.propagatedBuildInputs)); - - lisp-source-registry = pkg: concatStringsSep ":" (map (p: "${p}//") (gather-dependencies pkg)); -} diff --git a/lib/network.nix b/lib/network.nix deleted file mode 100644 index 944b722..0000000 --- a/lib/network.nix +++ /dev/null @@ -1,53 +0,0 @@ -{ pkgs, ... }: - -with pkgs.lib; -let - generate-mac-address = hostname: interface: pkgs.stdenv.mkDerivation { - name = "mk-mac-${hostname}-${interface}"; - phases = [ "installPhase" ]; - installPhase = '' - echo ${hostname}-${interface} | sha1sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/' > $out - ''; - }; - - # dropUntil = pred: lst: let - # drop-until-helper = pred: lst: - # if (length lst) == 0 then [] else - # if (pred (head lst)) then lst else (drop-until-helper pred (tail lst)); - # in drop-until-helper pred lst; - - # dropWhile = pred: dropUntil (el: !(pred el)); - - # is-whitespace = str: (builtins.match "^[[:space:]]*$" str) != null; - - # stripWhitespace = str: let - # lines = builtins.split "\n" str; - # lines-front-stripped = dropWhile is-whitespace lines; - # lines-rear-stripped = lib.reverseList - # (dropWhile is-whitespace - # (lib.reverseList lines-front-stripped)); - # in concatStringsSep "\n" lines-rear-stripped; - - host-ipv4 = config: hostname: let - domain = config.fudo.hosts.${hostname}.domain; - host-network = config.fudo.networks.${domain}; - in host-network.hosts.${hostname}.ipv4-address; - - host-ipv6 = config: hostname: let - domain = config.fudo.hosts.${hostname}.domain; - host-network = config.fudo.networks.${domain}; - in host-network.hosts.${hostname}.ipv6-address; - - host-ips = config: hostname: let - ipv4 = host-ipv4 config hostname; - ipv6 = host-ipv6 config hostname; - not-null = o: o != null; - in filter not-null [ ipv4 ipv6 ]; - -in { - inherit host-ipv4 host-ipv6 host-ips; - - generate-mac-address = hostname: interface: let - pkg = generate-mac-address hostname interface; - in removeSuffix "\n" (builtins.readFile "${pkg}"); -} diff --git a/lib/overlay.nix b/lib/overlay.nix deleted file mode 100644 index 1646cf3..0000000 --- a/lib/overlay.nix +++ /dev/null @@ -1,13 +0,0 @@ -(final: prev: { - lib = prev.lib // { - fudo = let - lib = prev.lib; - in { - ip = import ./ip.nix { pkgs = prev; }; - dns = import ./dns.nix { pkgs = prev; }; - passwd = import ./passwd.nix { pkgs = prev; }; - lisp = import ./lisp.nix { pkgs = prev; }; - network = import ./network.nix { pkgs = prev; }; - }; - }; -}) diff --git a/lib/passwd.nix b/lib/passwd.nix deleted file mode 100644 index 97c2088..0000000 --- a/lib/passwd.nix +++ /dev/null @@ -1,54 +0,0 @@ -{ pkgs, ... }: - -with pkgs.lib; -let - hash-ldap-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation { - name = "${name}-ldap-passwd"; - - phases = [ "installPhase" ]; - - buildInputs = with pkgs; [ openldap ]; - - installPhase = '' - slappasswd -T ${passwd-file} > $out - ''; - }; - - hash-ldap-passwd = name: passwd-file: - builtins.readFile "${hash-ldap-passwd-pkg name passwd-file}"; - - generate-random-passwd = name: length: pkgs.stdenv.mkDerivation { - name = "${name}-random-passwd"; - - phases = [ "installPhase" ]; - - buildInputs = with pkgs; [ pwgen ]; - - installPhase = '' - pwgen --secure --num-passwords=1 ${toString length} > $out - ''; - }; - - generate-stablerandom-passwd = name: { seed, length ? 20, ... }: - pkgs.stdenv.mkDerivation { - name = "${name}-stablerandom-passwd"; - - phases = [ "installPhase" ]; - - buildInputs = with pkgs; [ pwgen ]; - - installPhase = '' - echo "${name}-${seed}" > seedfile - pwgen --secure --num-passwords=1 -H seedfile ${toString length} > $out - ''; - }; - -in { - hash-ldap-passwd = hash-ldap-passwd; - - random-passwd-file = name: length: - builtins.toPath "${generate-random-passwd name length}"; - - stablerandom-passwd-file = name: seed: - builtins.toPath "${generate-stablerandom-passwd name { seed = seed; }}"; -} diff --git a/lib/system.nix b/lib/system.nix deleted file mode 100644 index bfb502e..0000000 --- a/lib/system.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ lib, ... }: - -with lib; -let - head-or-null = lst: if (lst == []) then null else head lst; - is-regular-file = filename: type: type == "regular" || type == "link"; - regular-files = path: filterAttrs is-regular-file (builtins.readDir path); - matches-ext = ext: filename: type: (builtins.match ".+[.]${ext}$" filename) != null; - is-nix-file = matches-ext "nix"; - strip-ext = ext: filename: head-or-null (builtins.match "(.+)[.]${ext}$" filename); - get-ext = filename: head-or-null (builtins.match "^.+[.](.+)$" filename); - hostname-from-file = filename: strip-ext "nix"; - nix-files = path: - attrNames - (filterAttrs is-nix-file - (filterAttrs is-regular-file - (builtins.readDir path))); - - basename-to-file = path: let - files = nix-files path; - in listToAttrs - (map (file: - nameValuePair (strip-ext "nix" file) - (path + "/${file}")) - files); - - basename-import = path: - mapAttrs (attr: attr-file: import attr-file) - (basename-to-file path); - -in { - - hosts = host-path: basename-import host-path; - - networks = network-path: basename-import network-path; -} diff --git a/lib/types/host.nix b/lib/types/host.nix deleted file mode 100644 index c01d0a4..0000000 --- a/lib/types/host.nix +++ /dev/null @@ -1,305 +0,0 @@ -{ lib, ... }: - -with lib; -let - passwd = import ../passwd.nix { inherit lib; }; - -in rec { - encryptedFSOpts = { ... }: let - mountpoint = { mp, ... }: { - options = with types; { - mountpoint = mkOption { - type = str; - description = "Path at which to mount the filesystem."; - default = mp; - }; - - options = mkOption { - type = listOf str; - description = "List of filesystem options specific to this mountpoint (eg: subvol)."; - }; - - group = mkOption { - type = nullOr str; - description = "Group to which the mountpoint should belong."; - default = null; - }; - - users = mkOption { - type = listOf str; - description = '' - List of users who should have access to the filesystem. - - Requires a group to be set. - ''; - default = [ ]; - }; - - world-readable = mkOption { - type = bool; - description = "Whether to leave the top level world-readable."; - default = true; - }; - }; - }; - in { - options = with types; { - encrypted-device = mkOption { - type = str; - description = "Path to the encrypted device."; - }; - - key-path = mkOption { - type = str; - description = '' - Path at which to locate the key file. - - The filesystem will be decrypted and mounted once available."; - ''; - }; - - filesystem-type = mkOption { - type = str; - description = "Filesystem type of the decrypted filesystem."; - }; - - options = mkOption { - type = listOf str; - description = "List of filesystem options with which to mount."; - }; - - mountpoints = mkOption { - type = attrsOf (submodule mountpoint); - description = "A map of mountpoints for this filesystem to fs options. Multiple to support btrfs."; - default = {}; - }; - }; - }; - - masterKeyOpts = { ... }: { - options = with types; { - key-path = mkOption { - type = str; - description = "Path of the host master key file, used to decrypt secrets."; - }; - - public-key = mkOption { - type = str; - description = "Public key used during deployment to decrypt secrets for the host."; - }; - }; - }; - - hostOpts = { name, ... }: let - hostname = name; - in { - options = with types; { - master-key = mkOption { - type = nullOr (submodule masterKeyOpts); - description = "Public key for the host master key, used by the host to decrypt secrets."; - }; - - domain = mkOption { - type = str; - description = - "Primary domain to which the host belongs, in the form of a domain name."; - default = "fudo.org"; - }; - - extra-domains = mkOption { - type = listOf str; - description = "Extra domain in which this host is reachable."; - default = [ ]; - }; - - aliases = mkOption { - type = listOf str; - description = - "Host aliases used by the current host. Note this will be multiplied with extra-domains."; - default = [ ]; - }; - - site = mkOption { - type = str; - description = "Site at which the host is located."; - default = "unsited"; - }; - - local-networks = mkOption { - type = listOf str; - description = - "A list of networks to be considered trusted by this host."; - default = [ "127.0.0.0/8" ]; - }; - - profile = mkOption { - type = listOf (enum ["desktop" "server" "laptop"]); - description = - "The profile to be applied to the host, determining what software is included."; - }; - - admin-email = mkOption { - type = nullOr str; - description = "Email for the administrator of this host."; - default = null; - }; - - local-users = mkOption { - type = listOf str; - description = - "List of users who should have local (i.e. login) access to the host."; - default = [ ]; - }; - - description = mkOption { - type = str; - description = "Description of this host."; - default = "Another Fudo Host."; - }; - - local-admins = mkOption { - type = listOf str; - description = - "A list of users who should have admin access to this host."; - default = [ ]; - }; - - local-groups = mkOption { - type = listOf str; - description = "List of groups which should exist on this host."; - default = [ ]; - }; - - ssh-fingerprints = mkOption { - type = listOf str; - description = '' - A list of DNS SSHFP records for this host. Get with `ssh-keygen -r ` - ''; - default = [ ]; - }; - - rp = mkOption { - type = nullOr str; - description = "Responsible person."; - default = null; - }; - - tmp-on-tmpfs = mkOption { - type = bool; - description = - "Use tmpfs for /tmp. Great if you've got enough (>16G) RAM."; - default = true; - }; - - enable-gui = mkEnableOption "Install desktop GUI software."; - - docker-server = mkEnableOption "Enable Docker on the current host."; - - kerberos-services = mkOption { - type = listOf str; - description = - "List of services which should exist for this host, if it belongs to a realm."; - default = [ "ssh" "host" ]; - }; - - ssh-pubkeys = mkOption { - type = listOf path; - description = - "SSH key files of the host."; - default = []; - }; - - build-pubkeys = mkOption { - type = listOf str; - description = "SSH public keys used to access the build server."; - default = [ ]; - }; - - external-interfaces = mkOption { - type = listOf str; - description = "A list of interfaces on which to enable the firewall."; - default = [ ]; - }; - - keytab-secret-file = mkOption { - type = nullOr str; - description = "Keytab from which to create a keytab secret."; - default = null; - }; - - keep-cool = mkOption { - type = bool; - description = "A host that tends to overheat. Try to keep it cooler."; - default = false; - }; - - nixos-system = mkOption { - type = bool; - description = "Whether the host is a NixOS system."; - default = true; - }; - - arch = mkOption { - type = str; - description = "System architecture of the system."; - }; - - machine-id = mkOption { - type = nullOr str; - description = "Machine id of the system. See: man machine-id."; - default = null; - }; - - android-dev = mkEnableOption "Enable ADB on the host."; - - encrypted-filesystems = mkOption { - type = attrsOf (submodule encryptedFSOpts); - description = "List of encrypted filesystems to mount on the local host when the key is available."; - default = { }; - }; - - initrd-network = let - keypair-type = { ... }: { - options = { - public-key = mkOption { - type = str; - description = "SSH public key."; - }; - - private-key-file = mkOption { - type = str; - description = "Path to SSH private key (on the local host!)."; - }; - }; - }; - - initrd-network-config = { ... }: { - options = { - ip = mkOption { - type = str; - description = "IP to assign to the initrd image, allowing access to host during bootup."; - }; - keypair = mkOption { - type = (submodule keypair-type); - description = "SSH host key pair to use for initrd."; - }; - interface = mkOption { - type = str; - description = "Name of interface on which to listen for connections."; - }; - }; - }; - - in mkOption { - type = nullOr (submodule initrd-network-config); - description = "Configuration parameters to set up initrd SSH network."; - default = null; - }; - - backplane-password-file = mkOption { - options = path; - description = "File containing the password used by this host to connect to the backplane."; - }; - }; - }; -} diff --git a/lib/types/network-definition.nix b/lib/types/network-definition.nix deleted file mode 100644 index e3b9599..0000000 --- a/lib/types/network-definition.nix +++ /dev/null @@ -1,108 +0,0 @@ -{ lib, ... }: - -with lib; -let - srvRecordOpts = { ... }: { - options = with types; { - priority = mkOption { - type = int; - description = "Priority to give to this record."; - default = 0; - }; - - weight = mkOption { - type = int; - description = - "Weight to give this record, among records of equivalent priority."; - default = 5; - }; - - port = mkOption { - type = port; - description = "Port for service on this host."; - example = 88; - }; - - host = mkOption { - type = str; - description = "Host providing service."; - example = "my-host.my-domain.com"; - }; - }; - }; - - networkHostOpts = import ./network-host.nix { inherit lib; }; - -in { - options = with types; { - hosts = mkOption { - type = attrsOf (submodule networkHostOpts); - description = "Hosts on the local network, with relevant settings."; - example = { - my-host = { - ipv4-address = "192.168.0.1"; - mac-address = "aa:aa:aa:aa:aa"; - }; - }; - default = { }; - }; - - srv-records = mkOption { - type = attrsOf (attrsOf (listOf (submodule srvRecordOpts))); - description = "SRV records for the network."; - example = { - tcp = { - kerberos = { - port = 88; - host = "krb-host.my-domain.com"; - }; - }; - }; - default = { }; - }; - - aliases = mkOption { - type = attrsOf str; - default = { }; - description = - "A mapping of host-alias -> hostnames to add to the domain record."; - example = { - mail = "my-mail-host"; - music = "musicall-host.other-domain.com."; - }; - }; - - verbatim-dns-records = mkOption { - type = listOf str; - description = "Records to be inserted verbatim into the DNS zone."; - example = [ "some-host IN CNAME base-host" ]; - default = [ ]; - }; - - dmarc-report-address = mkOption { - type = nullOr str; - description = "The email to use to recieve DMARC reports, if any."; - example = "admin-user@domain.com"; - default = null; - }; - - default-host = mkOption { - type = nullOr str; - description = - "IP of the host which will act as the default server for this domain, if any."; - default = null; - }; - - mx = mkOption { - type = listOf str; - description = "A list of mail servers serving this domain."; - default = [ ]; - }; - - gssapi-realm = mkOption { - type = nullOr str; - description = "Kerberos GSSAPI realm of the network."; - default = null; - }; - }; -} diff --git a/lib/types/network-host.nix b/lib/types/network-host.nix deleted file mode 100644 index ce7518f..0000000 --- a/lib/types/network-host.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ lib, ... }: - -{ hostname, ... }: -with lib; -{ - options = with types; { - ipv4-address = mkOption { - type = nullOr str; - description = "The V4 IP of a given host, if any."; - default = null; - }; - - ipv6-address = mkOption { - type = nullOr str; - description = "The V6 IP of a given host, if any."; - default = null; - }; - - mac-address = mkOption { - type = nullOr types.str; - description = - "The MAC address of a given host, if desired for IP reservation."; - default = null; - }; - - description = mkOption { - type = nullOr str; - description = "Description of the host."; - default = null; - }; - }; -} diff --git a/lib/types/user.nix b/lib/types/user.nix deleted file mode 100644 index e344e7b..0000000 --- a/lib/types/user.nix +++ /dev/null @@ -1,157 +0,0 @@ -{ lib, ... }: - -with lib; -rec { - systemUserOpts = { name, ... }: { - options = with lib.types; { - username = mkOption { - type = str; - description = "The system user's login name."; - default = name; - }; - - description = mkOption { - type = str; - description = "Description of this system user's purpose or role"; - }; - - ldap-hashed-password = mkOption { - type = str; - description = - "LDAP-formatted hashed password for this user. Generate with slappasswd."; - }; - }; - }; - - userOpts = { name, ... }: let - username = name; - in { - options = with lib.types; { - username = mkOption { - type = str; - description = "The user's login name."; - default = username; - }; - - uid = mkOption { - type = int; - description = "Unique UID number for the user."; - }; - - common-name = mkOption { - type = str; - description = "The user's common or given name."; - }; - - primary-group = mkOption { - type = str; - description = "Primary group to which the user belongs."; - }; - - login-shell = mkOption { - type = nullOr shellPackage; - description = "The user's preferred shell."; - }; - - description = mkOption { - type = str; - default = "Fudo Member"; - description = "A description of this user's role."; - }; - - ldap-hashed-passwd = mkOption { - type = nullOr str; - description = - "LDAP-formatted hashed password, used for email and other services. Use slappasswd to generate the properly-formatted password."; - default = null; - }; - - login-hashed-passwd = mkOption { - type = nullOr str; - description = - "Hashed password for shell, used for shell access to hosts. Use mkpasswd to generate the properly-formatted password."; - default = null; - }; - - ssh-authorized-keys = mkOption { - type = listOf str; - description = "SSH public keys this user can use to log in."; - default = [ ]; - }; - - home-directory = mkOption { - type = nullOr str; - description = "Default home directory for the given user."; - default = null; - }; - - k5login = mkOption { - type = listOf str; - description = "List of Kerberos principals that map to this user."; - default = [ ]; - }; - - ssh-keys = mkOption { - type = listOf (submodule sshKeyOpts); - description = "Path to the user's public and private key files."; - default = []; - }; - - email = mkOption { - type = nullOr str; - description = "User's primary email address."; - default = null; - }; - - email-aliases = mkOption { - type = listOf str; - description = "Email aliases that should map to this user."; - default = []; - }; - }; - }; - - groupOpts = { name, ... }: { - options = with lib.types; { - group-name = mkOption { - description = "Group name."; - default = name; - }; - - description = mkOption { - type = str; - description = "Description of the group or it's purpose."; - }; - - members = mkOption { - type = listOf str; - default = [ ]; - description = "A list of users who are members of the current group."; - }; - - gid = mkOption { - type = int; - description = "GID number of the group."; - }; - }; - }; - - sshKeyOpts = { ... }: { - options = with lib.types; { - private-key = mkOption { - type = str; - description = "Path to the user's private key."; - }; - - public-key = mkOption { - type = str; - description = "Path to the user's public key."; - }; - - key-type = mkOption { - type = enum [ "rsa" "ecdsa" "ed25519" ]; - description = "Type of the user's public key."; - }; - }; - }; -}