Simon Bruder
f945341668
This applies the REUSE specification to the repository, so the licensing information can be tracked for every file individually.
220 lines
6.8 KiB
Nix
220 lines
6.8 KiB
Nix
# 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 couldn’t 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.";
|
||
}
|
||
];
|
||
};
|
||
}
|