{ 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/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): a dedicated TSIG key, scoped to ns1 only, that an # external ACME client uses to write _acme-challenge TXT records. acl_acme # (referenced by each zone below) limits the key to TXT updates at or under # _acme-challenge.; Knot then signs the record and transfers it to ns2, # which never needs this key. Retrieve the secret for the client 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 ''; }; services.knot.keyFiles = [ config.clan.core.vars.generators.dns-acme-tsig.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" ]; } ]; # 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" ]; acl = [ "acl_ns2" "acl_acme" ]; # ns2 transfers; acme_ddns key does DNS-01 updates }) domains; }