# SPDX-FileCopyrightText: 2023 Simon Bruder # # 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 = [ "82.165.242.252" "2001:8d8:1800:8627::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."; } ]; }; }