From 9ceddb33272e5fca6382c1b4dec2074bd1167738 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sat, 18 Jul 2026 23:04:53 +0200 Subject: Simplify config and add AdGuard DoH/DoT support - Replace the hand-rolled Podman OCI container for Actual Budget with the native services.actual module (available in the pinned nixpkgs release); the container never actually had a backend enabled, so it likely never ran. - Collapse cgit's Caddy routing to a single reverse proxy - the smart-HTTP git backend was already served on the same nginx vhost/port as cgit itself, so the separate /git/* -> 8085 route was dead and pointed at a port nothing listened on. - Drop the unused kvm-intel kernel module from the guest hardware profile, and rename disko's misleadingly-named ESP partition (it's ext4, not a real EFI System Partition). - Point common.nix's disabled autoUpgrade flake URL at this repo's own cgit hosting instead of a generic GitHub placeholder. - Add AdGuard Home DNS-over-TLS (853) and DNS-over-HTTPS (8443) support, backed by an independent ACME certificate (modules/acme.nix) issued via a webroot Caddy serves on port 80. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 58 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 20 deletions(-) (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md index 35a6923..c11918d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,30 +76,48 @@ service module: - `sops.nix` — sops-nix wiring. The host's own age key is derived at boot from `/etc/ssh/ssh_host_ed25519_key`, so no separate host key material needs to be provisioned. Declares which secrets exist, their owning user/group, and file mode. -- `caddy.nix` — the only thing that binds to 80/443. Reverse-proxies each subdomain to a - service's localhost port. All application ports (3000, 5006, 8080, 8085/8086) are bound to - `127.0.0.1` and never exposed directly — Caddy is the sole public HTTP(S) entry point and - handles Let's Encrypt automatically. -- `adguard.nix` — AdGuard Home; DNS on `0.0.0.0:53`, web UI on `127.0.0.1:3000`. Runs with - `mutableSettings = false` (fully declarative config), which creates a specific problem: the - module's own `ExecStartPre` overwrites the state file with a config containing a literal - `REPLACED_AT_ACTIVATION` placeholder on every start. A second `ExecStartPre`, ordered with - `lib.mkAfter`, substitutes in the real bcrypt hash from the sops secret immediately before - `ExecStart`. It also uses a static (non-`DynamicUser`) system user, because sops-nix chowns the - secret to that user during activation while the service is stopped — a `DynamicUser` doesn't - exist at that point. If you touch this module, preserve that ordering; it's the whole point of - the file. +- `acme.nix` — an independent `security.acme` certificate for `dns.karanj.com`, used only by + AdGuard's own DoH/DoT TLS listener (not by Caddy, which gets its HTTPS for the same domain + through its own separate, built-in ACME client). Issued via HTTP-01 against a webroot that + `caddy.nix` serves on port 80 for that one vhost. The cert directory is group-owned by + `adguardhome` (`security.acme.certs..group`) so the service can read it without running + as root, and `reloadServices` restarts `adguardhome` on renewal. +- `caddy.nix` — the main thing that binds to 80/443 (see `adguard.nix` below for the one + exception). Reverse-proxies each subdomain to a service's localhost port; the `dns.karanj.com` + vhost additionally serves the ACME HTTP-01 webroot for `acme.nix`. Application ports (5006, + 8080, 8086) are bound to `127.0.0.1` and never exposed directly — Caddy is the public HTTP(S) + entry point for everything except AdGuard's encrypted-DNS ports, and handles Let's Encrypt + automatically for its own certs. +- `adguard.nix` — AdGuard Home; plain DNS on `0.0.0.0:53`, DNS-over-TLS on `0.0.0.0:853`, + DNS-over-HTTPS on `0.0.0.0:8443` (path `/dns-query`), web UI on `0.0.0.0:3000` (port 3000 is + never opened in the firewall, so it's still effectively internal-only, reached via Caddy). + `http.address` binds `0.0.0.0` rather than `127.0.0.1` specifically because AdGuard's + HTTPS/DoH listener reuses that same bind host internally (confirmed from AdGuard's source) — + narrowing it back to loopback would silently make DoH unreachable. TLS for DoH/DoT uses the + cert from `acme.nix`; `systemd.services.adguardhome.after`/`wants` on `acme-dns.karanj.com.service` + ensures the cert exists before AdGuard's first start. + Runs with `mutableSettings = false` (fully declarative config), which creates a specific + problem: the module's own `ExecStartPre` overwrites the state file with a config containing a + literal `REPLACED_AT_ACTIVATION` placeholder on every start. A second `ExecStartPre`, ordered + with `lib.mkAfter`, substitutes in the real bcrypt hash from the sops secret immediately before + `ExecStart`. It also uses a static (non-`DynamicUser`) system user, because sops-nix (and now + the acme cert's group ownership) need a stable UID/GID to chown to during activation, while a + `DynamicUser` only exists while its service is actually running. If you touch this module, + preserve that ordering; it's the whole point of the file. - `miniflux.nix` — RSS reader; admin account seeded from a sops credentials file (`ADMIN_USERNAME` / `ADMIN_PASSWORD` format) on first run. Uses the NixOS module's own PostgreSQL integration. -- `actual.nix` — Actual Budget, run as a Podman OCI container (`virtualisation.oci-containers`) - rather than a native NixOS module, because none exists for this nixpkgs version. No secrets - involved; the server password is set interactively on first browser visit. +- `actual.nix` — Actual Budget via the native `services.actual` module (ships in this flake's + pinned nixpkgs). No secrets involved; the server password is set interactively on first + browser visit. - `cgit.nix` — git hosting. Three-way access split: web browsing is public/unauthenticated via cgit, `git clone`/`pull` over HTTPS is public/read-only via `git-http-backend` + fcgiwrap, and - `git push` is SSH-only through a dedicated `git` system user with its own authorized keys. Both - cgit and git-http-backend actually run behind a minimal localhost-only nginx instance, which - Caddy then proxies to — nginx exists here purely as an implementation detail for cgit/fcgiwrap, - not as the public-facing server. + `git push` is SSH-only through a dedicated `git` system user with its own authorized keys. cgit + browsing and git-http-backend are both served by the *same* localhost-only nginx vhost + (127.0.0.1:8086) — nginx tells the two apart by matching the request path (a regex for + `info/refs`/`git-upload-pack`), not by a separate port — and Caddy reverse-proxies everything + for `git.karanj.com` straight to that one vhost. Only `git-upload-pack` is wired up, so push + over HTTP is impossible regardless of nginx config; push stays SSH-only structurally, not just + by convention. **Secrets (`secrets/secrets.yaml` + `.sops.yaml`)** — encrypted with sops using age keys. Three recipients: two personal keys (`einstein`, `galileo` — the user's two machines) plus the host's -- cgit v1.3.1