48bf7fb250
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.
174 lines
6.2 KiB
Nix
174 lines
6.2 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
domains = import ../../modules/dns/domains.nix;
|
|
mesh = import ../../modules/mesh-hosts.nix { inherit config lib; };
|
|
hosts = import ../../modules/hosts.nix;
|
|
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
|
|
];
|
|
|
|
clan.core.sops.defaultGroups = [ "admins" ];
|
|
|
|
# Knot's state dir holds the non-regenerable DNSSEC key material (KSK/ZSK
|
|
# private keys in the KASP keystore). Declaring it as clan state makes the
|
|
# borgbackup client back it up; losing it forces an emergency DS rollover at
|
|
# the registrar. mode 0700 owned by knot, but borg runs as root so it reads it.
|
|
clan.core.state.knot.folders = [ "/var/lib/knot" ];
|
|
|
|
# The borgbackup repo is addressed as `borg@control`; mesh peers have no name
|
|
# resolution, so map the control machine name to its ZeroTier mesh address.
|
|
networking.hosts.${mesh.hosts.control} = [ "control" ];
|
|
|
|
# Public IPv6 (from modules/hosts.nix; matches the ns1 AAAA glue); SLAAC
|
|
# doesn't bring it up here.
|
|
cnx.staticIPv6 = {
|
|
enable = true;
|
|
address = hosts.${config.networking.hostName}.ipv6;
|
|
};
|
|
|
|
time.timeZone = "Etc/GMT-1"; # UTC+1 (fixed offset, no DST)
|
|
services.timesyncd.enable = true;
|
|
|
|
# ACME DNS-01 (RFC 2136), general key. A dedicated TSIG key scoped by acl_acme
|
|
# (referenced by every zone below) to TXT updates at or under _acme-challenge.
|
|
# Retrieve the client config with:
|
|
# clan vars get ns1 dns-acme-tsig/acme.conf
|
|
clan.core.vars.generators.dns-acme-tsig = {
|
|
files."acme.conf" = {
|
|
secret = true;
|
|
owner = "knot";
|
|
group = "knot";
|
|
};
|
|
runtimeInputs = [ pkgs.knot-dns ];
|
|
script = ''
|
|
keymgr -t acme_ddns hmac-sha256 > "$out"/acme.conf
|
|
'';
|
|
};
|
|
|
|
# ACME DNS-01, dedicated mx1 key. A *separate* TSIG key (acme_mx1) that only
|
|
# mx1 holds, rendered from the shared secret (generator dns-acme-mx1-secret,
|
|
# imported above). acl_acme_mx1 scopes it to TXT updates at exactly
|
|
# _acme-challenge.{mx1,mta-sts,mail} (the mail cert and its MTA-STS + client-
|
|
# alias SANs), and it is attached only to the cnx.email zone below — so this
|
|
# credential can write nothing but mx1's own cert challenges.
|
|
clan.core.vars.generators.dns-acme-mx1-knot = {
|
|
files."acme.conf" = {
|
|
secret = true;
|
|
owner = "knot";
|
|
group = "knot";
|
|
};
|
|
dependencies = [ "dns-acme-mx1-secret" ];
|
|
script = ''
|
|
printf 'key:\n - id: acme_mx1\n algorithm: hmac-sha256\n secret: %s\n' \
|
|
"$(cat "$in"/dns-acme-mx1-secret/secret)" > "$out"/acme.conf
|
|
'';
|
|
};
|
|
|
|
# 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 = [
|
|
{
|
|
id = "acl_acme";
|
|
key = "acme_ddns";
|
|
action = [ "update" ];
|
|
"update-type" = [ "TXT" ];
|
|
"update-owner" = "name";
|
|
"update-owner-match" = "sub-or-equal";
|
|
"update-owner-name" = [ "_acme-challenge" ];
|
|
}
|
|
{
|
|
id = "acl_acme_mx1";
|
|
key = "acme_mx1";
|
|
action = [ "update" ];
|
|
"update-type" = [ "TXT" ];
|
|
"update-owner" = "name";
|
|
"update-owner-match" = "sub-or-equal";
|
|
"update-owner-name" = [
|
|
"_acme-challenge.mx1"
|
|
"_acme-challenge.mta-sts"
|
|
"_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
|
|
# Knot's default key management: the ZSK auto-rolls and the KSK is kept stable,
|
|
# so the DS at the registrar only changes on a manual KSK rollover.
|
|
services.knot.settings.policy = [
|
|
{
|
|
id = "cnx";
|
|
algorithm = "ecdsap256sha256";
|
|
}
|
|
];
|
|
|
|
# ns1 = primary (master): loads each zone from its file and serves it to ns2.
|
|
# zonefile-load = difference-no-serial lets us edit records without touching the
|
|
# SOA serial; Knot diffs the file, assigns a unixtime serial, signs the zone,
|
|
# then notifies ns2 and lets it pull the signed zone via AXFR/IXFR. unixtime is
|
|
# strictly monotonic per reload, so two zone versions can never share a serial
|
|
# (the failure mode dateserial's 2-digit daily counter allowed after a journal reset).
|
|
services.knot.settings.zone = map (d: {
|
|
domain = d;
|
|
file = ../../modules/dns/zones + "/${d}.zone";
|
|
"zonefile-load" = "difference-no-serial";
|
|
"zonefile-sync" = "-1";
|
|
"journal-content" = "all"; # required by difference-no-serial; holds the live signed zone
|
|
"serial-policy" = "unixtime";
|
|
"dnssec-signing" = true;
|
|
"dnssec-policy" = "cnx";
|
|
notify = [ "ns2" ];
|
|
# ns2 transfers; acme_ddns does general DNS-01 updates. The dedicated
|
|
# acme_mx1 key is attached only to cnx.email, so it can't touch other zones.
|
|
acl = [
|
|
"acl_ns2"
|
|
"acl_acme"
|
|
]
|
|
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ]
|
|
++ lib.optionals (d == "cnx.network") [ "acl_acme_web01" ];
|
|
}) domains;
|
|
}
|