nixos-config/modules/authoritative-dns.nix

216 lines
6.7 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ config, lib, pkgs, ... }:
let
cfg = config.sbruder.knot;
primaryHost = "vueko";
secondaryHosts = [ "okarin" ];
isPrimaryHost = config.networking.hostName == primaryHost;
isSecondaryHost = lib.elem config.networking.hostName secondaryHosts;
addresses = {
vueko = [ "168.119.176.53" "2a01:4f8:c012:2f4::1" ];
okarin = [ "82.165.242.252" "2001:8d8:1800:8627::1" ];
};
in
{
options = {
sbruder.knot.generated-zones = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = { };
description = "List of zones generated by a nix expression";
};
};
config = lib.mkIf (isPrimaryHost || isSecondaryHost) {
services.knot = {
enable = true;
keyFiles = [
# Managed in separate repository.
# It includes all statically managed zones.
# Even though it is not a key,
# it needs to be included here
# so the module disables configuration checks.
"/var/lib/knot/static.conf"
];
settings = lib.mkMerge [
{
server = {
listen = map (address: "${address}@53") addresses.${config.networking.hostName};
automatic-acl = true;
};
log = lib.singleton {
target = "syslog";
server = "info";
control = "warning"; # otherwise stats gets logged every scrape
zone = "info";
};
mod-stats = lib.singleton {
id = "custom";
edns-presence = true;
flag-presence = true;
query-size = true;
query-type = true;
reply-size = true;
};
remote = (lib.mapAttrsToList
(host: hostAddresses: {
id = host;
address = hostAddresses;
})
addresses) ++ lib.optional isPrimaryHost {
id = "inwx";
# INWX only allows the specification of one primary DNS,
# which limits the IP protocol usable for zone transfers to one.
address = lib.singleton "185.181.104.96";
};
}
(lib.mkIf isPrimaryHost {
policy = lib.singleton {
id = "default";
nsec3 = true;
};
template = [
{
id = "default";
storage = "/var/lib/knot/zones/";
semantic-checks = true;
# auto increment serial
zonefile-sync = -1;
zonefile-load = "difference-no-serial";
journal-content = "all";
# secondary
notify = [ "inwx" ] ++ secondaryHosts;
# dnssec
dnssec-signing = true;
dnssec-policy = "default";
# stats
module = "mod-stats/custom";
}
{
id = "nix-generated";
storage = "/var/lib/knot/nix-zones/";
semantic-checks = true;
# auto increment serial
zonefile-sync = -1;
zonefile-load = "difference-no-serial";
journal-content = "all";
# stats
module = "mod-stats/custom";
}
];
zone = map
(domain: {
inherit domain;
template = "nix-generated";
})
(lib.attrNames cfg.generated-zones);
})
(lib.mkIf isSecondaryHost {
acl = lib.singleton {
id = "primary_notify";
address = lib.flatten addresses.${primaryHost};
action = "notify";
};
template = lib.singleton {
id = "default";
master = [ primaryHost ];
acl = [ "primary_notify" ];
# stats
module = "mod-stats/custom";
};
})
];
};
users.users.knot = {
openssh.authorizedKeys.keys = config.sbruder.pubkeys.trustedKeys;
shell = pkgs.bashInteractive;
};
systemd.tmpfiles.rules = [
"f /var/lib/knot/static.conf 0644 knot knot - -"
] ++ (lib.optionals isPrimaryHost [
"d /var/lib/knot/nix-zones 0755 knot knot - -"
]);
systemd.services.knot-generated-zones = lib.mkIf isPrimaryHost {
wantedBy = [ "knot.service" ];
after = [ "knot.service" ];
path = with pkgs; [ knot-dns ];
script = ''
set -euo pipefail
${lib.concatStringsSep "\n" (lib.mapAttrsToList (domain: zonefile: ''
kzonecheck -o ${lib.escapeShellArg domain} ${lib.escapeShellArg zonefile}
target=/var/lib/knot/nix-zones/${lib.escapeShellArg domain}.zone
if [ -h "$target" ]; then
pre_target="$(readlink "$target")"
else
pre_target="/invalid/path"
fi
ln -sf ${lib.escapeShellArg zonefile} "$target"
if [ "$pre_target" != ${lib.escapeShellArg domain} ]; then
echo -n "Zone for ${lib.escapeShellArg domain} changed, reloading "
knotc zone-reload ${lib.escapeShellArg domain}
fi
'') cfg.generated-zones)}
'';
restartTriggers = lib.attrValues cfg.generated-zones;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "knot";
CapabilityBoundingSet = ""; # clear
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateNetwork = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = true;
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX" ]; # knot socket
# this is not ideal, but I couldnt find out how to get a bind mount of the knot socket to work otherwise
RestrictNamespaces = [ true "~mnt" ];
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
};
};
networking.firewall = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
services.prometheus.exporters.knot = {
enable = true;
listenAddress = config.sbruder.wireguard.home.address;
};
assertions = [
{
assertion = isPrimaryHost -> (lib.hasAttr "vpn.sbruder.de" cfg.generated-zones);
message = "The authoritative DNS module requires the server the wg-home wireguard server to run on the same host.";
}
];
};
}