diff --git a/modules/authoritative-dns.nix b/modules/authoritative-dns.nix new file mode 100644 index 0000000..97c0cad --- /dev/null +++ b/modules/authoritative-dns.nix @@ -0,0 +1,177 @@ +{ 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" + ]; + extraConfig = '' + server: + ${lib.concatStringsSep "\n" (map (address: " listen: ${address}@53") addresses.${config.networking.hostName})} + automatic-acl: on + + log: + - target: syslog + any: info + 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 + - 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 + + 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] + ''); + }; + + 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"; + + BindReadOnlyPaths = [ "/run/knot/knot.sock" ]; + 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 + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; + }; + }; + + networking.firewall = { + allowedTCPPorts = [ 53 ]; + allowedUDPPorts = [ 53 ]; + }; + + 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."; + } + ]; + }; +} diff --git a/modules/default.nix b/modules/default.nix index b8b5e15..10dee35 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -20,6 +20,7 @@ imports = [ ../pkgs/modules.nix ./ausweisapp.nix + ./authoritative-dns.nix ./cups.nix ./docker.nix ./fancontrol.nix diff --git a/modules/wireguard/home.nix b/modules/wireguard/home.nix index 9cbed29..67172a1 100644 --- a/modules/wireguard/home.nix +++ b/modules/wireguard/home.nix @@ -126,38 +126,18 @@ in networking.firewall = { trustedInterfaces = [ "wg-home" ]; - allowedUDPPorts = lib.optionals enableServer [ - serverPort - 53 - ]; + allowedUDPPorts = lib.optional enableServer serverPort; }; - services.bind = lib.mkIf enableServer { - enable = true; - zones = lib.singleton { - name = "vpn.sbruder.de"; - master = true; - file = - let - # !!! very hacky - hexStringToInt = hex: (builtins.fromTOML "int = 0x${hex}").int; - - peerRecords = lib.concatStrings - (lib.mapAttrsToList - (peer: peerConfig: '' - ${peer} IN A ${peerConfig.address} - '') - peers); - - peerRecordsHash = builtins.hashString "sha256" peerRecords; - serial = hexStringToInt (lib.substring 0 8 peerRecordsHash); - in - pkgs.writeText "vpn.sbruder.de.zone" ('' - $TTL 3600 - @ IN SOA ${serverHostName}.sbruder.de. hostmaster.sbruder.de. ${toString serial} 28800 3600 604800 3600 - @ IN NS ${serverHostName}.sbruder.de. - '' + peerRecords); - }; - }; + sbruder.knot.generated-zones."vpn.sbruder.de" = pkgs.writeText "vpn.sbruder.de.zone" ('' + ; having $ORIGIN set here fails + @ IN SOA ${serverHostName}.sbruder.de. hostmaster.sbruder.de. 1 86400 10800 3600000 3600 + @ IN NS ${serverHostName}.sbruder.de. + '' + lib.concatStrings + (lib.mapAttrsToList + (peer: peerConfig: '' + ${peer} IN A ${peerConfig.address} + '') + peers)); }; }