diff options
author | Linnnus <[email protected]> | 2025-03-16 13:22:12 +0100 |
---|---|---|
committer | Linnnus <[email protected]> | 2025-03-16 13:31:07 +0100 |
commit | 617dd624903cdb23951f1484c19bc1574b10fa74 (patch) | |
tree | bd61b9ea0fd631372e6909ee3f90beec2e272362 | |
parent | d7746e79e33eac666e2fdf4dfe9862a0a8c736a8 (diff) |
ahmed: Add certificates for local DNS
-rw-r--r-- | hosts/ahmed/configuration.nix | 2 | ||||
-rw-r--r-- | hosts/ahmed/dns/default.nix | 33 | ||||
-rw-r--r-- | hosts/ahmed/local-dns/certificates.nix | 52 | ||||
-rw-r--r-- | hosts/ahmed/local-dns/default.nix | 41 | ||||
-rw-r--r-- | hosts/ahmed/local-dns/dns-resolver.nix | 57 | ||||
-rw-r--r-- | hosts/ahmed/torrenting/reverse-proxy.nix | 23 | ||||
-rw-r--r-- | secrets/cloudflare-acme-token.env.age | bin | 0 -> 1038 bytes | |||
-rw-r--r-- | secrets/cloudflare-acme-token.env.example | 2 | ||||
-rw-r--r-- | secrets/secrets.nix | 1 |
9 files changed, 161 insertions, 50 deletions
diff --git a/hosts/ahmed/configuration.nix b/hosts/ahmed/configuration.nix index c36f607..4386460 100644 --- a/hosts/ahmed/configuration.nix +++ b/hosts/ahmed/configuration.nix @@ -23,7 +23,7 @@ ./dyndns ./minecraft ./nginx - ./dns + ./local-dns ]; # Create the main user. diff --git a/hosts/ahmed/dns/default.nix b/hosts/ahmed/dns/default.nix deleted file mode 100644 index f590f2f..0000000 --- a/hosts/ahmed/dns/default.nix +++ /dev/null @@ -1,33 +0,0 @@ -{metadata, ...}: { - services.dnscache = { - enable = true; - clientIps = [ - "192.168" # LAN - "127.0.0.1" # Local connections - ]; - - domainServers = { - "internal" = ["127.0.0.1"]; - }; - }; - - services.tinydns = { - enable = true; - - # We will only listen for internal queries from the DNS cache. - ip = "127.0.0.1"; - - data = '' - .internal:127.0.0.1:a - =ahmed.internal:${metadata.hosts.ahmed.ipAddress} - =muhammed.internal:${metadata.hosts.muhammed.ipAddress} - =jellyfin.internal:${metadata.hosts.ahmed.ipAddress} - =qbittorrent.internal:${metadata.hosts.ahmed.ipAddress} - ''; - }; - - networking.firewall = { - allowedTCPPorts = [53]; - allowedUDPPorts = [53]; - }; -} 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]; + }; +} diff --git a/hosts/ahmed/torrenting/reverse-proxy.nix b/hosts/ahmed/torrenting/reverse-proxy.nix index 1cee18f..7f8db3e 100644 --- a/hosts/ahmed/torrenting/reverse-proxy.nix +++ b/hosts/ahmed/torrenting/reverse-proxy.nix @@ -1,20 +1,8 @@ # This module configures a reverse proxy for the various services that are # exposed to the internet. -{ - pkgs, - config, - lib, - ... -}: let - baseDomain = "internal"; - qbDomain = "qbittorrent.${baseDomain}"; - jellyfinDomain = "jellyfin.${baseDomain}"; - +{config, ...}: let # The internal port where qBittorrents web UI will be served. qbWebUiPort = 8082; - - # Whether to use ACME/Letsencrypt to get free certificates. - useACME = true; in { services.qbittorrent = { openFirewall = false; @@ -32,14 +20,14 @@ in { # Use NGINX as a reverse proxy. services.nginx = { - virtualHosts.${qbDomain} = { + virtualHosts."qbittorrent.${config.linus.local-dns.domain}" = { locations."/" = { proxyPass = "http://localhost:${toString qbWebUiPort}"; recommendedProxySettings = true; }; }; - virtualHosts.${jellyfinDomain} = { + virtualHosts."jellyfin.${config.linus.local-dns.domain}" = { locations."/" = { # This is the "static port" of the HTTP web interface. # @@ -62,5 +50,8 @@ in { }; }; - # See also `hosts/ahmed/dns/default.nix`. + linus.local-dns.subdomains = [ + "qbittorrent" + "jellyfin" + ]; } diff --git a/secrets/cloudflare-acme-token.env.age b/secrets/cloudflare-acme-token.env.age Binary files differnew file mode 100644 index 0000000..cd761fe --- /dev/null +++ b/secrets/cloudflare-acme-token.env.age diff --git a/secrets/cloudflare-acme-token.env.example b/secrets/cloudflare-acme-token.env.example new file mode 100644 index 0000000..c8c10d7 --- /dev/null +++ b/secrets/cloudflare-acme-token.env.example @@ -0,0 +1,2 @@ +CF_DNS_API_TOKEN=7qv9jF93ls9Vww8wi3d7yRtWtki8FLbRQj2-OKSX diff --git a/secrets/secrets.nix b/secrets/secrets.nix index a747bcb..45a40a5 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -6,6 +6,7 @@ let muhammedKey = metadata.hosts.muhammed.sshPubKey; in { "cloudflare-ddns-token.env.age".publicKeys = [muhammedKey ahmedKey]; + "cloudflare-acme-token.env.age".publicKeys = [muhammedKey ahmedKey]; "duksebot.env.age".publicKeys = [muhammedKey ahmedKey]; "mullvad-wg.key.age".publicKeys = [muhammedKey ahmedKey]; "wraaath-sftp-password.txt.age".publicKeys = [muhammedKey ahmedKey]; |