{
  config,
  secrets,
  ...
}: {
  # Knot DNS is a high-performance authoritative-only DNS server which supports
  # all key features of the modern domain name system.
  # https://www.knot-dns.cz/
  # https://knot.readthedocs.io/en/master/
  services.knot = {
    enable = true;
    keyFiles = [
      config.age.secrets.acme-knot-key-file.path
    ];
    settings = {
      server = {
        listen = [
          "116.203.179.206@53"
          "2a01:4f8:c2c:71c0::@53"
        ];
      };
      policy = [
        {
          id = "default";
          # Disallow zone enumeration by using NSEC3 instead of NSEC
          # https://dnsinstitute.com/documentation/dnssec-guide/ch06s02.html
          nsec3 = "on";
        }
      ];
      acl = [
        {
          # Allow zone updates using the 'acme' TSIG key
          # https://knot.readthedocs.io/en/master/configuration.html#restricting-dynamic-updates
          id = "acme";
          key = "acme";
          action = "update";
          # Dynamic updates are restricted to TXT records matching the given
          # list of domain names. The list is considered relative to the zone
          # name unless it is a FQDN (i.e. ends in a dot).
          update-type = ["TXT"];
          update-owner = "name";
          update-owner-match = "equal";
          update-owner-name = [
            "_acme-challenge"
          ];
        }
      ];
      template = [
        {
          id = "default";
          # Enable extra zone semantic error checks
          semantic-checks = "on";
          # Enable ACME ACL on all zones
          acl = ["acme"];
          # Enable automatic DNSSEC signing on all zones. The KSK must be
          # configured in the parent zone. Use the following command to get the
          # required record(s):
          # > nix shell nixpkgs#knot-dns -c sudo keymgr caspervk.net ds
          # [<zone> <record-type> <key-tag> <algorithm-type> <digest-type> <digest>]
          # https://knot.readthedocs.io/en/master/configuration.html#automatic-dnssec-signing
          dnssec-signing = "on";
          dnssec-policy = "default";
          # Knot overwrites the zonefiles with auto-generated DNSSEC records by
          # default. Configure it to never overwrite, and store changes in the
          # journal (database) instead. This also allows Knot to handle the SOA
          # serial for us automatically, so we no longer need to update it.
          # https://knot.readthedocs.io/en/master/operation.html#handling-zone-file-journal-changes-serials
          zonefile-sync = -1;
          zonefile-load = "difference-no-serial";
          journal-content = "all";
        }
      ];
      zone = [
        {
          domain = "caspervk.net";
          file = "${secrets}/hosts/alpha/knot/caspervk.net.zone";
        }
        {
          domain = "spervk.com";
          file = "${secrets}/hosts/alpha/knot/spervk.com.zone";
        }
        {
          domain = "sudomail.org";
          file = "${secrets}/hosts/alpha/knot/sudomail.org.zone";
        }
        {
          domain = "vkristensen.dk";
          file = "${secrets}/hosts/alpha/knot/vkristensen.dk.zone";
        }
      ];
    };
  };

  networking.firewall = {
    allowedTCPPorts = [53];
    allowedUDPPorts = [53];
  };

  # Persist state
  environment.persistence."/nix/persist" = {
    directories = [
      {
        directory = "/var/lib/knot";
        user = "knot";
        group = "knot";
        mode = "0700";
      }
    ];
  };

  age.secrets.acme-knot-key-file = {
    file = "${secrets}/secrets/acme-knot-key-file.age";
    mode = "400";
    owner = "knot";
    group = "knot";
  };
}