2024-01-06 01:19:35 +01:00
|
|
|
|
# SPDX-FileCopyrightText: 2023 Simon Bruder <simon@sbruder.de>
|
|
|
|
|
#
|
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
2023-10-23 23:23:37 +02:00
|
|
|
|
{ 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"
|
|
|
|
|
];
|
2023-12-02 17:06:33 +01:00
|
|
|
|
settings = lib.mkMerge [
|
|
|
|
|
{
|
|
|
|
|
server = {
|
|
|
|
|
listen = map (address: "${address}@53") addresses.${config.networking.hostName};
|
|
|
|
|
automatic-acl = true;
|
|
|
|
|
};
|
2023-12-02 18:54:23 +01:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
log = lib.singleton {
|
|
|
|
|
target = "syslog";
|
|
|
|
|
server = "info";
|
|
|
|
|
control = "warning"; # otherwise stats gets logged every scrape
|
|
|
|
|
zone = "info";
|
|
|
|
|
};
|
2023-10-23 23:23:37 +02:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
mod-stats = lib.singleton {
|
|
|
|
|
id = "custom";
|
|
|
|
|
edns-presence = true;
|
|
|
|
|
flag-presence = true;
|
|
|
|
|
query-size = true;
|
|
|
|
|
query-type = true;
|
|
|
|
|
reply-size = true;
|
|
|
|
|
};
|
2023-10-26 01:18:17 +02:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
remote = (lib.mapAttrsToList
|
|
|
|
|
(host: hostAddresses: {
|
|
|
|
|
id = host;
|
|
|
|
|
address = hostAddresses;
|
|
|
|
|
})
|
|
|
|
|
addresses) ++ lib.optional isPrimaryHost {
|
|
|
|
|
id = "inwx";
|
2023-10-23 23:23:37 +02:00
|
|
|
|
# INWX only allows the specification of one primary DNS,
|
|
|
|
|
# which limits the IP protocol usable for zone transfers to one.
|
2023-12-02 17:06:33 +01:00
|
|
|
|
address = lib.singleton "185.181.104.96";
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
(lib.mkIf isPrimaryHost {
|
|
|
|
|
policy = lib.singleton {
|
|
|
|
|
id = "default";
|
|
|
|
|
nsec3 = true;
|
|
|
|
|
};
|
2023-10-23 23:23:37 +02:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
];
|
2023-10-23 23:23:37 +02:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
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";
|
|
|
|
|
};
|
2023-10-23 23:23:37 +02:00
|
|
|
|
|
2023-12-02 17:06:33 +01:00
|
|
|
|
template = lib.singleton {
|
|
|
|
|
id = "default";
|
|
|
|
|
master = [ primaryHost ];
|
|
|
|
|
acl = [ "primary_notify" ];
|
2023-10-26 01:18:17 +02:00
|
|
|
|
# stats
|
2023-12-02 17:06:33 +01:00
|
|
|
|
module = "mod-stats/custom";
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
];
|
2023-10-23 23:23:37 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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
|
2023-12-02 18:54:23 +01:00
|
|
|
|
# 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" ];
|
2023-10-23 23:23:37 +02:00
|
|
|
|
RestrictRealtime = true;
|
|
|
|
|
RestrictSUIDSGID = true;
|
|
|
|
|
SystemCallArchitectures = "native";
|
|
|
|
|
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
networking.firewall = {
|
|
|
|
|
allowedTCPPorts = [ 53 ];
|
|
|
|
|
allowedUDPPorts = [ 53 ];
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-26 01:18:17 +02:00
|
|
|
|
services.prometheus.exporters.knot = {
|
|
|
|
|
enable = true;
|
|
|
|
|
listenAddress = config.sbruder.wireguard.home.address;
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-23 23:23:37 +02:00
|
|
|
|
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.";
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
}
|