Skip to content

Instantly share code, notes, and snippets.

@andir
Created December 23, 2025 21:19
Show Gist options
  • Select an option

  • Save andir/6de9a5d650e31f781e053244223c205e to your computer and use it in GitHub Desktop.

Select an option

Save andir/6de9a5d650e31f781e053244223c205e to your computer and use it in GitHub Desktop.
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.h4ck.authorative-dns;
asBool = x: if x then "true" else "false";
# indent = n:
# let
# prefix = lib.concatStrings (map (_: " ") (lib.genList 0 n));
# in str: lib.concatMapStringsSep "\n" (line: "${prefix}${line}") (if builtins.typeOf str == "list" then lib.flatten str else (lib.splitString "\n" str));
#i2 = indent 2;
#i4 = indent 4;
authZoneOptions = { name, ... }: {
options = {
name = mkOption {
type = types.str;
};
zoneFile = mkOption {
type = types.path;
};
slaves = mkOption {
type = types.attrsOf (types.submodule slaveOptions);
default = { };
};
dnssecSigning = mkOption {
type = types.bool;
default = true;
};
};
config = {
inherit name;
};
};
slaveOptions = { name, ... }: {
options = {
name = mkOption {
type = types.str;
};
address = mkOption {
type = types.str;
};
port = mkOption {
type = types.port;
default = 53;
};
keyName = mkOption {
type = types.nullOr types.str;
default = null;
};
};
config = {
inherit name;
};
};
slaveZoneOptions = { name, ... }: {
options = {
name = mkOption {
type = types.str;
};
masters = mkOption {
type = types.listOf types.str;
};
tsigKeyFile = mkOption {
type = types.listOf types.str;
default = [ ];
};
dnssecSigning = mkOption {
type = types.bool;
default = true;
};
semanticChecks = mkOption {
type = types.bool;
default = true;
};
storage = mkOption {
type = types.str;
default = "/var/lib/knot";
};
};
config = {
inherit name;
};
};
in
{
options.h4ck.authorative-dns = {
enable = mkEnableOption "Enable authorative dns server";
listenAddresses = mkOption {
type = types.listOf types.str;
default = [ "::@53" "0.0.0.0@53" ];
};
verbose = mkOption {
default = false;
type = types.bool;
};
secondary = mkOption {
default = null;
};
authZones = mkOption {
type = types.attrsOf (types.submodule authZoneOptions);
default = { };
};
slaveZones = mkOption {
type = types.attrsOf (types.submodule slaveZoneOptions);
default = { };
};
enableStats = mkOption {
type = types.bool;
default = false;
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (builtins.intersectAttrs cfg.authZones cfg.slaveZones) == { };
message = "auth zones can't overlap with slave zones (yet)";
}
];
environment.systemPackages = [ pkgs.ldns ];
networking.firewall.allowedUDPPorts = [ 53 ];
networking.firewall.allowedTCPPorts = [ 53 ];
systemd.tmpfiles.rules = [
"d /var/lib/knot/keys - knot knot"
];
systemd.services."knot" = {
serviceConfig.Restart = lib.mkForce "always";
};
services.knot = {
enable = true;
extraArgs = mkIf cfg.verbose [ "-v" ];
extraConfig =
let
mkCheckedZone = zoneName: file: pkgs.runCommand "${zoneName}.zone"
{
buildInputs = [ pkgs.knot-dns ];
} ''
ln -s ${file} $out
kzonecheck -o ${zoneName} -v $out
'';
authZoneFilesDir = pkgs.runCommand "zonefiles" { } (
''
mkdir $out
''
+ lib.concatStringsSep "\n" (
lib.mapAttrsToList
(
_: zone: ''
ln -s $(readlink -f "${mkCheckedZone zone.name zone.zoneFile}") "$out/${zone.name}.zone"
''
)
cfg.authZones
)
);
authZoneFile = name: zone: pkgs.writeText "zone-${name}.conf" ''
remote:
${lib.concatStringsSep "\n" (
lib.mapAttrsToList
(
_: slave: ''
- id: ${name}-${slave.name}
address: ${slave.address}@${toString slave.port}
''
)
zone.slaves
)
}
acl:
${lib.concatStringsSep "\n" (
lib.mapAttrsToList
(
_: slave: ''
- id: ${slave.name}-${zone.name}
action: ["transfer", "notify"]
address: ${slave.address}
${lib.optionalString (slave.keyName != null) "key: ${slave.keyName}"}
''
)
zone.slaves
)
}
zone:
- domain: ${name}
storage: ${authZoneFilesDir}
file: ${zone.name}.zone
dnssec-signing: ${asBool zone.dnssecSigning}
acl: [${lib.concatMapStringsSep "," (slave: "\"${slave.name}-${zone.name}\"") (lib.attrValues zone.slaves)}]
${lib.optionalString (zone.slaves != { }) ''
notify: [${lib.concatStringsSep ", " (
lib.mapAttrsToList
(_: slave: "${name}-${slave.name}")
zone.slaves
)
}]
''}
'';
slaveZoneFile = name: zone: pkgs.writeText "zone-${name}.conf" ''
remote:
${lib.concatMapStringsSep "\n"
(
master: ''
- id: ${name}-${master}
address: ${master}
''
)
zone.masters}
acl:
- id: ${name}_notify
address: [${lib.concatStringsSep ", " zone.masters}]
action: notify
zone:
- domain: ${name}
acl: ${name}_notify
semantic-checks: ${asBool zone.semanticChecks}
dnssec-signing: ${asBool zone.dnssecSigning}
storage: ${zone.storage}
zonefile-sync: 0
zonefile-load: whole
journal-content: changes
master: [${lib.concatStringsSep ", " (map (x: "${name}-${x}") zone.masters)}]
'';
authZoneFiles = lib.mapAttrsToList authZoneFile cfg.authZones;
slaveZoneFiles = lib.mapAttrsToList slaveZoneFile cfg.slaveZones;
in
''
log:
- target: syslog
control: debug
zone: debug
server:
${lib.concatMapStringsSep "\n" (addr: " listen: ${addr}") cfg.listenAddresses}
${lib.optionalString cfg.enableStats ''
mod-stats:
- id: stats
request-protocol: true
server-operation: true
request-bytes: true
response-bytes: true
edns-presence: true
flag-presence: true
response-code: true
request-edns-option: true
response-edns-option: true
reply-nodata: true
query-type: true
query-size: true
reply-size: true
''}
database:
#storage: ${authZoneFilesDir}
journal-db: /var/lib/knot/journal
kasp-db: /var/lib/knot/kasp
timer-db: /var/lib/knot/timer
template:
- id: default
#storage: ${authZoneFilesDir}
journal-content: changes
zonefile-sync: -1
zonefile-load: difference
${lib.optionalString cfg.enableStats "module: mod-stats/stats"}
policy:
- id: rsa
zsk-size: 2048
ksk-size: 1024
algorithm: "RSASHA256"
nsec3: true
keystore:
- id: default
backend: pem
config: /var/lib/knot/keys
${lib.concatMapStringsSep "\n" (file: "include: ${file}") authZoneFiles}
${lib.concatMapStringsSep "\n" (file: "include: ${file}") slaveZoneFiles}
'';
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment