diff --git a/lib.nix b/lib.nix index 63bb5e5..1fc1f30 100644 --- a/lib.nix +++ b/lib.nix @@ -1,10 +1,11 @@ { pkgs, ... }: { - ip = import ./lib/lib/ip.nix { inherit pkgs; }; dns = import ./lib/lib/dns.nix { inherit pkgs; }; - passwd = import ./lib/lib/passwd.nix { inherit pkgs; }; + fs = import ./lib/lib/filesystem.nix { inherit pkgs; }; + ip = import ./lib/lib/ip.nix { inherit pkgs; }; lisp = import ./lib/lib/lisp.nix { inherit pkgs; }; network = import ./lib/lib/network.nix { inherit pkgs; }; - fs = import ./lib/lib/filesystem.nix { inherit pkgs; }; + passwd = import ./lib/lib/passwd.nix { inherit pkgs; }; + text = import ./lib/lib/text.nix { inherit pkgs; }; } diff --git a/lib/fudo/acme-certs.nix b/lib/fudo/acme-certs.nix index 2480e71..3bc0c0a 100644 --- a/lib/fudo/acme-certs.nix +++ b/lib/fudo/acme-certs.nix @@ -118,25 +118,48 @@ in { # Sigh. Leave it the same as nginx default, so it works whether or not # nginx feels like helping or not. default = "/var/lib/acme/acme-challenge"; + # default = "/run/acme-challenge"; }; }; config = { security.acme.certs = mapAttrs (domain: domainOpts: { - email = domainOpts.admin-email; - webroot = cfg.challenge-path; - extraDomainNames = domainOpts.extra-domains; + # email = domainOpts.admin-email; + # webroot = cfg.challenge-path; + # group = "nginx"; + # 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 = let + server-path = "/.well-known/acme-challenge"; + in (mapAttrs (domain: domainOpts: { + # THIS IS A HACK. Getting redundant paths. So if {domain} is configured + # somewhere else, assume ACME is already set. + # locations.${server-path} = mkIf (! (hasAttr domain config.services.nginx.virtualHosts)) { + # root = cfg.challenge-path; + # extraConfig = "auth_basic off;"; + # }; + enableACME = true; + forceSSL = true; + serverAliases = domainOpts.extra-domains; + }) localDomains) // { + "default" = { + serverName = "_"; + default = true; + locations = { + ${server-path} = { + root = cfg.challenge-path; + extraConfig = "auth_basic off;"; + }; + "/".return = "403 Forbidden"; + }; + }; + }; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; @@ -170,7 +193,17 @@ in { if (copyOpts.group != null) then "${copyOpts.user}:${copyOpts.group}" else copyOpts.user; + dirs = unique [ + (dirOf copyOpts.certificate) + (dirOf copyOpts.full-certificate) + (dirOf copyOpts.chain) + (dirOf copyOpts.private-key) + ]; install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" '' + ${concatStringsSep "\n" (map (dir: '' + mkdir -p ${dir} + chown ${owners} ${dir} + '') dirs)} cp ${source}/cert.pem ${copyOpts.certificate} chmod 0444 ${copyOpts.certificate} chown ${owners} ${copyOpts.certificate} diff --git a/lib/fudo/client/dns.nix b/lib/fudo/client/dns.nix index d9751d3..68adf81 100644 --- a/lib/fudo/client/dns.nix +++ b/lib/fudo/client/dns.nix @@ -32,7 +32,6 @@ in { domain = mkOption { type = types.str; description = "Domain under which this host is registered."; - default = "fudo.link"; }; server = mkOption { diff --git a/lib/fudo/default.nix b/lib/fudo/default.nix index 3472697..3517122 100644 --- a/lib/fudo/default.nix +++ b/lib/fudo/default.nix @@ -1,48 +1,52 @@ { config, lib, pkgs, ... }: with lib; { - imports = [ - ./acme-certs.nix - ./authentication.nix - ./backplane - ./chat.nix - ./client/dns.nix - ./deploy.nix - ./distributed-builds.nix - ./dns.nix - ./domains.nix - ./garbage-collector.nix - ./git.nix - ./global.nix - ./grafana.nix - ./hosts.nix - ./host-filesystems.nix - ./initrd-network.nix - ./ipfs.nix - ./jabber.nix - ./kdc.nix - ./ldap.nix - ./local-network.nix - ./mail.nix - ./mail-container.nix - ./minecraft-server.nix - ./netinfo-email.nix - ./node-exporter.nix - ./nsd.nix - ./password.nix - ./postgres.nix - ./prometheus.nix - ./secrets.nix - ./secure-dns-proxy.nix - ./sites.nix - ./slynk.nix - ./ssh.nix - ./system.nix - ./system-networking.nix - ./users.nix - ./vpn.nix - ./webmail.nix - ./wireless-networks.nix - ./zones.nix - ]; + imports = [ + ./backplane-service/dns.nix + + ./acme-certs.nix + ./adguard-dns-proxy.nix + ./authentication.nix + ./backplane.nix + ./chat.nix + ./client/dns.nix + ./deploy.nix + ./distributed-builds.nix + ./dns.nix + ./domains.nix + ./garbage-collector.nix + ./git.nix + ./global.nix + ./grafana.nix + ./hosts.nix + ./host-filesystems.nix + ./initrd-network.nix + ./ipfs.nix + ./jabber.nix + ./kdc.nix + ./ldap.nix + ./local-network.nix + ./mail.nix + ./mail-container.nix + ./minecraft-server.nix + ./netinfo-email.nix + ./node-exporter.nix + ./nsd.nix + ./password.nix + ./postgres.nix + ./powerdns.nix + ./prometheus.nix + ./secrets.nix + ./secure-dns-proxy.nix + ./sites.nix + ./slynk.nix + ./ssh.nix + ./system.nix + ./system-networking.nix + ./users.nix + ./vpn.nix + ./webmail.nix + ./wireless-networks.nix + ./zones.nix + ]; } diff --git a/lib/fudo/domains.nix b/lib/fudo/domains.nix index 83f714e..b351d81 100644 --- a/lib/fudo/domains.nix +++ b/lib/fudo/domains.nix @@ -48,12 +48,54 @@ let default = "admin@${domain}"; }; - gssapi-realm = mkOption { + grafana-hosts = mkOption { + type = listOf str; + description = "List of hosts acting as Grafana metric analyzers. Requires prometheus hosts as well."; + default = []; + }; + + log-aggregator = mkOption { type = nullOr str; - description = "GSSAPI (i.e. Kerberos) realm of this domain."; + description = "Host which will accept incoming log pushes."; default = null; }; + postgresql-server = mkOption { + type = nullOr str; + description = "Hostname acting as the local PostgreSQL server."; + default = null; + }; + + backplane = mkOption { + type = nullOr (submodule { + options = { + nameserver = mkOption { + type = nullOr str; + description = "Host acting as backplane dynamic DNS server."; + default = null; + }; + + dns-service = mkOption { + type = nullOr str; + description = "DNS backplane service host."; + default = null; + }; + + domain = mkOption { + type = str; + description = "Domain name of the dynamic zone served by this server."; + }; + }; + }); + description = "Backplane configuration."; + default = null; + }; + + gssapi-realm = mkOption { + type = str; + description = "GSSAPI (i.e. Kerberos) realm of this domain."; + }; + kerberos-master = mkOption { type = nullOr str; description = "Hostname of the Kerberos master server for the domain, if applicable."; @@ -72,6 +114,12 @@ let default = []; }; + prometheus-hosts = mkOption { + type = listOf str; + description = "List of hosts acting aas prometheus metric scrapers for hosts in this network."; + default = []; + }; + primary-nameserver = mkOption { type = nullOr str; description = "Hostname of the primary nameserver for this domain."; diff --git a/lib/fudo/grafana.nix b/lib/fudo/grafana.nix index aa88f83..d4763d2 100644 --- a/lib/fudo/grafana.nix +++ b/lib/fudo/grafana.nix @@ -4,14 +4,14 @@ with lib; let - cfg = config.fudo.grafana; + cfg = config.fudo.metrics.grafana; hostname = config.instance.hostname; domain-name = config.fudo.hosts.${hostname}.domain; in { - options.fudo.grafana = with types; { + options.fudo.metrics.grafana = with types; { enable = mkEnableOption "Fudo Metrics Display Service"; hostname = mkOption { @@ -88,72 +88,75 @@ in { description = "Directory at which to store Grafana state data."; default = "/var/lib/grafana"; }; + + private-network = mkEnableOption "Network is private, no SSL."; }; config = mkIf cfg.enable { - services.nginx = { - enable = true; + systemd.tmpfiles.rules = let + grafana-user = config.systemd.services.grafana.serviceConfig.User; + in [ + "d ${cfg.state-directory} 0700 ${grafana-user} - - -" + ]; - virtualHosts = { - "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; + services = { + nginx = { + enable = true; + recommendedOptimisation = true; + recommendedProxySettings = 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; - ''; + virtualHosts = { + "${cfg.hostname}" = { + enableACME = ! cfg.private-network; + forceSSL = ! cfg.private-network; + locations."/".proxyPass = "http://127.0.0.1:3000"; }; }; }; - }; - services.grafana = { - enable = true; - - addr = "127.0.0.1"; - protocol = "http"; - port = 3000; - domain = cfg.hostname; - rootUrl = "https://${cfg.hostname}/"; - dataDir = cfg.state-directory; - - security = { - adminPasswordFile = cfg.admin-password-file; - secretKeyFile = cfg.secret-key-file; - }; - - smtp = { + grafana = { 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"; - }; + addr = "127.0.0.1"; + protocol = "http"; + port = 3000; + domain = cfg.hostname; + rootUrl = let + scheme = if cfg.private-network then "http" else "https"; + in "${scheme}://${cfg.hostname}/"; + dataDir = cfg.state-directory; - provision.datasources = [ - { + security = { + adminPasswordFile = cfg.admin-password-file; + secretKeyFile = cfg.secret-key-file; + }; + + smtp = { + enable = true; + fromAddress = "metrics@fudo.org"; + host = "${cfg.smtp.hostname}: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 = imap0 (i: host: { editable = false; - isDefault = true; - name = cfg.prometheus-host; + isDefault = (i == 0); + name = builtins.trace "PROMETHEUS-HOST: ${host}" host; type = "prometheus"; - url = "https://${cfg.prometheus-host}/"; - } - ]; + url = let + scheme = if private-network then "http" else "https"; + in "${scheme}://${host}/"; + }) cfg.prometheus-hosts; + }; }; }; } diff --git a/lib/fudo/hosts.nix b/lib/fudo/hosts.nix index a9bb724..866e807 100644 --- a/lib/fudo/hosts.nix +++ b/lib/fudo/hosts.nix @@ -82,7 +82,7 @@ in { packages = map (p: "${p.name}") config.environment.systemPackages; sorted-unique = sort lessThan (unique packages); - in concatStringsSep "\n" sorted-unique; + in "${concatStringsSep "\n" sorted-unique}\n"; }; systemPackages = with pkgs; diff --git a/lib/fudo/initrd-network.nix b/lib/fudo/initrd-network.nix index f492505..f3347d2 100644 --- a/lib/fudo/initrd-network.nix +++ b/lib/fudo/initrd-network.nix @@ -32,8 +32,9 @@ in { config = { boot = mkIf (initrd-cfg != null) { kernelParams = let - site = config.fudo.sites.${config.instance.local-site}; - site-gateway = site.gateway-v4; + site-name = config.instance.local-site; + site = config.fudo.sites.${site-name}; + site-gateway = pkgs.lib.network.site-gateway config site-name; netmask = pkgs.lib.ip.maskFromV32Network site.network; in [ diff --git a/lib/fudo/kdc.nix b/lib/fudo/kdc.nix index 95c1e39..eb34925 100644 --- a/lib/fudo/kdc.nix +++ b/lib/fudo/kdc.nix @@ -337,11 +337,15 @@ in { users = { users.${cfg.user} = { isSystemUser = true; + uid = 88; home = state-directory; group = cfg.group; }; - groups.${cfg.group} = { members = [ cfg.user ]; }; + groups.${cfg.group} = { + gid = 88; + members = [ cfg.user ]; + }; }; krb5 = { diff --git a/lib/fudo/ldap.nix b/lib/fudo/ldap.nix index 5a93339..91c65ca 100644 --- a/lib/fudo/ldap.nix +++ b/lib/fudo/ldap.nix @@ -218,6 +218,15 @@ in { config = mkIf cfg.enable { + users = { + users.openldap = { + uid = 389; + }; + groups.openldap = { + gid = 389; + }; + }; + environment = { etc = { "openldap/sasl2/slapd.conf" = mkIf (cfg.kerberos-keytab != null) { diff --git a/lib/fudo/local-network.nix b/lib/fudo/local-network.nix index 7ebca2c..99a6dbe 100644 --- a/lib/fudo/local-network.nix +++ b/lib/fudo/local-network.nix @@ -31,7 +31,13 @@ in { dns-listen-ips = mkOption { type = listOf str; - description = "A list of IPs on which to server DNS queries."; + description = "A list of IPv4 addresses on which to server DNS queries."; + }; + + dns-listen-ipv6s = mkOption { + type = listOf str; + description = "A list of IPv6 addresses on which to server DNS queries."; + default = [ ]; }; gateway = mkOption { @@ -61,10 +67,22 @@ in { default = false; }; - recursive-resolver = mkOption { - type = str; - description = "DNS nameserver to use for recursive resolution."; - default = "1.1.1.1 port 53"; + # recursive-resolver = mkOption { + # type = str; + # description = "DNS nameserver to use for recursive resolution."; + # default = "1.1.1.1 port 53"; + # }; + + recursive-resolver = { + host = mkOption { + type = str; + description = "DNS server host or (preferably) IP."; + }; + port = mkOption { + type = port; + description = "Remote host port for DNS queries."; + default = 53; + }; }; search-domains = mkOption { @@ -161,18 +179,22 @@ in { ''; }; - ipToBlock = ip: + filterRedundantIps = official-hosts: hosts: let + host-by-ip = groupBy (hostOpts: hostOpts.ipv4-address) hosts; + in filter (hostOpts: + if (length (getAttr hostOpts.ipv4-address host-by-ip) == 1) then + true + else elem hostOpts.hostname official-hosts) hosts; + ipTo24Block = ip: concatStringsSep "." (reverseList (take 3 (splitString "." ip))); - compactHosts = - mapAttrsToList (host: data: data // { host = host; }) zone.hosts; - hostsByBlock = - groupBy (host-data: ipToBlock host-data.ipv4-address) compactHosts; + hostsByBlock = official-hosts: + groupBy (host-data: ipTo24Block host-data.ipv4-address) + (filterRedundantIps official-hosts (attrValues zone.hosts)); hostPtrRecord = host-data: - "${ - last (splitString "." host-data.ipv4-address) - } IN PTR ${host-data.host}.${cfg.domain}."; + "${last (splitString "." host-data.ipv4-address)} IN PTR ${host-data.hostname}.${cfg.domain}."; - blockZones = mapAttrsToList blockHostsToZone hostsByBlock; + blockZones = official-hosts: + mapAttrsToList blockHostsToZone (hostsByBlock official-hosts); hostARecord = host: data: "${host} IN A ${data.ipv4-address}"; hostSshFpRecords = host: data: @@ -189,13 +211,21 @@ in { known-hosts = config.fudo.hosts; + domain-name = config.instance.local-domain; + + domain-hosts = + attrNames + (filterAttrs (_: hostOpts: + hostOpts.domain == domain-name) + config.fudo.hosts); + in { enable = true; cacheNetworks = [ cfg.network "localhost" "localnets" ]; - forwarders = [ cfg.recursive-resolver ]; + forwarders = [ "${cfg.recursive-resolver.host} port ${toString cfg.recursive-resolver.port}" ]; listenOn = cfg.dns-listen-ips; + listenOnIpv6 = cfg.dns-listen-ipv6s; extraOptions = concatStringsSep "\n" [ - "dnssec-enable yes;" "dnssec-validation yes;" "auth-nxdomain no;" "recursion yes;" @@ -204,36 +234,44 @@ in { 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) + file = let + zone-data = pkgs.lib.dns.zoneToZonefile + config.instance.build-timestamp + cfg.domain + zone; + in pkgs.writeText "zone-${cfg.domain}" zone-data; + # 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 + # $TTL 1h - @ IN NS ns1.${cfg.domain}. + # @ IN NS ns1.${cfg.domain}. - $ORIGIN ${cfg.domain}. + # $ORIGIN ${cfg.domain}. - $TTL 30m + # $TTL 30m - ${optionalString (zone.gssapi-realm != null) - ''_kerberos IN TXT "${zone.gssapi-realm}"''} + # ${optionalString (zone.gssapi-realm != null) + # ''_kerberos IN TXT "${zone.gssapi-realm}"''} - ${join-lines - (imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}") - cfg.dns-servers)} - ${join-lines (mapAttrsToList hostARecord zone.hosts)} - ${join-lines (mapAttrsToList hostSshFpRecords zone.hosts)} - ${join-lines (mapAttrsToList cnameRecord zone.aliases)} - ${join-lines zone.verbatim-dns-records} - ${pkgs.lib.dns.srvRecordsToBindZone zone.srv-records} - ${join-lines cfg.extra-records} - ''; - }] ++ blockZones; + # ${join-lines + # (imap1 (i: server-ip: "ns${toString i} IN A ${server-ip}") + # cfg.dns-servers)} + # ${join-lines (mapAttrsToList hostARecord zone.hosts)} + # ${join-lines (mapAttrsToList hostSshFpRecords zone.hosts)} + # ${join-lines (mapAttrsToList cnameRecord zone.aliases)} + # ${join-lines zone.verbatim-dns-records} + # ${pkgs.lib.dns.srvRecordsToBindZone zone.srv-records} + # ${join-lines cfg.extra-records} + # ''; + }] ++ (optionals + cfg.enable-reverse-mappings + (blockZones domain-hosts)); }; }; } diff --git a/lib/fudo/node-exporter.nix b/lib/fudo/node-exporter.nix index b2a6e20..cf6a95d 100644 --- a/lib/fudo/node-exporter.nix +++ b/lib/fudo/node-exporter.nix @@ -4,14 +4,14 @@ with lib; let inherit (lib.strings) concatStringsSep; - cfg = config.fudo.node-exporter; + cfg = config.fudo.metrics.node-exporter; allow-network = network: "allow ${network};"; local-networks = config.instance.local-networks; in { - options.fudo.node-exporter = with types; { + options.fudo.metrics.node-exporter = with types; { enable = mkEnableOption "Enable a Prometheus node exporter with some reasonable settings."; hostname = mkOption { @@ -24,6 +24,8 @@ in { description = "User as which to run the node exporter job."; default = "node-exporter"; }; + + private-network = mkEnableOption "Network is private."; }; config = mkIf cfg.enable { @@ -45,10 +47,12 @@ in { # list of trusted networks, with SSL protection. nginx = { enable = true; + recommendedOptimisation = true; + recommendedProxySettings = true; virtualHosts."${cfg.hostname}" = { - enableACME = true; - forceSSL = true; + enableACME = ! cfg.private-network; + forceSSL = ! cfg.private-network; locations."/metrics/node" = { extraConfig = '' @@ -57,7 +61,6 @@ in { deny all; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; ''; proxyPass = "http://127.0.0.1:9100/metrics"; diff --git a/lib/fudo/postgres.nix b/lib/fudo/postgres.nix index 9aedfbd..cad745f 100644 --- a/lib/fudo/postgres.nix +++ b/lib/fudo/postgres.nix @@ -105,17 +105,22 @@ let nameValuePair "DATABASE ${database}" databaseOpts.access) databases; makeEntry = nw: - "host all all ${nw} gss include_realm=0 krb_realm=${gssapi-realm}"; + "hostssl 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)); + makeLocalUserPasswordEntries = users: networks: let + network-entries = user: db: + join-lines + (map (network: "hostssl ${db} ${user} ${network} md5") + networks); + in 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 + ${network-entries user db} + '') (attrNames opts.databases))) (filterPasswordedUsers users)); userTableAccessSql = user: entity: access: "GRANT ${access} ON ${entity} TO ${user};"; @@ -134,18 +139,21 @@ in { enable = mkEnableOption "Fudo PostgreSQL Server"; ssl-private-key = mkOption { - type = str; + type = nullOr str; description = "Location of the server SSL private key."; + default = null; }; ssl-certificate = mkOption { - type = str; + type = nullOr str; description = "Location of the server SSL certificate."; + default = null; }; keytab = mkOption { - type = str; + type = nullOr str; description = "Location of the server Kerberos keytab."; + default = null; }; local-networks = mkOption { @@ -224,32 +232,9 @@ in { config = mkIf cfg.enable { - environment = { - systemPackages = with pkgs; [ postgresql_11_gssapi ]; + networking.firewall.allowedTCPPorts = [ 5432 ]; - # 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; - # }; - # }; - }; + environment.systemPackages = with pkgs; [ postgresql_11_gssapi ]; users.groups = { ${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; }; @@ -269,12 +254,14 @@ in { ensurePermissions = { "DATABASE ${database}" = "ALL PRIVILEGES"; }; }) opts.users)) cfg.databases))); - settings = { - krb_server_keyfile = cfg.keytab; + settings = let + ssl-enabled = cfg.ssl-certificate != null; + in { + krb_server_keyfile = mkIf (cfg.keytab != null) cfg.keytab; - ssl = true; - ssl_cert_file = cfg.ssl-certificate; - ssl_key_file = cfg.ssl-private-key; + ssl = ssl-enabled; + ssl_cert_file = mkIf ssl-enabled cfg.ssl-certificate; + ssl_key_file = mkIf ssl-enabled cfg.ssl-private-key; unix_socket_directories = cfg.socket-directory; unix_socket_group = cfg.socket-group; @@ -282,12 +269,12 @@ in { }; authentication = lib.mkForce '' - ${makeLocalUserPasswordEntries cfg.users} + ${makeLocalUserPasswordEntries cfg.users cfg.local-networks} 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 127.0.0.1/16 gss include_realm=0 krb_realm=${gssapi-realm} host all all ::1/128 gss include_realm=0 krb_realm=${gssapi-realm} # local networks @@ -308,6 +295,22 @@ in { wantedBy = [ "multi-user.target" ]; }; + paths = let + user-password-files = mapAttrsToList + (user: userOpts: userOpts.password-file) + cfg.users; + in { + postgresql-password-watcher = mkIf (length user-password-files > 0) { + wantedBy = [ "default.target" ]; + description = + "Reset all user passwords if any changes occur."; + pathConfig = { + PathChanged = user-password-files; + Unit = "postgresql-password-setter.service"; + }; + }; + }; + services = { postgresql-password-setter = let passwords-script = passwords-setter-script cfg.users; @@ -350,22 +353,42 @@ in { partOf = [ cfg.systemd-target ]; wants = [ "postgresql-password-setter.service" ]; - postStart = let + # 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; + }; + + postgresql-finalizer = { + requires = [ "postgresql.target" ]; + after = [ "postgresql.target" "postgresql-password-setter.target" ]; + partOf = [ cfg.systemd-target ]; + script = 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 '' + ${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 index e74a343..d67fca7 100644 --- a/lib/fudo/prometheus.nix +++ b/lib/fudo/prometheus.nix @@ -2,12 +2,11 @@ with lib; let - inherit (lib.strings) concatStringsSep; - cfg = config.fudo.prometheus; + cfg = config.fudo.metrics.prometheus; in { - options.fudo.prometheus = with types; { + options.fudo.metrics.prometheus = with types; { enable = mkEnableOption "Fudo Prometheus Data-Gathering Server"; service-discovery-dns = mkOption { @@ -78,32 +77,33 @@ in { description = "Directory at which to store Prometheus state."; default = "/var/lib/prometheus"; }; + + private-network = mkEnableOption "Network is private."; }; config = mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d ${cfg.state-directory} 0700 ${config.systemd.services.prometheus.serviceConfig.User} - - -" + ]; + services.nginx = { enable = true; + recommendedOptimisation = true; + recommendedProxySettings = true; virtualHosts = { "${cfg.hostname}" = { - enableACME = true; - forceSSL = true; + enableACME = ! cfg.private-network; + forceSSL = ! cfg.private-network; 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;"} - ''; + in "${optionalString ((length local-networks) > 0) + (concatStringsSep "\n" + (map (network: "allow ${network};") local-networks)) + "\ndeny all;"}"; }; }; }; @@ -118,85 +118,18 @@ in { listenAddress = "127.0.0.1"; port = 9090; - scrapeConfigs = [ - { - job_name = "docker"; + scrapeConfigs = let + make-job = type: { + job_name = type; 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; - } - ]; - } - ]; + scheme = if cfg.private-network then "http" else "https"; + metrics_path = "/metrics/${type}"; + dns_sd_configs = if (hasAttr type cfg.service-discovery-dns) then + [ { names = cfg.service-discovery-dns.${type}; } ] else []; + static_configs = if (hasAttr type cfg.static-targets) then + [ { targets = cfg.static-targets.${type}; } ] else []; + }; + in map make-job ["docker" "node" "dovecot" "postfix" "rspamd"]; pushgateway = { enable = if (cfg.push-url != null) then true else false; diff --git a/lib/fudo/secrets.nix b/lib/fudo/secrets.nix index 4f83f26..2382693 100644 --- a/lib/fudo/secrets.nix +++ b/lib/fudo/secrets.nix @@ -66,6 +66,7 @@ let user = mkOption { type = str; description = "User (on target host) to which the file will belong."; + default = "root"; }; group = mkOption { diff --git a/lib/fudo/sites.nix b/lib/fudo/sites.nix index 435c983..5bf4638 100644 --- a/lib/fudo/sites.nix +++ b/lib/fudo/sites.nix @@ -133,6 +133,12 @@ let type = str; description = "Hostname of the mail server to use for this site."; }; + + local-gateway = mkOption { + type = nullOr str; + description = "If this is a NAT site, this should point to the host acting as network gateway."; + default = null; + }; }; }; diff --git a/lib/fudo/system.nix b/lib/fudo/system.nix index e4a5b61..e1a037e 100644 --- a/lib/fudo/system.nix +++ b/lib/fudo/system.nix @@ -166,7 +166,7 @@ let "Level of protection to apply to the system for this service."; }; addressFamilies = mkOption { - type = listOf (enum address-families); + type = nullOr (listOf (enum address-families)); default = [ ]; description = "List of address families which the service can use."; }; @@ -435,7 +435,8 @@ in { WorkingDirectory = mkIf (opts.workingDirectory != null) opts.workingDirectory; RestrictAddressFamilies = - restrict-address-families opts.addressFamilies; + optionals (opts.addressFamilies != null) + (restrict-address-families opts.addressFamilies); RestrictNamespaces = opts.restrictNamespaces; User = mkIf (opts.user != null) opts.user; Group = mkIf (opts.group != null) opts.group; diff --git a/lib/informis/cl-gemini.nix b/lib/informis/cl-gemini.nix index 648ccf8..a18d9d6 100644 --- a/lib/informis/cl-gemini.nix +++ b/lib/informis/cl-gemini.nix @@ -161,7 +161,7 @@ in { GEMINI_TEXTFILES_ROOT = cfg.textfiles-archive; GEMINI_FEEDS = "${generate-feeds cfg.feeds}"; - CL_SOURCE_REGISTRY = "${pkgs.lib.lisp.lisp-source-registry pkgs.cl-gemini}"; + CL_SOURCE_REGISTRY = pkgs.lib.lisp.lisp-source-registry pkgs.cl-gemini; }; path = with pkgs; [ diff --git a/lib/instance.nix b/lib/instance.nix index 7249050..56bc1c1 100644 --- a/lib/instance.nix +++ b/lib/instance.nix @@ -62,9 +62,21 @@ in { description = "Networks which are considered local to this host, site, or domain."; }; + service-home = mkOption { + type = str; + description = "Path to runtime home directories for services."; + default = "/run/service"; + }; + build-seed = mkOption { type = str; description = "Seed used to generate configuration."; }; }; + + config = { + systemd.tmpfiles.rules = [ + "d ${config.instance.service-home} 755 root root - -" + ]; + }; } diff --git a/lib/lib/dns.nix b/lib/lib/dns.nix index aa2789e..0864831 100644 --- a/lib/lib/dns.nix +++ b/lib/lib/dns.nix @@ -67,7 +67,7 @@ let toString record.priority } ${ toString record.weight - } ${record.host}.") records); + } ${toString record.port} ${record.host}.") records); srvRecordOpts = with types; { options = { diff --git a/lib/lib/network.nix b/lib/lib/network.nix index 57572c2..960eae3 100644 --- a/lib/lib/network.nix +++ b/lib/lib/network.nix @@ -44,8 +44,14 @@ let not-null = o: o != null; in filter not-null [ ipv4 ipv6 ]; + site-gateway = config: site-name: let + site = config.fudo.sites.${site-name}; + in if (site.local-gateway != null) + then host-ipv4 config site.local-gateway + else site.gateway-v4; + in { - inherit host-ipv4 host-ipv6 host-ips; + inherit host-ipv4 host-ipv6 host-ips site-gateway; generate-mac-address = hostname: interface: let pkg = generate-mac-address hostname interface; diff --git a/lib/lib/passwd.nix b/lib/lib/passwd.nix index 28d58d4..b38e786 100644 --- a/lib/lib/passwd.nix +++ b/lib/lib/passwd.nix @@ -12,12 +12,12 @@ let installPhase = let passwd = removeSuffix "\n" (readFile passwd-file); in '' - slappasswd -s ${passwd} > $out + slappasswd -s ${passwd} | tr -d '\n' > $out ''; }; hash-ldap-passwd = name: passwd-file: - builtins.readFile "${hash-ldap-passwd-pkg name passwd-file}"; + readFile "${hash-ldap-passwd-pkg name passwd-file}"; generate-random-passwd = name: length: pkgs.stdenv.mkDerivation { name = "${name}-random-passwd"; @@ -27,10 +27,28 @@ let buildInputs = with pkgs; [ pwgen ]; installPhase = '' - pwgen --secure --num-passwords=1 ${toString length} > $out + pwgen --secure --num-passwords=1 ${toString length} | tr -d '\n' > $out ''; }; + bcrypt-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation { + name = "${name}-bcrypt"; + + phases = [ "installPhase" ]; + + buildInputs = with pkgs; [ apacheHttpd ]; + + installPhase = let + passwd = removeSuffix "\n" (readFile passwd-file); + in '' + htpasswd -bnBC 10 "" ${passwd} | tr -d ':\n' | sed 's/$2y/$2a/' > $out + ''; + }; + + bcrypt-passwd = name: passwd-file: + readFile "${bcrypt-passwd-pkg name passwd-file}"; + + generate-stablerandom-passwd = name: { seed, length ? 20, ... }: pkgs.stdenv.mkDerivation { name = "${name}-stablerandom-passwd"; @@ -41,13 +59,15 @@ let installPhase = '' echo "${name}-${seed}" > seedfile - pwgen --secure --num-passwords=1 -H seedfile ${toString length} > $out + pwgen --secure --num-passwords=1 -H seedfile ${toString length} | tr -d '\n' > $out ''; }; in { hash-ldap-passwd = hash-ldap-passwd; + bcrypt-passwd = bcrypt-passwd; + random-passwd-file = name: length: builtins.toPath "${generate-random-passwd name length}"; diff --git a/lib/types/network-host.nix b/lib/types/network-host.nix index 87339ac..aad81f4 100644 --- a/lib/types/network-host.nix +++ b/lib/types/network-host.nix @@ -1,8 +1,13 @@ { lib, ... }: with lib; -{ +{ name, ... }: { options = with types; { + hostname = mkOption { + type = str; + description = "Hostname."; + default = name; + }; ipv4-address = mkOption { type = nullOr str; description = "The V4 IP of a given host, if any.";