Files
cnx-network-clan/modules/web-proxy.nix
T
2026-09-17 08:57:32 +07:00

77 lines
3.0 KiB
Nix

# 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 {
dir = config.clan.core.settings.directory;
inherit 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.<name>).
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
];
}