From fd36e012f93d36c463f9b86b8e48dbd28e4a6866 Mon Sep 17 00:00:00 2001
From: prescientmoon <git@moonythm.dev>
Date: Mon, 8 Jul 2024 03:06:27 +0200
Subject: [PATCH] Custom octodns setup!

---
 dns/common.nix                                | 74 +++++++++++++++++++
 dns/octodns.yaml                              | 20 +++++
 dns/pkgs.nix                                  | 37 ++++++++++
 flake.nix                                     |  7 +-
 home/features/wayland/hyprland/hyprland.conf  |  2 +-
 hosts/nixos/common/global/default.nix         |  3 +
 hosts/nixos/common/global/ports.nix           |  1 +
 .../nixos/common/optional/services/nginx.nix  |  1 -
 .../common/optional/services/syncthing.nix    |  7 ++
 hosts/nixos/common/secrets.yaml               |  6 +-
 hosts/nixos/lapetus/default.nix               | 18 ++++-
 hosts/nixos/lapetus/services/forgejo.nix      | 17 ++---
 hosts/nixos/lapetus/services/homer.nix        |  2 +-
 hosts/nixos/lapetus/services/jupyter.nix      |  4 +-
 hosts/nixos/lapetus/services/microbin.nix     | 10 +--
 hosts/nixos/lapetus/services/pounce.nix       | 17 ++++-
 hosts/nixos/lapetus/services/syncthing.nix    | 12 ---
 hosts/nixos/tethys/default.nix                | 16 +++-
 modules/common/octodns.nix                    | 46 ++++++++++++
 modules/nixos/cloudflared.nix                 | 39 +++++++++-
 modules/nixos/default.nix                     |  2 +
 modules/nixos/dns-assertions.nix              | 17 +++++
 modules/nixos/dns.nix                         | 59 +++++++++++++++
 modules/nixos/nginx.nix                       | 27 +++++--
 pkgs/default.nix                              |  2 +-
 pkgs/octodns-cloudflare.nix                   | 45 +++++++++++
 scripts/dns/dns.txt                           |  2 +-
 27 files changed, 434 insertions(+), 59 deletions(-)
 create mode 100644 dns/common.nix
 create mode 100644 dns/octodns.yaml
 create mode 100644 dns/pkgs.nix
 delete mode 100644 hosts/nixos/lapetus/services/syncthing.nix
 create mode 100644 modules/common/octodns.nix
 create mode 100644 modules/nixos/dns-assertions.nix
 create mode 100644 modules/nixos/dns.nix
 create mode 100644 pkgs/octodns-cloudflare.nix

