summaryrefslogtreecommitdiff
path: root/hosts/ahmed/local-dns
diff options
context:
space:
mode:
authorLinnnus <[email protected]>2025-03-16 13:22:12 +0100
committerLinnnus <[email protected]>2025-03-16 13:31:07 +0100
commit617dd624903cdb23951f1484c19bc1574b10fa74 (patch)
treebd61b9ea0fd631372e6909ee3f90beec2e272362 /hosts/ahmed/local-dns
parentd7746e79e33eac666e2fdf4dfe9862a0a8c736a8 (diff)
ahmed: Add certificates for local DNS
Diffstat (limited to 'hosts/ahmed/local-dns')
-rw-r--r--hosts/ahmed/local-dns/certificates.nix52
-rw-r--r--hosts/ahmed/local-dns/default.nix41
-rw-r--r--hosts/ahmed/local-dns/dns-resolver.nix57
3 files changed, 150 insertions, 0 deletions
diff --git a/hosts/ahmed/local-dns/certificates.nix b/hosts/ahmed/local-dns/certificates.nix
new file mode 100644
index 0000000..25784c1
--- /dev/null
+++ b/hosts/ahmed/local-dns/certificates.nix
@@ -0,0 +1,52 @@
+# Getting HTTPS to work for local domains is pretty hard. The approach I've
+# gone with is to request a wildcard domain for `*.rumpenettet.linus.onl`. We
+# can do this because `linus.onl` is a public domain which we have control
+# over.
+#
+# This module requests a certificate from letsencrypt using DNS-01
+# verification. I have an API token which can modify DNS records for
+# `linus.onl`. This is how Lego (i.e. `security.acme`) proves domain ownership
+# when renewing the certificate.
+#
+# Any services running under `rumpenettet.local.onl` and use this certificate.
+# For NGINX that happens via `useACMEHost` and one of the options that enable
+# HTTPS.
+{
+ lib,
+ config,
+ ...
+}: {
+ security.acme = {
+ certs.${config.linus.local-dns.domain} = {
+ dnsProvider = "cloudflare";
+ dnsResolver = "1.1.1.1:53";
+ environmentFile = config.age.secrets.cloudflare-acme-token.path;
+ dnsPropagationCheck = true;
+ domain = "*.${config.linus.local-dns.domain}";
+
+ group = config.services.nginx.group;
+ reloadServices = ["nginx"];
+ };
+ };
+
+ # This file contains the variables that Lego needs to authenticate to
+ # Cloudflare. This is how we prove ownership of the domain.
+ #
+ # See: https://go-acme.github.io/lego/dns/cloudflare/
+ # See: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#EnvironmentFile=
+ age.secrets.cloudflare-acme-token.file = ../../../secrets/cloudflare-acme-token.env.age;
+
+ # Use the certificate for each subdomain in NGINX. Luckily, we can be pretty
+ # opinionated since this isn't reusable logic.
+ #
+ # NOTE: This assumes that each subdomain *has* an NGINX virtual host, which
+ # may not be the case in the future.
+ services.nginx.virtualHosts = let
+ virtualHostConfig = subdomain:
+ lib.nameValuePair "${subdomain}.${config.linus.local-dns.domain}" {
+ forceSSL = true;
+ useACMEHost = config.linus.local-dns.domain; # Same as security.acme.certs.${...} above.
+ };
+ in
+ builtins.listToAttrs (map virtualHostConfig config.linus.local-dns.subdomains);
+}
diff --git a/hosts/ahmed/local-dns/default.nix b/hosts/ahmed/local-dns/default.nix
new file mode 100644
index 0000000..6ac96e9
--- /dev/null
+++ b/hosts/ahmed/local-dns/default.nix
@@ -0,0 +1,41 @@
+# This module sets up local DNS so that services on this host become visible to devices on LAN.
+# The work is split in submodules, coordinated via the options set in this module:
+#
+# - certificates.nix: Get certs for HTTPS (surprisingly hard)
+# - dns-resolver.nix: Make local domains visible to devices
+#
+# See the files for more info on how each part works.
+{lib, ...}: {
+ imports = [
+ ./certificates.nix
+ ./dns-resolver.nix
+ ];
+
+ options = {
+ linus.local-dns = {
+ domain = lib.mkOption {
+ description = ''
+ A (sub)domain we have ownership over.
+
+ To devices using our DNS cache (on port 53), it will look like this
+ computer has the authority over that domain. It should not be used to
+ server anything public, as that would then be overwritten.
+ '';
+ type = lib.types.nonEmptyStr;
+ };
+
+ # TODO: This assumes that all subdomains are located on this host. What about our NAS? Be more flexible.
+ subdomains = lib.mkOption {
+ description = ''
+ List of subdomains that to {option}`domain` which are in use.
+ '';
+ type = with lib.types; listOf nonEmptyStr;
+ default = [];
+ };
+ };
+ };
+
+ config = {
+ linus.local-dns.domain = "rumpenettet.linus.onl";
+ };
+}
diff --git a/hosts/ahmed/local-dns/dns-resolver.nix b/hosts/ahmed/local-dns/dns-resolver.nix
new file mode 100644
index 0000000..1954a52
--- /dev/null
+++ b/hosts/ahmed/local-dns/dns-resolver.nix
@@ -0,0 +1,57 @@
+# This module creates a local DNS server which provides "split horizon DNS".
+#
+# It only serves devices on the LAN (see `services.dnscache.clientIps`) and for
+# those, it claims to have authority over the domain set in `config.linus.local-dns.domain`.
+#
+# See: https://www.fefe.de/djbdns/split-horizon
+{
+ config,
+ metadata,
+ lib,
+ ...
+}: {
+ services.dnscache = {
+ enable = true;
+ clientIps = [
+ "192.168" # LAN
+ "127.0.0.1" # Local connections
+ ];
+
+ domainServers = {
+ # Forward any requests to the split domain to our local, authoritative name server.
+ ${config.linus.local-dns.domain} = ["127.0.0.1"];
+ };
+ };
+
+ # Authoritative name server which claims ownership of the split domain.
+ services.tinydns = {
+ enable = true;
+
+ # We will only listen for internal queries from the DNS cache.
+ ip = "127.0.0.1";
+
+ # Here we publish all the services we want.
+ data = let
+ subdomainToARecord = subdomain: "=${subdomain}.${config.linus.local-dns.domain}:${metadata.hosts.ahmed.ipAddress}";
+ ARecords = lib.concatMapStringsSep "\n" subdomainToARecord config.linus.local-dns.subdomains;
+ in ''
+ # We are authoritative over ${config.linus.local-dns.domain}.
+ # Here we simply identify as localhost, as only the local dnscache instance will ever see this (I think).
+ .${config.linus.local-dns.domain}:127.0.0.1:a
+ # Next, we link all the subdomains to our LAN IP.
+ ${ARecords}
+ '';
+ };
+
+ # Allow other devices on LAN to interact with us. In the router's DHCP
+ # settings, I have set ahmed's IP as the primary DNS server. This will make
+ # all clients (which respect DNS from DHCP) use ahmed if he's online.
+ #
+ # Notably, the NAT on the router does not route external trafic here; we are
+ # a non-authoritative DNS resolver, so we don't want to service the global
+ # internet.
+ networking.firewall = {
+ allowedTCPPorts = [53];
+ allowedUDPPorts = [53];
+ };
+}