aboutsummaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md58
1 files changed, 38 insertions, 20 deletions
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.<name>.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