commit ecf831c5ec9fa9f3ec1b9300da9fbd5062bd490a
parent b00c6814c133964a5807bcef87889cb4d8d161ba
Author: Katja (ctucx) <git@ctu.cx>
Date: Sat, 1 Mar 2025 16:58:41 +0100
parent b00c6814c133964a5807bcef87889cb4d8d161ba
Author: Katja (ctucx) <git@ctu.cx>
Date: Sat, 1 Mar 2025 16:58:41 +0100
configurations/linux/services/dns: implement acme dns-challenge support
5 files changed, 199 insertions(+), 7 deletions(-)
M
|
132
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
diff --git a/configurations/linux/services/dns.nix b/configurations/linux/services/dns.nix @@ -1,11 +1,115 @@ -{ nodes, config, lib, pkgs, ...}: +{ currentSystem, nodes, config, lib, pkgs, ...}: + +let + acmeZone = "acme.ctu.cx"; + + generateACMERecord = recordName: ( + (builtins.hashString "sha1" recordName) + ".${acmeZone}." + ); + + nodesWithACMERecords = ( + nodes + |> lib.filterAttrs (hostName: nodeCfg: nodeCfg.config.security.acme.certs != {}) + ); + + getAllDomainsPerNode = hostName: ( + nodes.${hostName}.config.security.acme.certs + |> lib.mapAttrsToList (domain: cfg: [ domain ] ++ cfg.extraDomainNames) + |> lib.flatten + ); + + getACMERecordsPerNode = hostName: ( + hostName + |> getAllDomainsPerNode + |> builtins.map (recordName: (generateACMERecord recordName)) + ); + + generateACMERecordsPerZone = zoneName: ( + nodesWithACMERecords + |> lib.mapAttrsToList (hostName: _: (getAllDomainsPerNode hostName)) + |> lib.flatten + |> builtins.filter (lib.hasSuffix zoneName) + |> builtins.map (recordName: { + name = "_acme-challenge${if zoneName != recordName then "." else ""}${lib.removeSuffix "${if zoneName != recordName then "." else ""}${zoneName}" recordName}"; + value = { + CNAME = [ (generateACMERecord recordName) ]; + }; + }) + |> builtins.listToAttrs + ); + +in { + + age.secrets = lib.mkIf config.dns.primary { + knotKeys = { + file = ./. + "/../../../secrets/${config.networking.hostName}/knot-keys.age"; + owner = "knot"; + group = "knot"; + }; + }; + + systemd.tmpfiles.settings.knotExtraZones = lib.mkIf config.dns.primary { + "${config.dns.dataDir}/extraZones".d = { + group = "knot"; + user = "knot"; + mode = "770"; + age = "-"; + }; -{ + "${config.dns.dataDir}/extraZones/${acmeZone}.zone"."f~" = { + group = "knot"; + user = "knot"; + mode = "770"; + age = "-"; + argument = pkgs.toBase64 ( + pkgs.dns.lib.types.zoneToString acmeZone (pkgs.dns.lib.evalZone acmeZone (with pkgs.dns.lib.combinators; { + NS = [ "ns1.ctu.cx." "ns2.ctu.cx." ]; + SOA = { + nameServer = "ns1.ctu.cx."; + adminEmail = "dns@ctu.cx"; # Email address with a real `@`! + serial = 0; + }; + })) + ); + }; + }; dns = { - enable = lib.mkDefault (builtins.elem "dnsServer" config.deployment.tags); - primary = lib.mkDefault (config.networking.hostName == "hector"); - allZones = with pkgs.dns.lib.combinators; let + enable = lib.mkDefault (builtins.elem "dnsServer" config.deployment.tags); + primary = lib.mkDefault (config.networking.hostName == "hector"); + keyFiles = lib.mkIf config.dns.primary [ config.age.secrets.knotKeys.path ]; + extraZones = lib.mkIf config.dns.primary { + "${acmeZone}" = { + storage = "${config.dns.dataDir}/extraZones"; + file = "${acmeZone}.zone"; + + zonefile-sync = 0; + zonefile-load = "difference-no-serial"; + + journal-content = "all"; + + acl = ( + nodesWithACMERecords + |> lib.mapAttrsToList (hostName: _: "acme-nix-${hostName}") + ); + }; + }; + + extraACL = lib.mkIf config.dns.primary ( + nodesWithACMERecords + |> lib.mapAttrs' (hostName: _: { + name = "acme-nix-${hostName}"; + value = { + key = [ "acme-nix-${hostName}" ]; + action = "update"; + update-owner = "name"; + update-owner-match = "equal"; + update-owner-name = getACMERecordsPerNode hostName; + }; + }) + ); + + allZones = with pkgs.dns.lib.combinators; let CAA = [ { issuerCritical = false; tag = "issue"; value = "letsencrypt.org"; } ]; NS = [ "ns1.ctu.cx." "ns2.ctu.cx." ]; SOA = { @@ -22,33 +126,47 @@ ns1 = (host nodes.hector.config.networking.primaryIP4 nodes.hector.config.networking.primaryIP); ns2 = (host nodes.wanderduene.config.networking.primaryIP4 nodes.wanderduene.config.networking.primaryIP); + "acme".NS = [ "ns1" "ns2" ]; + _atproto.TXT = [ "did=did:plc:zaeuok3fmh2pcp4cjiicku4i" ]; test.TXT = [ "test uwu"]; - }; + } // (generateACMERecordsPerZone "ctu.cx"); }; "wifionic.de" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "wifionic.de"; }; "trans-agenda.de" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "trans-agenda.de"; }; "katja.wtf" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "katja.wtf"; }; "ctucx.de" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "ctucx.de"; }; "zuggeschmack.de" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "zuggeschmack.de"; }; "thein.ovh" = { inherit SOA NS CAA; + + subdomains = generateACMERecordsPerZone "thein.ovh"; }; "flauschehorn.sexy" = { @@ -60,7 +178,7 @@ subdomains = { _dmarc.TXT = [ "v=DMARC1; p=quarantine; rua=mailto:hostmaster@kunbox.net; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r" ]; "mail._domainkey".TXT = [ "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnh5Ym9PO7r+wdOIKfopvHzn3KU3qT6IlCG/gvvbmIqoeFQfRbAe3gQmcG6RcLue55cJQGhI6y2r0lm59ZeoHR40aM+VabAOlplekM7xWmoXb/9vG2OZLIqAyF4I+7GQmTN6B9keBHp9SWtDUkI0B0G9neZ5MkXJP705M0duxritqQlb4YvCZwteHiyckKcg9aE9j+GF2EEawBoVDpoveoB3+wgde3lWEUjjwKFtXNXxuN354o6jgXgPNWtIEdPMLfK/o0CaCjZNlzaLTsTegY/+67hdHFqDmm8zXO9s+Xiyfq7CVq21t7wDhQ2W1agj+up6lH82FMh5rZNxJ6XB0yQIDAQAB" ]; - }; + } // (generateACMERecordsPerZone "flauschehorn.sexy"); }; };
diff --git a/pkgs/overlay.nix b/pkgs/overlay.nix @@ -2,6 +2,7 @@ final: prev: { + toBase64 = (final.callPackage ./toBase64.nix {}).toBase64; writePythonScriptBin = (final.callPackage ./writePythonScriptBin.nix {}).writePythonScriptBin; adwaita-colors-icon-theme = final.callPackage ./adwaita-colors.nix {};
diff --git a/pkgs/toBase64.nix b/pkgs/toBase64.nix @@ -0,0 +1,36 @@ +{ lib, ... }: + +{ + + toBase64 = text: let + inherit (lib) sublist mod stringToCharacters concatMapStrings; + inherit (lib.strings) charToInt; + inherit (builtins) substring foldl' genList elemAt length concatStringsSep stringLength; + lookup = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + sliceN = size: list: n: sublist (n * size) size list; + pows = [(64 * 64 * 64) (64 * 64) 64 1]; + intSextets = i: map (j: mod (i / j) 64) pows; + compose = f: g: x: f (g x); + intToChar = elemAt lookup; + convertTripletInt = sliceInt: concatMapStrings intToChar (intSextets sliceInt); + sliceToInt = foldl' (acc: val: acc * 256 + val) 0; + convertTriplet = compose convertTripletInt sliceToInt; + join = concatStringsSep ""; + convertLastSlice = slice: let + len = length slice; + in + if len == 1 + then (substring 0 2 (convertTripletInt ((sliceToInt slice) * 256 * 256))) + "==" + else if len == 2 + then (substring 0 3 (convertTripletInt ((sliceToInt slice) * 256))) + "=" + else ""; + len = stringLength text; + nFullSlices = len / 3; + bytes = map charToInt (stringToCharacters text); + tripletAt = sliceN 3 bytes; + head = genList (compose convertTriplet tripletAt) nFullSlices; + tail = convertLastSlice (tripletAt nFullSlices); + in + join (head ++ [tail]); + +}+ \ No newline at end of file
diff --git a/secrets/hector/knot-keys.age b/secrets/hector/knot-keys.age @@ -0,0 +1,35 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkM3Rob242Z2IzYzZRQzVh +T0RsMHJaZTVsc0xWY3E3WTBNYTZrckhObDFZCjFsVjZPYzZIUkErVHlJQVcwVHQv +U1NSRE9DTWtrMVVCVnJudGFGRHhPRXMKLT4gc3NoLWVkMjU1MTkgeWFMSFNRIEZV +L1owZzIyK0tHQnp1SFk3cWlZUjcycThWSTNzZXJOTmtrUWp6SGZjMmcKNXlIVnBr +N3EvNzNEOVpMcWtVVEhuYzJCd1djZE94dGQyY0lXczNITTVkYwotPiBVM1YtZ3Jl +YXNlICs2dj4gdWsKeG1FdFpLWEx5UU9FT1hNbW9ZWTIvK0k1RXBpdTJGL1k5MHFi +US93THRjTzJMSERUclhIcXZEM25rOG5WaWdTeAo5QUlibzdSTnZOWExjSm1YUFND +bitRCi0tLSAvRFNubVU2VkUxTHhmbFIyYWZWbmdWcTAwK1FWZTZ0dFRHdEZLMVIw +M0c4Cv79lH/bA6fAx0nQSXvVvCwK5DbxqL1jcN/1/ISpRS7JVVtHxKV9R9Ka/N2F +oRMFsjeHZGIlBqVFz1KvtedJvJ92GOym0dY8rOvYvzJYbkMyf1FSayxVWv+7lrqB +tv51nWtK81wfR2I4W+Z7M3gPLE7JLRyGDQ/C1yiAqk7JpN/G4RwhU2E/7ODDJlVp +y2laCbPMl0xUZMHO0ZEsBMRBi30j8hlssOOJ4o/zN4Ny3/yKhQtNaD1ARi8ojBV8 +814wWaL4w4sGiNBcZWR62zo46WuckDrvhyXanLPwCip1LFTZqtTq5fhvNo+N/Ank +s1HFlfFveouweeTDJRsXQUDmsbfV99hZsDgC4qOnelVgMkrOOoMGOeehsujrdLqg +BZrnPIZLtIa9IKHe9NyBrL3u34B7jmRqmwq2lBQZHkBDqQ4zOm08Pg3NmjbcD9Y3 +DEBsVCZE6B6Kw9kbB2Rk0QQzaI4NG2jgAmO7RowuIymvyjFCq/EkUM6JFVyiEkYd +PCAf92VfILyuENrFYO37zwwi55VM13E0ozSu1+9jHcT49wikdZqT/n4fckeMj/SC +H4BkP0F7s1BIOJok5Sdk2bEcdqOZo75KylkIQIF2NOxiImJV5VI9l6+tT75bKcaX +sLttIRtyUkSRrAWtu/KCBSQvnoMRG3sCtY9g9YFx53Re1RdCTcxPMXUt86rknZT7 +bicLWQ2DW0xL+j8XUxz6RxQ8AYWNrETlpHuCA/cCf8YnThY1wuamfTzbsu6kXjlO +u+f1RzLmzpLJ3yfySHHmld42Dc+v3TY3mZC28KG4pkl6Pkp4/mHcUYg2zEPPXrgS +GN0dvL63gLGu4Z0KTFNudF2SSjckKNyiWSoFPpOnQ1lsm5mjPzOk2olV0KjA5gYM +bABBe0YwIs2OpXbt4Ikfue7XWuzFmY7QcIAYvNI9xtu/KXAwM7qKlrUjOf8y9Ea9 +Z5IlW/VukmvKfHiVYOnD0ca677MvMkSZpoSLdu1vkYfOdSFEqBTqWfxPhvyRCx3T +1/aA1es7mH7u6vVps+xLFmGhQIMMzdqt9jaAvn9xRU8xrhddD5MCM3B7p2aAgLLc +gDf+6D3lOMH/0+pl0LAxIvHOAmG8+y21ttK5PGoZtjwldLnhB+nJ4vnT38YlRWjD +7HJNVuvDZRnZODdV6nT5VQYz3EOPOWVe2kBeBZ+ItSYM8Ms1XnsWMI3oxMUZiQB9 +9adreq0gsnYyMRTnstqadZnPv1oz6Oo2zzfAcDsfMnJk6a7dAvY6hjcz/pCtLwKy +97aL1//w4j97ER/rhN6IO+tpOJHR5aVJJZ3IMbxC+hX9QUJCEu8Ewm9ANqWNIFt6 +c0PNQCmqxapPeFh6tbtfC3LSvkSXw/8koQ52dneR+I73df7Behe/REANyFB/gdS/ +yG9KX6zsplephttzlL9B0pAUJ+A96yE8Hb3qeAnQ+pvyyXvjauP92drGW7NER7sb +1kY3nOmVYhqRM9Ch+Tih/y6y/L1pi/OcE4beMVaAXIozdmoJXrTMVeathD+dmux1 +qSy7PAd/EH2ygNdgsBL81rQnsHeHQCdxLZWUiEu6QKVf9mZCR6l6RhKNWO37Glg= +-----END AGE ENCRYPTED FILE-----
diff --git a/secrets/secrets.nix b/secrets/secrets.nix @@ -68,6 +68,7 @@ in { "wanderduene/syncthing/key.age".publicKeys = [ main-key wanderduene ]; "wanderduene/syncthing/cert.age".publicKeys = [ main-key wanderduene ]; + "hector/knot-keys.age".publicKeys = [ main-key hector ]; "hector/restic/radicale.age".publicKeys = [ main-key hector ]; "hector/restic/vaultwarden.age".publicKeys = [ main-key hector ];