# Public reverse proxy with TLS termination for web01. Caddy fronts internal # services and forwards to them over the ZeroTier mesh, never the public net. # The cert is a single wildcard (*.cnx.network) obtained via ACME DNS-01, so # adding a vhost needs no new issuance. Public ports: 443 for the proxy and 80 # only for Caddy's HTTP->HTTPS redirect (issuance never uses inbound HTTP). { config, lib, pkgs, ... }: let mesh = import ./mesh-hosts.nix { inherit config lib; }; hosts = import ./hosts.nix; certName = "cnx.network"; in { imports = [ ./dns/acme-web01-secret.nix ]; # Render the shared acme_web01 TSIG secret into a lego rfc2136 env file. lego # (via security.acme below) uses it to write _acme-challenge.cnx.network TXT # records on ns1, which authorizes the acme_web01 key for exactly that owner. clan.core.vars.generators.dns-acme-web01-rfc2136 = { files."rfc2136.env".secret = true; # root-owned; systemd reads it as root dependencies = [ "dns-acme-web01-secret" ]; script = '' printf 'RFC2136_NAMESERVER=${hosts.ns1.ipv4}:53\nRFC2136_TSIG_ALGORITHM=hmac-sha256.\nRFC2136_TSIG_KEY=acme_web01\nRFC2136_TSIG_SECRET=%s\n' \ "$(cat "$in"/dns-acme-web01-secret/secret)" > "$out"/rfc2136.env ''; }; security.acme = { acceptTerms = true; defaults.email = "postmaster@cnx.email"; # One wildcard cert for every vhost this proxy serves, via DNS-01 (so issuance # never depends on inbound HTTP). Port 80 is open only for Caddy's # HTTP->HTTPS redirect, not for ACME. certs.${certName} = { domain = "*.cnx.network"; extraDomainNames = [ "cnx.network" ]; dnsProvider = "rfc2136"; environmentFile = config.clan.core.vars.generators.dns-acme-web01-rfc2136.files."rfc2136.env".path; # ns1 is the only nameserver that accepts the acme_web01 UPDATE; check # propagation against it directly rather than a public resolver. dnsResolver = "${hosts.ns1.ipv4}:53"; # Caddy reads the cert from explicit file paths (tls directive below), so it # won't notice a renewal on its own — reload it whenever the cert changes. reloadServices = [ "caddy.service" ]; }; }; # The lego-issued cert is owned group=acme; Caddy needs to read the key. users.users.caddy.extraGroups = [ "acme" ]; # Reverse proxy. The explicit `tls cert key` points Caddy at the wildcard cert # and disables its automatic ACME, so no extra issuance happens. Backends are # dialed over the mesh by their ZeroTier address (mesh.hosts.). services.caddy = { enable = true; virtualHosts."grafana.cnx.network".extraConfig = '' tls /var/lib/acme/${certName}/cert.pem /var/lib/acme/${certName}/key.pem reverse_proxy http://[${mesh.hosts.control}]:3000 ''; }; # 443 serves the proxy; 80 only carries Caddy's automatic HTTP->HTTPS redirect # (the Hetzner cloud firewall also scopes these in # modules/hetzner-firewall-rules.nix). Admin still rides the mesh. networking.firewall.allowedTCPPorts = [ 80 443 ]; }