From 7b3c103b5b6d392e6cb715980f69eaa79ea382b7 Mon Sep 17 00:00:00 2001 From: Asyncnomi Date: Sat, 26 Jul 2025 22:52:35 +0200 Subject: [PATCH] add dns role --- mapping.nix | 12 +++ secrets/dns/tsig.age | Bin 0 -> 1025 bytes secrets/mail/dkim.age | Bin 0 -> 4005 bytes secrets/secrets.nix | 19 ++++ shared/dns.nix | 7 ++ shared/dns/dkim.nix | 3 + shared/dns/knot.nix | 210 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 251 insertions(+) create mode 100644 secrets/dns/tsig.age create mode 100644 secrets/mail/dkim.age create mode 100644 shared/dns.nix create mode 100644 shared/dns/dkim.nix create mode 100644 shared/dns/knot.nix diff --git a/mapping.nix b/mapping.nix index e9c044c..65d0f77 100644 --- a/mapping.nix +++ b/mapping.nix @@ -15,6 +15,18 @@ ]; }; + dns = rec { + master = "master-dns-aur-lasuite-federez"; + secondary = [ + "dns-mtz-lasuite-federez" + "dns-ren-lasuite-federez" + ]; + hosts = [ master ] ++ secondary; + _inherit = [ + ./shared/dns.nix + ]; + }; + # For instance: # psql = rec { # master = "some-node-1"; diff --git a/secrets/dns/tsig.age b/secrets/dns/tsig.age new file mode 100644 index 0000000000000000000000000000000000000000..7aef06ac61d300f7c2c005eb3016296693caa438 GIT binary patch literal 1025 zcmZ9_&#T*X008j0iDCp1nRxJ0Mcg3PG-;DG=mvRhel2_vL^u(??SAG*te+veH0 z<|x&+&G0c_3n%Pqj?H-iUyMW-I<6`aOy6BuCO~>sO_@5F>$xl3FjRrBz@**>wBPsS zK~$<>45NVtO))sTxvfFj?SKM+o>~T(Zsh8^Q`cEmY^U?OBDnNGCV_+}YYP=^BIHaP ziD|{e9L&%Op4NlrSYLO*icjXmtkLLh#RF;8&9xwuWpuf@arUCpWDARNPRH$6QWz$(cbAYsKtYLE7j^NLceT6|m{FmHQ=mT;WXBjmm>LwcsPPLlYx0 ztd^iQ%S>Z)%S30SE(^)ZHY-bt%#5gC!`U2VB|If8t*^lpMssgsQxb)a7;9=EYcwON z!1Pp@L*JPadg?8CiN@uoY2YcUELsj~Y4v)wr*3YkPp?e2z*DdtH?o@7#2397U3+M| zY5{TtW;m(fe%EMq%U!ECDVG9dq*|e=?G_;bmz;H{lXtyj(6Zs+1U> zU$=}FYHigN88*N!{er~jx=t|K?^pZFcSGQ4$;q(5Muzi{vF&iyZcICJ^l zpW=CV?6vpy-^`-?xP9SLGUy#s&#=8k*#-Oo=s%C~#_N1r%*{_OXk-hA=O>Fc8-JHzdRj|P{2 F{10T~W-0&x literal 0 HcmV?d00001 diff --git a/secrets/mail/dkim.age b/secrets/mail/dkim.age new file mode 100644 index 0000000000000000000000000000000000000000..fdeb75e1757fbb0314c44c647e69373bfa679a37 GIT binary patch literal 4005 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 $out + ''; + timestamp = builtins.readFile timestampDerivation; + + # Domain key + domainkey = '' + v=DKIM1; k=rsa; p=${dkim.dkim_pub}''; + segments = ((lib.stringLength domainkey) / 255); + domainkeySplitted = map (x: lib.substring (x*255) 255 domainkey) (lib.range 0 segments); + + + ##### + ## Knot specific + ##### + + # Define remote based on current role + remotes = if myNode == mapping.dns.master then + map (hostname: { + id = hostname; + address = "172.19.${toString nodes.${hostname}.zone}.${toString nodes.${hostname}.id}@53"; + }) mapping.dns.secondary + else + [{ + id = mapping.dns.master; + address = "172.19.${toString nodes.${mapping.dns.master}.zone}.${toString nodes.${mapping.dns.master}.id}@53"; + }]; + + remotesNames = map (remote: remote.id) remotes; + + # Remotes ACL + remotesACL = if myNode == mapping.dns.master then + map (hostname: { + id = "acl_${hostname}"; + address = "172.19.${toString nodes.${hostname}.zone}.${toString nodes.${hostname}.id}@53"; + action = "transfer"; + }) mapping.dns.secondary + else + [{ + id = "acl_${mapping.dns.master}"; + address = "172.19.${toString nodes.${mapping.dns.master}.zone}.${toString nodes.${mapping.dns.master}.id}@53"; + action = "notify"; + }]; + + remotesACLNames = map (remote: remote.id) remotesACL; + + # Other ACL + letsencryptACL = if myNode == mapping.dns.master then + [{ + id = "acl_le_challenge"; + address = [ + "172.19.0.0/16" + "fc00::/96" + ]; + action = "update"; + update-type = "TXT"; + key = "letsencrypt"; + }] + else []; + + ##### + ## Zone specific + ##### + + # host to dn + hostToDomain = hostname: builtins.replaceStrings ["-"] ["."] hostname; + + # Gen NS + toNSRecord = host: "IN NS ${hostToDomain host}.net."; + nsRecords = map toNSRecord mapping.dns.secondary; + + dnsSecondaryConfigs = lib.filterAttrs (peerName: _peerConfig: lib.elem peerName mapping.dns.secondary) nodes; + + # Gen A NS + nsARecords = lib.flatten (lib.mapAttrsToList (hostname: node: + lib.optional (supportsIPv4 node) "${hostToDomain hostname}.net. IN A ${node.ip4}" + ) dnsSecondaryConfigs); + + # Gen AAAA NS + nsAAAARecords = lib.flatten (lib.mapAttrsToList (hostname: node: + lib.optional (supportsIPv6 node) "${hostToDomain hostname}.net. IN AAAA ${node.ip6}" + ) dnsSecondaryConfigs); + + # Zone conf + zoneFilePath = "/var/lib/knot/zones/zone-lasuite-federez-net"; + zone-lasuite-federez-net = pkgs.writeText "zone-lasuite-federez-net" '' + $ORIGIN lasuite.federez.net. + $TTL 60 + @ IN SOA ${builtins.head nsRecords}. monitoring.lasuite.federez.net. ( + ${timestamp} ; serial + 60 ; refresh + 60 ; retry + 60 ; expire + 60 ) ; minimum TTL + + IN TXT "v=spf1 a:lasuite.federez.net ~all" + + ${builtins.concatStringsSep "\n" nsRecords} + + ${builtins.concatStringsSep "\n" nsARecords} + ${builtins.concatStringsSep "\n" nsAAAARecords} + + _dmarc IN TXT "v=DMARC1; p=quarantine; ruf=mailto:postmaster@lasuite.federez.net" + _mta-sts IN TXT "v=STSv1; id=1" + _smtp._tls IN TXT "v=TLSRPTv1;rua=mailto:postmaster@lasuite.federez.net" + default._domainkey IN TXT "${lib.concatStringsSep "\" \"" domainkeySplitted}" + cause-toujours IN TXT "v=spf1 a:lasuite.federez.net ~all" + ''; +in +{ + age.secrets = { + "tsig" = { + file = ./../../secrets/dns/tsig.age; + owner = "knot"; + group = "knot"; + }; + }; + + # Ensure the directory exists and is writable + systemd.tmpfiles.rules = [ + "d /var/lib/knot/zones 0755 knot knot -" + "f /var/lib/knot/zones/zone-lasuite-federez-net 0644 knot knot -" + ]; + + # Knot messes with resolvd + services.resolved.enable = false; + networking.resolvconf.extraConfig = '' + name_servers="1.1.1.1 1.0.0.1 2606:4700:4700::1111" + ''; + + # Attach knot to writeZoneFile to force knot to restart after rebuild (otherwise no changes will be detected) + systemd.services.knot = { + partOf = [ "writeZoneFile.service" ]; + restartTriggers = [ zone-lasuite-federez-net ]; + reloadIfChanged = lib.mkForce false; + }; + services.knot = { + enable = true; + package = pkgs.knot-dns; + + # To regenerate the secret use: + # nix-shell -p knot-dns + # keymgr tsig generate -t letsencrypt hmac-sha512 + keyFiles = [ + config.age.secrets.tsig.path + ]; + + settings = { + server = { + listen = [ + "0.0.0.0@53" + "::@53" + ]; + }; + + remote = remotes; + + acl = remotesACL ++ otherACL; + + zone = if myNode == mapping.dns.master then [ + { + domain = "lasuite.federez.net"; + file = zoneFilePath; + notify = remotesNames; + acl = remotesACLNames ++ [ + "acl_le_challenge" + ]; + } + ] else [ + { + domain = "lasuite.federez.net"; + master = builtins.head remotesNames; + acl = remotesACLNames; + } + ]; + + log = [ + { + target = "syslog"; + any = "debug"; + } + ]; + }; + }; + + # Write the generated zone file to the writable path + systemd.services.writeZoneFile = { + before = [ "knot.service" ]; + description = "Write initial zone file for lasuite.federez.net"; + serviceConfig = { + ExecStart = "${pkgs.coreutils}/bin/cp '${zone-lasuite-federez-net}' ${zoneFilePath}"; + }; + wantedBy = [ "multi-user.target" ]; + }; +} \ No newline at end of file