{ 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" ]; # TODO migrate to settings settingsFile = pkgs.writeText "knot.conf" ('' include: /var/lib/knot/static.conf server: ${lib.concatStringsSep "\n" (map (address: " listen: ${address}@53") addresses.${config.networking.hostName})} automatic-acl: on log: - target: syslog server: info control: warning # otherwise stats gets logged every scrape zone: info mod-stats: - id: custom edns-presence: on flag-presence: on query-size: on query-type: on reply-size: on remote: ${lib.concatStrings (lib.mapAttrsToList (host: hostAddresses: '' - id: ${host} address: [${lib.concatStringsSep ", " hostAddresses}] '') addresses)} '' + (lib.optionalString isPrimaryHost '' # HACK: this string just continues the previous section - id: inwx # INWX only allows the specification of one primary DNS, # which limits the IP protocol usable for zone transfers to one. address: [185.181.104.96] policy: - id: default nsec3: on template: - id: default storage: /var/lib/knot/zones/ semantic-checks: on # auto increment serial zonefile-sync: -1 zonefile-load: difference-no-serial journal-content: all # secondary notify: [inwx, ${lib.concatStringsSep ", " secondaryHosts}] # dnssec dnssec-signing: on dnssec-policy: default # stats module: mod-stats/custom - id: nix-generated storage: /var/lib/knot/nix-zones/ semantic-checks: on # auto increment serial zonefile-sync: -1 zonefile-load: difference-no-serial journal-content: all # stats module: mod-stats/custom zone: ${lib.concatMapStrings (domain: '' - domain: ${domain} template: nix-generated '') (lib.attrNames cfg.generated-zones)} '') + (lib.optionalString isSecondaryHost '' acl: - id: primary_notify address: [${lib.concatStringsSep ", " (lib.flatten addresses.${primaryHost})}] action: notify template: - 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."; } ]; }; }