diff --git a/dns/common.nix b/dns/common.nix
new file mode 100644
index 0000000..39e0ebe
--- /dev/null
+++ b/dns/common.nix
@@ -0,0 +1,74 @@
+# DNS entries which do not belong to a particular host
+{ lib, ... }:
+let
+  # {{{ Github pages helper
+  ghPage = at: [{
+    inherit at; type = "CNAME";
+    value = "prescientmoon.github.io.";
+  }];
+  # }}}
+  # {{{ Migadu mail DNS setup
+  migaduMail = at: verifyKey:
+    let atPrefix = prefix: if at == "" then prefix else "${prefix}.${at}";
+    in
+    [
+      {
+        inherit at;
+        ttl = 600;
+        type = "MX";
+        value = [
+          {
+            exchange = "aspmx1.migadu.com.";
+            preference = 10;
+          }
+          {
+            exchange = "aspmx2.migadu.com.";
+            preference = 20;
+          }
+        ];
+      }
+      {
+        inherit at;
+        ttl = 600;
+        type = "TXT";
+        value = [
+          "v=spf1 include:spf.migadu.com -all"
+          "hosted-email-verify=${verifyKey}"
+        ];
+      }
+      {
+        at = atPrefix "_dmarc";
+        type = "TXT";
+        value = ''v=DMARC1\; p=quarantine\;'';
+        ttl = 600;
+      }
+      {
+        at = atPrefix "key1._domainkey";
+        type = "CNAME";
+        value = "key1.orbit.moonythm.dev._domainkey.migadu.com.";
+        ttl = 600;
+      }
+      {
+        at = atPrefix "key2._domainkey";
+        type = "CNAME";
+        value = "key2.orbit.moonythm.dev._domainkey.migadu.com.";
+        ttl = 600;
+      }
+      {
+        at = atPrefix "key3._domainkey";
+        type = "CNAME";
+        value = "key3.orbit.moonythm.dev._domainkey.migadu.com.";
+        ttl = 600;
+      }
+    ];
+  # }}}
+in
+{
+  satellite.dns.domain = "moonythm.dev";
+  satellite.dns.records = lib.flatten [
+    (ghPage "doffycup")
+    (ghPage "erratic-gate")
+    (migaduMail "" "kfkhyexd")
+    (migaduMail "orbit" "24s7lnum")
+  ];
+}
diff --git a/dns/octodns.yaml b/dns/octodns.yaml
new file mode 100644
index 0000000..9734776
--- /dev/null
+++ b/dns/octodns.yaml
@@ -0,0 +1,20 @@
+manager:
+  max_workers: 10
+
+providers:
+  zones:
+    class: octodns.provider.yaml.YamlProvider
+    default_ttl: 300
+    enforce_order: true
+    directory: this is set by nix :3
+
+  cloudflare:
+    class: octodns_cloudflare.CloudflareProvider
+    token: 'env/CLOUDFLARE_TOKEN'
+
+zones:
+  moonythm.dev.:
+    sources:
+      - zones
+    targets:
+      - cloudflare
diff --git a/dns/pkgs.nix b/dns/pkgs.nix
new file mode 100644
index 0000000..3773b1a
--- /dev/null
+++ b/dns/pkgs.nix
@@ -0,0 +1,37 @@
+{ pkgs, self, system, ... }: rec {
+  octodns-zones =
+    let
+      nixosConfigModules = pkgs.lib.mapAttrsToList
+        (_: current: { satellite.dns = current.config.satellite.dns; })
+        self.nixosConfigurations;
+
+      evaluated = pkgs.lib.evalModules {
+        specialArgs = { inherit pkgs; };
+        modules = [
+          ../modules/nixos/dns.nix
+          ../modules/common/octodns.nix
+          ./common.nix
+        ]
+        ++ nixosConfigModules;
+      };
+    in
+    evaluated.config.satellite.dns.octodns;
+  octodns-sync =
+    pkgs.symlinkJoin {
+      name = "octodns-sync";
+      paths = [ self.packages.${system}.octodns ];
+      buildInputs = [ pkgs.makeWrapper pkgs.yq ];
+      postBuild = ''
+        cat ${./octodns.yaml} | yq '.providers.zones.directory="${octodns-zones}"' > $out/config.yaml
+        wrapProgram $out/bin/octodns-sync \
+          --run 'export CLOUDFLARE_TOKEN=$( \
+              sops \
+                --decrypt \
+                --extract "[\"cloudflare_dns_api_token\"]" \
+                ./hosts/nixos/common/secrets.yaml \
+            )' \
+          --add-flags "--config-file $out/config.yaml"
+      '';
+    };
+}
+
diff --git a/flake.nix b/flake.nix
index 76017af..ed55489 100644
--- a/flake.nix
+++ b/flake.nix
@@ -106,11 +106,12 @@
           let
             pkgs = nixpkgs.legacyPackages.${system};
             upkgs = inputs.nixpkgs-unstable.legacyPackages.${system};
+            myPkgs = import ./pkgs { inherit pkgs upkgs; };
           in
-          import ./pkgs { inherit pkgs upkgs; } // {
+          myPkgs // {
             octodns = upkgs.octodns.withProviders
-              (ps: [ (import ./pkgs { inherit pkgs upkgs; }).octodns-cloudflare ]);
-          }
+              (ps: [ myPkgs.octodns-cloudflare ]);
+          } // (import ./dns/pkgs.nix) { inherit pkgs self system; }
         );
       # }}}
       # {{{ Bootstrapping and other pinned devshells
diff --git a/home/features/wayland/hyprland/hyprland.conf b/home/features/wayland/hyprland/hyprland.conf
index 915a275..9a1b4c2 100644
--- a/home/features/wayland/hyprland/hyprland.conf
+++ b/home/features/wayland/hyprland/hyprland.conf
@@ -44,7 +44,7 @@ animations {
 }
 
 # Execute apps at launch
-exec-once = wezterm & firefox & discocss & spot & obsidian & obsidiantui & smosgui
+exec-once = wezterm & firefox & discocss & spotify & obsidiantui & smostui
 
 # Without this, xdg-open doesn't work
 exec = systemctl --user import-environment PATH && systemctl --user restart xdg-desktop-portal.service
diff --git a/hosts/nixos/common/global/default.nix b/hosts/nixos/common/global/default.nix
index 0934727..18abaa5 100644
--- a/hosts/nixos/common/global/default.nix
+++ b/hosts/nixos/common/global/default.nix
@@ -54,4 +54,7 @@ in
 
     config.allowUnfree = true;
   };
