Add web01 public reverse proxy with DNS-01 wildcard TLS

web01 terminates TLS for grafana.cnx.network and proxies to Grafana on
control over the mesh. Caddy serves a *.cnx.network wildcard cert obtained
via ACME DNS-01, using a dedicated acme_web01 TSIG key scoped on ns1 to
_acme-challenge on the cnx.network zone only. Ports 80/443 are the only
public exposure (80 just redirects); admin and the backend ride ZeroTier.

Also reload Caddy on cert renewal for both web01 and mx1, since both
reference the cert via explicit tls file paths and would otherwise keep
serving a stale cert after a silent renewal.
This commit is contained in:
Berwn
2026-06-21 03:05:54 +07:00
parent 86a2928825
commit 48bf7fb250
10 changed files with 182 additions and 4 deletions
+33 -1
View File
@@ -13,6 +13,7 @@ in
imports = [
../../modules/dns/authoritative.nix
../../modules/dns/acme-mx1-secret.nix
../../modules/dns/acme-web01-secret.nix
../../modules/static-ipv6.nix
../../modules/monitoring/exporters.nix
];
@@ -74,9 +75,28 @@ in
'';
};
# ACME DNS-01, dedicated web01 key. A *separate* TSIG key (acme_web01) that only
# web01 holds, rendered from the shared secret (generator dns-acme-web01-secret,
# imported above). acl_acme_web01 scopes it to TXT updates at _acme-challenge on
# the cnx.network zone — the owner the wildcard *.cnx.network challenge uses — so
# this credential can write nothing but web01's own cert challenges.
clan.core.vars.generators.dns-acme-web01-knot = {
files."acme.conf" = {
secret = true;
owner = "knot";
group = "knot";
};
dependencies = [ "dns-acme-web01-secret" ];
script = ''
printf 'key:\n - id: acme_web01\n algorithm: hmac-sha256\n secret: %s\n' \
"$(cat "$in"/dns-acme-web01-secret/secret)" > "$out"/acme.conf
'';
};
services.knot.keyFiles = [
config.clan.core.vars.generators.dns-acme-tsig.files."acme.conf".path
config.clan.core.vars.generators.dns-acme-mx1-knot.files."acme.conf".path
config.clan.core.vars.generators.dns-acme-web01-knot.files."acme.conf".path
];
services.knot.settings.acl = [
@@ -102,6 +122,17 @@ in
"_acme-challenge.mail"
];
}
{
id = "acl_acme_web01";
key = "acme_web01";
action = [ "update" ];
"update-type" = [ "TXT" ];
"update-owner" = "name";
"update-owner-match" = "sub-or-equal";
# Wildcard *.cnx.network places its challenge at _acme-challenge.cnx.network,
# i.e. _acme-challenge at the cnx.network apex (where this acl is attached).
"update-owner-name" = [ "_acme-challenge" ];
}
];
# Automatic DNSSEC signing policy (primary only). ECDSA P-256/SHA-256 with
@@ -136,6 +167,7 @@ in
"acl_ns2"
"acl_acme"
]
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ];
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ]
++ lib.optionals (d == "cnx.network") [ "acl_acme_web01" ];
}) domains;
}
+16 -2
View File
@@ -1,7 +1,21 @@
{ config, ... }:
let
hosts = import ../../modules/hosts.nix;
in
{
imports = [
../../modules/static-ipv6.nix
../../modules/monitoring/exporters.nix
../../modules/web-proxy.nix
];
# New machine!
clan.core.sops.defaultGroups = [ "admins" ];
# Public IPv6 (from modules/hosts.nix); SLAAC doesn't bring it up here.
cnx.staticIPv6 = {
enable = true;
address = hosts.${config.networking.hostName}.ipv6;
};
services.timesyncd.enable = true;
}
+1 -1
View File
@@ -1,7 +1,7 @@
# ---
# schema = "single-disk"
# [placeholders]
# mainDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_108706511"
# mainDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_108706511"
# ---
# This file was automatically generated!
# CHANGING this configuration requires wiping and reinstalling the machine