nixos-config/modules/authoritative-dns.nix

220 lines
6.8 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# SPDX-FileCopyrightText: 2023 Simon Bruder <simon@sbruder.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
{ 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.";
}
];
};
}