ctucx.git: nixfiles

ctucx' nixfiles

commit ecf831c5ec9fa9f3ec1b9300da9fbd5062bd490a
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
configurations/linux/services/dns.nix
|
132
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
M
pkgs/overlay.nix
|
1
+
A
pkgs/toBase64.nix
|
37
+++++++++++++++++++++++++++++++++++++
A
secrets/hector/knot-keys.age
|
35
+++++++++++++++++++++++++++++++++++
M
secrets/secrets.nix
|
1
+
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 ];