+
+  # Root domain used throughout my config
+  satellite.dns.domain = "moonythm.dev";
 }
diff --git a/hosts/nixos/common/global/ports.nix b/hosts/nixos/common/global/ports.nix
index b94489b..f16797b 100644
--- a/hosts/nixos/common/global/ports.nix
+++ b/hosts/nixos/common/global/ports.nix
@@ -23,5 +23,6 @@
     forgejo = 8419;
     jupyterhub = 8420;
     guacamole = 8421;
+    syncthing = 8422;
   };
 }
diff --git a/hosts/nixos/common/optional/services/nginx.nix b/hosts/nixos/common/optional/services/nginx.nix
index 723a441..3e6b801 100644
--- a/hosts/nixos/common/optional/services/nginx.nix
+++ b/hosts/nixos/common/optional/services/nginx.nix
@@ -1,6 +1,5 @@
 {
   imports = [ ./acme.nix ];
-  satellite.nginx.domain = "moonythm.dev"; # Root domain used throughout my config
   services.nginx = {
     enable = true;
     recommendedGzipSettings = true;
diff --git a/hosts/nixos/common/optional/services/syncthing.nix b/hosts/nixos/common/optional/services/syncthing.nix
index 8e0a191..ba4c1eb 100644
--- a/hosts/nixos/common/optional/services/syncthing.nix
+++ b/hosts/nixos/common/optional/services/syncthing.nix
@@ -28,8 +28,15 @@ in
 
       extraOptions.options.crashReportingEnabled = false;
     };
+
+    guiAddress = "127.0.0.1:${toString config.satellite.ports.syncthing}";
+    settings.gui.insecureSkipHostcheck = true;
   };
 
+  # Expose gui interface via nginx
+  satellite.nginx.at."syncthing.${config.networking.hostName}".port =
+    config.satellite.ports.syncthing;
+
   # Syncthing seems to leak memory, so we want to restart it daily.
   systemd.services.syncthing.serviceConfig.RuntimeMaxSec = "1d";
 
diff --git a/hosts/nixos/common/secrets.yaml b/hosts/nixos/common/secrets.yaml
index 291e79a..2d9d7dd 100644
--- a/hosts/nixos/common/secrets.yaml
+++ b/hosts/nixos/common/secrets.yaml
@@ -1,6 +1,6 @@
 wireless: ENC[AES256_GCM,data:Ib0PdBd2r/DPyE6Ah9NffT8Tw8c2y+seGFrE0e9GkyRaStdYMiiIlWCiaBO0u1HHaVV+2MQ33MnMdqyCGRlqGk45kl0GIwVR5iAiSYnobj/6wcse+kx/+5mzNOHXD1kJRGJBm5+SN9ntiGABNkQXJdn/Qoc/ukY1uaGe2nBeFKmGdD9JL7KfgdI5jYjQYyDbCL9JUszxkXNcplIRBAAy8JDaBVeo9HgI0QDIZToPKwuEeQoA9XzdimrjbCazlZy3ZvjAuoQXmrc1nIRHF5GabSRGTFTnTfcBeW2fGpUxmIhLyucn2DIQBXLm+RDdMLWoqcGbKiLVqKyUXck3ZZyoHMf2b9N52xMUwcS7,iv:ozkDwWmurWTD8TZHGvWL9Yh8cOrP1PzSBkz+1bBZybo=,tag:iGPjRaOoGRcOWJMweTL2yA==,type:str]
 pilot_password: ENC[AES256_GCM,data:PiKJCv5x68O9HFM4UvqLnsSPtqFslBLeAg67OkvFAbw7WaqbXh/p5SQblhPHcJ7jQDc4kI3XesOxruZrfJ0aZNDV1g7MWecgKg==,iv:EVs/m83Zfx2NRQMO52cF6pCe1ETpYfaR6lmXg2Na/DI=,tag:dl2x1aTsaTgtHEZYdW2lmg==,type:str]
