Simon Bruder
d93d724b9f
Previously, it was hosted on Ionos’s VMware-based infrastructure. I already had a VPS on their new KVM-based infrastructure, as I was planning to migrate okarin to it eventually (as it is cheaper). However, the new infrastructure does not offer PTR records for IPv6 addresses. Therefore, I was waiting until they would implement that feature (as the support promised me they would to in the near future). However, they are now migrating the (at least my) guests from their VMware hypervisors onto the KVM ones, assigning new IPv6 addresses to them. This makes the old VPS essentially the same as the old one, but with less memory and more expensive. So I decided to migrate now.
217 lines
6.6 KiB
Nix
217 lines
6.6 KiB
Nix
# SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||
#
|
||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
{ config, lib, pkgs, ... }:
|
||
let
|
||
cfg = config.sbruder.knot;
|
||
|
||
primaryHost = "vueko";
|
||
secondaryHosts = [ "renge" "okarin" "yuzuru" ];
|
||
|
||
isPrimaryHost = config.networking.hostName == primaryHost;
|
||
isSecondaryHost = lib.elem config.networking.hostName secondaryHosts;
|
||
|
||
addresses = {
|
||
vueko = [ "168.119.176.53" "2a01:4f8:c012:2f4::1" ];
|
||
renge = [ "152.53.13.113" "2a03:4000:6b:d2::1" ];
|
||
okarin = [ "85.215.165.213" "2a01:239:24b:1c00::1" ];
|
||
yuzuru = [ "85.215.73.203" "2a02:247a:272:1600::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.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 = 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.";
|
||
}
|
||
];
|
||
};
|
||
}
|