-cloudflare_dns_api_token: ENC[AES256_GCM,data:SAIMCvKOpGb5g9s03Xapc08KpOgLI+qlT5oiH/uNGxV+9JFSX3nvmQ==,iv:HFKcmHRG4EEOuJ8gRD0ZWsE18SLaZjewMSLznboLUeI=,tag:z21GURSxvNmZ4qkbri9mDQ==,type:str]
+cloudflare_dns_api_token: ENC[AES256_GCM,data:QlLxQ/4AQsdqdWJC//FRgbMRqR0Ni51JgCDlyXfNe4pfPtiPs+Gb6Q==,iv:7SS+EzeHk0J1DzVvKxd40AuZUidV2asoQbSr5vyxl+U=,tag:T1KGXOsZ26sICYbrcmU8+w==,type:str]
 backup_password: ENC[AES256_GCM,data:Tu7ODTALfQLX7Mbo/BqiM6gaErGv07urwN1iHwGgurKWDuuE1h5NMV5J0cJqW6orTIloVtoZTJgSJ2lZlMcfUQ==,iv:78ha833ZzgEDChIuGjCMVA89U4qY9lWqUmfPCiiQeQM=,tag:u8KWw/060UVP+OOoPhbjRA==,type:str]
 sops:
     kms: []
@@ -35,8 +35,8 @@ sops:
             WFd4ZFNHWG5Cakw5cU9MRE9HWHQ4THMKr/S7v1Oj3zQziMtI/NuFVm6AaJF5JV5U
             sEr2nEptYFz4G6YL5psQGXHaKzQKBg+crgKRbYL4akhqT7pfYPC0bQ==
             -----END AGE ENCRYPTED FILE-----
-    lastmodified: "2024-05-29T22:07:18Z"
-    mac: ENC[AES256_GCM,data:HQJU1hZs8S4b8LAPdAg1/IuIX3VETXHrE/lKzODjCb/ndWV8Qh5v8OKg4X8xFw13PJpEeQqIznh6qplxMHJYGcYnUK/TSTP+399BZ3M0NLGWyF0vfFn1JIKu7zg8iHpi491/T+I6TDy5hp9+Y6V0sjpZ4pEzhZTwPW9t+NieSbQ=,iv:lNu0aLUO2P+2Mq7kVDGt6llshu5wgb++3VMX91w1a+8=,tag:WSoUh4XnRenvhb+vwLUpRg==,type:str]
+    lastmodified: "2024-07-08T00:25:56Z"
+    mac: ENC[AES256_GCM,data:v+p223kf9JLRMJ6moIpA5wZOemJY0+BSnX30MY8g28RBGaR+I7AbUHOrd+GUPAXLqwfqtrFdPt8pULT+fzuxL4wnlB9NPZxCYFMhSGGj8HysmDuytYXfSD1LZWD9fymE4KuyTZHv7I/coEM/iobbvutu9cmTKN05i1atjeh4B30=,iv:hPiQkvbeFjLyzTNoHMqqPikMPuDvT2X2iAo7JBlEpHY=,tag:fdHvvH+qPrv8UhwIA6aZSA==,type:str]
     pgp: []
     unencrypted_suffix: _unencrypted
     version: 3.8.1
diff --git a/hosts/nixos/lapetus/default.nix b/hosts/nixos/lapetus/default.nix
index 0d6eefd..e005019 100644
--- a/hosts/nixos/lapetus/default.nix
+++ b/hosts/nixos/lapetus/default.nix
@@ -1,4 +1,4 @@
-{
+{ config, ... }: {
   imports = [
     ../common/global
     ../common/users/pilot.nix
@@ -7,6 +7,7 @@
     ../common/optional/services/kanata.nix
     ../common/optional/services/nginx.nix
     ../common/optional/services/postgres.nix
+    ../common/optional/services/syncthing.nix
     ../common/optional/services/restic
 
     # ./services/commafeed.nix
@@ -30,7 +31,6 @@
     ./services/radicale.nix
     ./services/redlib.nix
     ./services/smos.nix
-    ./services/syncthing.nix
     ./services/vaultwarden.nix
     ./services/whoogle.nix
     ./services/zfs.nix
@@ -49,4 +49,18 @@
 
   # Bootloader
   boot.loader.systemd-boot.enable = true;
+
+  # Tailscale internal IP DNS records
+  satellite.dns.records = [
+    {
+      at = config.networking.hostName;
+      type = "A";
+      value = "100.93.136.59";
+    }
+    {
+      at = config.networking.hostName;
+      type = "AAAA";
+      value = "fd7a:115c:a1e0::e75d:883b";
+    }
+  ];
 }
diff --git a/hosts/nixos/lapetus/services/forgejo.nix b/hosts/nixos/lapetus/services/forgejo.nix
index 0b44804..0a29139 100644
--- a/hosts/nixos/lapetus/services/forgejo.nix
+++ b/hosts/nixos/lapetus/services/forgejo.nix
@@ -1,17 +1,12 @@
 { lib, config, ... }:
-let
-  port = config.satellite.ports.forgejo;
-  host = "git.moonythm.dev";
-  cfg = config.services.forgejo;
-in
 {
   sops.secrets.forgejo_mail_password = {
     sopsFile = ../secrets.yaml;
-    owner = cfg.user;
-    group = cfg.group;
+    owner = config.services.forgejo.user;
+    group = config.services.forgejo.group;
   };
 
-  satellite.cloudflared.at.${host}.port = port;
+  satellite.cloudflared.at.git.port = config.satellite.ports.forgejo;
 
   services.forgejo = {
     enable = true;
@@ -30,9 +25,9 @@ in
       default.APP_NAME = "moonforge";
 
       server = {
-        DOMAIN = host;
-        HTTP_PORT = port;
-        ROOT_URL = "https://${host}";
+        DOMAIN = config.satellite.cloudflared.at.git.host;
+        HTTP_PORT = config.satellite.cloudflared.at.git.port;
+        ROOT_URL = config.satellite.cloudflared.at.git.host.url;
         LANDING_PAGE = "prescientmoon"; # Make my profile the landing page
       };
 
diff --git a/hosts/nixos/lapetus/services/homer.nix b/hosts/nixos/lapetus/services/homer.nix
index 523bb91..b7a1883 100644
--- a/hosts/nixos/lapetus/services/homer.nix
+++ b/hosts/nixos/lapetus/services/homer.nix
@@ -55,7 +55,7 @@ in
               name = "Syncthing";
               subtitle = "File synchronization";
               logo = icon "syncthing.png";
-              url = "https://lapetus.syncthing.moonythm.dev";
+              url = "https://syncthing.lapetus.moonythm.dev";
             }
             {
               name = "Guacamole";
diff --git a/hosts/nixos/lapetus/services/jupyter.nix b/hosts/nixos/lapetus/services/jupyter.nix
index c3bcce2..7ab8db9 100644
--- a/hosts/nixos/lapetus/services/jupyter.nix
+++ b/hosts/nixos/lapetus/services/jupyter.nix
@@ -18,7 +18,7 @@ in
 
   services.jupyterhub = {
     enable = true;
-    port = config.satellite.ports.jupyterhub;
+    port = config.satellite.cloudflared.at.jupyter.port;
 
     jupyterhubEnv = appEnv;
     jupyterlabEnv = appEnv;
@@ -71,7 +71,7 @@ in
   };
   # }}}
   # {{{ Networking & storage
-  satellite.cloudflared.at."jupyter.moonythm.dev".port = config.services.jupyterhub.port;
+  satellite.cloudflared.at.jupyter.port = config.services.jupyterhub.port;
 
   environment.persistence."/persist/state".directories = [
     "/var/lib/${config.services.jupyterhub.stateDirectory}"
diff --git a/hosts/nixos/lapetus/services/microbin.nix b/hosts/nixos/lapetus/services/microbin.nix
index 2d186c0..9654f58 100644
--- a/hosts/nixos/lapetus/services/microbin.nix
+++ b/hosts/nixos/lapetus/services/microbin.nix
@@ -1,11 +1,7 @@
 { config, lib, ... }:
-let
-  port = config.satellite.ports.microbin;
-  host = "bin.moonythm.dev";
-in
 {
   sops.secrets.microbin_env.sopsFile = ../secrets.yaml;
-  satellite.cloudflared.at.${host}.port = port;
+  satellite.cloudflared.at.bin.port = config.satellite.ports.microbin;
 
   services.microbin = {
     enable = true;
@@ -16,8 +12,8 @@ in
     settings = {
       # High level settings
       MICROBIN_ADMIN_USERNAME = "prescientmoon";
-      MICROBIN_PORT = toString port;
-      MICROBIN_PUBLIC_PATH = "https://bin.moonythm.dev/";
+      MICROBIN_PORT = toString config.satellite.cloudflared.at.bin.port;
+      MICROBIN_PUBLIC_PATH = config.satellite.cloudflared.at.bin.url;
       MICROBIN_DEFAULT_EXPIRY = "1week";
 
       # Disable online features
diff --git a/hosts/nixos/lapetus/services/pounce.nix b/hosts/nixos/lapetus/services/pounce.nix
index 6be9a44..2e54995 100644
--- a/hosts/nixos/lapetus/services/pounce.nix
+++ b/hosts/nixos/lapetus/services/pounce.nix
@@ -28,9 +28,22 @@ in
   # Configure pounce
   services.pounce = {
     enable = true;
-    externalHost = "irc.moonythm.dev";
-    bindHost = "irc.moonythm.dev";
+    externalHost = "irc.${config.satellite.dns.domain}";
+    bindHost = "irc.${config.satellite.dns.domain}";
     certDir = "/var/lib/acme/wildcard-irc.moonythm.dev";
     networks.tilde.config = config.sops.templates."pounce-tilde.cfg".path;
   };
+
+  satellite.dns.records = [
+    {
+      type = "CNAME";
+      at = "*.irc";
+      to = "irc";
+    }
+    {
+      type = "CNAME";
+      at = "irc";
+      to = config.networking.hostName;
+    }
+  ];
 }
diff --git a/hosts/nixos/lapetus/services/syncthing.nix b/hosts/nixos/lapetus/services/syncthing.nix
deleted file mode 100644
index 6b50103..0000000
--- a/hosts/nixos/lapetus/services/syncthing.nix
+++ /dev/null
@@ -1,12 +0,0 @@
-{ config, ... }:
-let port = 8384;
-in
-{
-  services.syncthing = {
-    settings.folders = { };
-    guiAddress = "127.0.0.1:${toString port}";
-    settings.gui.insecureSkipHostcheck = true;
-  };
-
-  satellite.nginx.at."lapetus.syncthing".port = port;
-}
diff --git a/hosts/nixos/tethys/default.nix b/hosts/nixos/tethys/default.nix
index 236fec4..6385482 100644
--- a/hosts/nixos/tethys/default.nix
+++ b/hosts/nixos/tethys/default.nix
@@ -1,4 +1,4 @@
-{ lib, pkgs, ... }: {
+{ config, lib, pkgs, ... }: {
   # {{{ Imports
   imports = [
     ../common/global
@@ -72,4 +72,18 @@
   programs.dconf.enable = true;
   services.gnome.evolution-data-server.enable = true;
   services.gnome.gnome-online-accounts.enable = true;
+
+  # Tailscale internal IP DNS records
+  satellite.dns.records = [
+    # {
+    #   at = config.networking.hostName;
+    #   type = "A";
+    #   value = "100.93.136.59";
+    # }
+    # {
+    #   at = config.networking.hostName;
+    #   type = "AAAA";
+    #   value = "fd7a:115c:a1e0::e75d:883b";
+    # }
+  ];
 }
diff --git a/modules/common/octodns.nix b/modules/common/octodns.nix
new file mode 100644
index 0000000..2d93246
--- /dev/null
+++ b/modules/common/octodns.nix
@@ -0,0 +1,46 @@
+{ config, pkgs, lib, ... }:
+let
+  format = pkgs.formats.yaml { };
+  cfg = config.satellite.dns;
+in
+{
+  options.satellite.dns.octodns = lib.mkOption {
+    description = "Derivation building a directory containing all the zone files";
+    type = lib.types.path;
+  };
+
+  config.satellite.dns.octodns =
+    let
+      grouped = builtins.groupBy (entry: entry.zone) cfg.records;
+      cpLines = lib.mapAttrsToList
+        (zone: group:
+          let
+            grouped = builtins.groupBy (entry: entry.at) group;
+            contents = lib.mapAttrs
+              (at: entries: lib.lists.forEach entries
+                (entry:
+                  let
+                    content =
+                      if builtins.typeOf entry.value == "list"
+                      then { values = entry.value; }
+                      else { inherit (entry) value; };
+                    cloudflare =
+                      if entry.enableCloudflareProxy then {
+                        octodns.cloudflare.proxied = true;
+                      } else { };
+                  in
+                  { inherit (entry) ttl type; }
+                  // content // cloudflare
+                ))
+              grouped;
+            file = format.generate "${zone}.yaml" contents;
+          in
+          "cp ${file} $out/${zone}.yaml"
+        )
+        grouped;
+    in
+    pkgs.runCommand "octodns-zones" { } ''
+      mkdir $out
+      ${lib.concatStringsSep "\n" cpLines}
+    '';
+}
diff --git a/modules/nixos/cloudflared.nix b/modules/nixos/cloudflared.nix
index 8c928e8..9c6f438 100644
--- a/modules/nixos/cloudflared.nix
+++ b/modules/nixos/cloudflared.nix
@@ -8,22 +8,44 @@ in
       description = "Cloudflare tunnel id to use for the `satellite.cloudflared.at` helper";
     };
 
+    domain = lib.mkOption {
+      description = "Root domain to use as a default for configurations.";
+      type = lib.types.str;
+      default = config.satellite.dns.domain;
+    };
+
     at = lib.mkOption {
       description = "List of hosts to set up ingress rules for";
       default = { };
-      type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
+      type = lib.types.attrsOf (lib.types.submodule ({ name, config, ... }: {
         options = {
+          subdomain = lib.mkOption {
+            description = ''
+              Subdomain to use for host generation. 
+              Only required if `host` is not set manually.
+            '';
+            type = lib.types.str;
+            default = name;
+          };
+
           port = lib.mkOption {
-            type = lib.types.port;
             description = "Localhost port to point the tunnel at";
+            type = lib.types.port;
           };
 
           host = lib.mkOption {
-            default = name;
-            type = lib.types.str;
             description = "Host to direct traffic from";
+            type = lib.types.str;
+            default = "${config.subdomain}.${cfg.domain}";
+          };
+
+          url = lib.mkOption {
+            description = "External https url used to access this host";
+            type = lib.types.str;
           };
         };
+
+        config.url = "https://${config.host}";
       }));
     };
   };
@@ -34,4 +56,13 @@ in
       value = "http://localhost:${toString port}";
     })
     cfg.at;
+
+  config.satellite.dns.records =
+    let mkDnsRecord = { subdomain, ... }: {
+      type = "CNAME";
+      at = subdomain;
+      zone = cfg.domain;
+      value = "${cfg.tunnel}.cfargotunnel.com.";
+    };
+    in lib.attrsets.mapAttrsToList (_: mkDnsRecord) cfg.at;
 }
diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix
index 4f811fa..e4e197f 100644
--- a/modules/nixos/default.nix
+++ b/modules/nixos/default.nix
@@ -7,4 +7,6 @@
   nginx = import ./nginx.nix;
   pilot = import ./pilot.nix;
   pounce = import ./pounce.nix;
+  dns = import ./dns.nix;
+  dns-assertions = import ./dns-assertions.nix;
 }
diff --git a/modules/nixos/dns-assertions.nix b/modules/nixos/dns-assertions.nix
new file mode 100644
index 0000000..d1409f9
--- /dev/null
+++ b/modules/nixos/dns-assertions.nix
@@ -0,0 +1,17 @@
+# This must only be loaded on actual Nixos, otherwise `assertions`
+# won't be defined when running `evaluateModules`.
+{ config, ... }:
+let cfg = config.satellite.dns;
+in
+{
+  config.assertions =
+    let assertProperToUsage = config:
+      {
+        assertion = (config.to == null) || (config.type == "CNAME");
+        message = ''
+          The option `satellite.dns.records[*].to` can only be used with `CNAME` records.
+          This was not the case for ${config.type} record at ${config.at}.${config.zone}.
+        '';
+      };
+    in builtins.map assertProperToUsage cfg.records;
+}
diff --git a/modules/nixos/dns.nix b/modules/nixos/dns.nix
new file mode 100644
index 0000000..edaad21
--- /dev/null
+++ b/modules/nixos/dns.nix
@@ -0,0 +1,59 @@
+{ config, pkgs, lib, ... }:
+let
+  format = pkgs.formats.yaml { };
+  cfg = config.satellite.dns;
+in
+{
+  options.satellite.dns = {
+    domain = lib.mkOption {
+      description = "Default zone to include records in";
+      type = lib.types.str;
+    };
+
+    records = lib.mkOption {
+      description = "List of records to create";
+      default = [ ];
+      type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
+        options = {
+          at = lib.mkOption {
+            description = "Subdomain to use for entry";
+            type = lib.types.nullOr lib.types.str;
+          };
+
+          zone = lib.mkOption {
+            description = "Zone this record is a part of";
+            type = lib.types.str;
+            default = cfg.domain;
+          };
+
+          type = lib.mkOption {
+            type = lib.types.enum [ "A" "AAAA" "TXT" "CNAME" "MX" ];
+            description = "The type of the DNS record";
+          };
+
+          to = lib.mkOption {
+            type = lib.types.nullOr lib.types.str;
+            description = "Shorthand for CNMAE-ing to a subdomain of the given zone";
+            default = null;
+          };
+
+          value = lib.mkOption {
+            type = format.type;
+            description = "The value assigned to the record, in octodns format";
+          };
+
+          ttl = lib.mkOption {
+            type = lib.types.int;
+            description = "The TTL assigned to the record";
+            default = 300;
+          };
+
+          enableCloudflareProxy = lib.mkEnableOption "proxying using cloudflare";
+        };
+
+        config.value = lib.mkIf (config.type == "CNAME" && config.to != null)
+          "${config.to}.${config.zone}.";
+      }));
+    };
+  };
+}
diff --git a/modules/nixos/nginx.nix b/modules/nixos/nginx.nix
index 6a2fa6b..9066e62 100644
--- a/modules/nixos/nginx.nix
+++ b/modules/nixos/nginx.nix
@@ -6,24 +6,28 @@ in
     domain = lib.mkOption {
       description = "Root domain to use as a default for configurations.";
       type = lib.types.str;
+      default = config.satellite.dns.domain;
     };
 
     at = lib.mkOption {
       description = "Per-subdomain nginx configuration";
       type = lib.types.attrsOf (lib.types.submodule ({ name, config, ... }: {
-        options.name = lib.mkOption {
-          description = "Attribute name leading to this submodule";
+        options.subdomain = lib.mkOption {
+          description = ''
+            Subdomain to use for host generation. 
+            Only required if `host` is not set manually.
+          '';
           type = lib.types.str;
+          default = name;
         };
 
-        config.name = name;
-
         options.host = lib.mkOption {
           description = "Host to route requests from";
           type = lib.types.str;
-          default = "${name}.${cfg.domain}";
         };
 
+        config.host = "${config.subdomain}.${cfg.domain}";
+
         options.url = lib.mkOption {
           description = "External https url used to access this host";
           type = lib.types.str;
@@ -53,8 +57,8 @@ in
         {
           assertion = (config.port == null) == (config.files != null);
           message = ''
-            Precisely one of the options 'satellite.nginx.at.${config.name}.port'
-            and 'satellite.nginx.at.${config.name}.files' must be specified.
+            Precisely one of the options 'satellite.nginx.at.${config.subdomain}.port'
+            and 'satellite.nginx.at.${config.subdomain}.files' must be specified.
           '';
         };
       in lib.mapAttrsToList (_: assertSingleTarget) cfg.at;
@@ -81,5 +85,14 @@ in
           } // extra;
       };
       in lib.attrsets.mapAttrs' (_: mkNginxConfig) cfg.at;
+
+    satellite.dns.records =
+      let mkDnsRecord = { subdomain, ... }: {
+        type = "CNAME";
+        zone = cfg.domain;
+        at = subdomain;
+        to = config.networking.hostName;
+      };
+      in lib.attrsets.mapAttrsToList (_: mkDnsRecord) cfg.at;
   };
 }
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 198747f..9763bca 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,7 +1,7 @@
 # Custom packages, that can be defined similarly to ones from nixpkgs
 # You can build them using 'nix build .#example' or (legacy) 'nix-build -A example'
 
-{ pkgs ? (import ../nixpkgs.nix) { }, upkgs ? pkgs }:
+{ pkgs ? (import ../nixpkgs.nix) { }, upkgs ? pkgs, ... }:
 let plymouthThemes = pkgs.callPackage (import ./plymouth-themes.nix) { }; in
 {
   # example = pkgs.callPackage (import ./example.nix) {};
diff --git a/pkgs/octodns-cloudflare.nix b/pkgs/octodns-cloudflare.nix
new file mode 100644
index 0000000..e960238
--- /dev/null
+++ b/pkgs/octodns-cloudflare.nix
@@ -0,0 +1,45 @@
+{ lib
+, buildPythonPackage
+, fetchFromGitHub
+, octodns
+, pytestCheckHook
+, pythonOlder
+, dnspython
+, setuptools
+, requests
+, requests-mock
+}:
+
+buildPythonPackage rec {
+  pname = "octodns-cloudflare";
+  version = "unstable-2024-05-31";
+  pyproject = true;
+
+  disabled = pythonOlder "3.8";
+
+  src = fetchFromGitHub {
+    owner = "octodns";
+    repo = "octodns-cloudflare";
+    rev = "3c01938e280767f433eb276a75d6b02c152c02af";
+    sha256 = "1dnvyvf6mlpqcsrj11192li2mhqfs8w6pvaqmsy3jsqjqczmgmf5";
+  };
+
+  nativeBuildInputs = [
+    setuptools
+  ];
+
+  propagatedBuildInputs = [
+    octodns
+    dnspython
+    requests
+  ];
+
+  env.OCTODNS_RELEASE = 1;
+
+  pythonImportsCheck = [ "octodns_cloudflare" ];
+
+  nativeCheckInputs = [
+    pytestCheckHook
+    requests-mock
+  ];
+}
diff --git a/scripts/dns/dns.txt b/scripts/dns/dns.txt
index 670816a..ee9445d 100644
--- a/scripts/dns/dns.txt
+++ b/scripts/dns/dns.txt
@@ -34,7 +34,7 @@ yt                 IN CNAME  lapetus
 *.irc              IN CNAME  irc
 
 ; Tunnel used by lapetus
-tunnel.lapetus     IN CNAME  347d9ead-a523-4f8b-bca7-3066e31e2952.cfargotunnel.com
+tunnel.lapetus     IN CNAME  347d9ead-a523-4f8b-bca7-3066e31e2952.cfargotunnel.com.
 
 ; lapetus services using cloudflare tunnels
 bin                IN CNAME  tunnel.lapetus