docs: condense comments across the repo

Comments had drifted into multi-paragraph narrative (git commit
lineage, debugging stories, restated code) in several hot spots
(scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix).
Trim every comment to its load-bearing "why" — gotchas, safety
warnings, and non-obvious rationale survive verbatim in substance,
just tightened to 1-2 sentences; historical narrative and anything
already covered in CLAUDE.md is cut. No code/logic changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
This commit is contained in:
2026-09-18 21:36:30 +02:00
co-authored by Claude Sonnet 5
parent 3899290c5b
commit 6f24ab69ad
47 changed files with 1051 additions and 1965 deletions
+22 -62
View File
@@ -43,30 +43,19 @@
# default via fe80::1 dev eth0 metric 1024 onlink
networking.defaultGateway6 = { address = "fe80::1"; interface = "eth0"; };
networking.nameservers = [ "9.9.9.9" "1.1.1.1" "2620:fe::fe" ];
# Addressing is fully static above, but netcup's router still sends periodic
# RAs on this segment; the kernel then tries (and fails, since the static
# route already exists) to install its own default route from them, spamming
# "ndisc_router_discovery failed to add default route" on the console. Stop
# it from processing RAs on eth0 at all rather than just live with the noise.
# netcup's router still sends periodic RAs on this segment despite fully static
# addressing, spamming "ndisc_router_discovery failed to add default route" on the
# console. Stop processing RAs on eth0 entirely instead of living with the noise.
boot.kernel.sysctl."net.ipv6.conf.eth0.accept_ra" = 0;
# ---- Local split-DNS stub ----
# neptun must NOT take the tailnet's DNS: headscale points every node at
# pihole on mercury, and making a public reverse proxy's name resolution
# depend on a Pi behind a domestic line would take ACME renewals — and so
# the certs for the control server every node needs — down with it. It is
# also circular, since tailscaled has to resolve vpn.mgaction.town to
# connect in the first place.
#
# So neptun opts out with --accept-dns=false and does its own split DNS.
# tailscaled still answers MagicDNS on 100.100.100.100 whenever it is
# running (--accept-dns only governs whether it rewrites resolv.conf), so
# dnsmasq forwards just the tailnet suffix there and everything else to the
# public resolvers above. jupiter's address is therefore resolved live and
# never pinned — nothing to update when the tailnet is rebuilt.
#
# resolveLocalQueries (default) points resolv.conf at 127.0.0.1 and feeds
# networking.nameservers to dnsmasq as upstreams via resolvconf.
# neptun must NOT take the tailnet's DNS: headscale points every node at pihole on
# mercury, and a public reverse proxy depending on a Pi on a domestic line for name
# resolution (and thus for its own ACME renewals) would be fragile and circular.
# It opts out (--accept-dns=false) and runs its own split DNS instead: dnsmasq
# forwards the tailnet suffix to MagicDNS (100.100.100.100, still answered by
# tailscaled) and everything else to the public resolvers above — jupiter's address
# is resolved live, never pinned.
services.tailscale.extraUpFlags = [ "--accept-dns=false" ];
services.dnsmasq = {
enable = true;
@@ -112,49 +101,20 @@
'';
# ---- Obsidian LiveSync (CouchDB on jupiter) ----
# Obsidian's mobile apps refuse cleartext HTTP and *.jupiter.sol cannot hold
# a publicly trusted cert, so the vault database is published here instead of
# staying on the LAN. That means a credentialed database on the open
# internet; two things keep it sane:
# Published publicly (mobile apps refuse cleartext HTTP; *.jupiter.sol has no public
# cert), kept safe by the plugin's end-to-end encryption (jupiter stores only
# ciphertext) plus this allowlist — CouchDB otherwise exposes Fauxton, /_all_dbs and
# /_node/_local/_config, the last of which can rewrite the server's config with admin
# creds. Use the tailnet directly for those: `curl http://jupiter.orbit.sol:5984/_utils/`.
#
# 1. The plugin's end-to-end encryption, switched on BEFORE the first sync.
# jupiter then stores only ciphertext, so a breach here is not a leak of
# the notes themselves.
# 2. This allowlist. CouchDB serves far more than the replication API —
# Fauxton (/_utils), /_all_dbs, and /_node/_local/_config, the last of
# which REWRITES the server's config given admin credentials. Only the
# paths the plugin actually speaks are proxied; everything else is
# answered here and never reaches jupiter. Use the tailnet for the rest:
# `curl http://jupiter.orbit.sol:5984/_utils/`.
# The regex keys off CouchDB's own naming rule (system paths start with `_`, user
# databases can't) rather than listing vaults, plus `_session` for cookie auth — so a
# mistyped-but-legal name reaches CouchDB (real 404) while an illegal one gets
# caddy's 404 with no CORS, which Obsidian shows as a silent connection failure.
# Never point two vaults at the same database (LiveSync merges them, not reversibly).
#
# ONE DATABASE PER VAULT, and the matcher keys off CouchDB's own naming rule
# rather than listing them: every system endpoint begins with `_`, and a
# user-creatable database never can (CouchDB requires a lowercase letter
# first). So adding a vault needs no edit here. `_session` is the single
# underscore path let through, for cookie auth.
#
# The flip side of not listing them: a mistyped but otherwise LEGAL database
# name is proxied through and reaches CouchDB, which answers a real 404 the
# plugin can report. An ILLEGAL one — anything starting with a capital or an
# underscore — fails the matcher instead and gets caddy's 404, which carries
# no CORS headers and surfaces in Obsidian as a connection failure with no
# error message at all. If a new vault refuses to connect and the plugin
# says nothing, check the database name is lowercase first.
#
# Never point two vaults at one database: LiveSync merges them into a single
# file tree, which is not cleanly reversible.
#
# Known consequence: LiveSync's "Check database configuration" panel reads
# /_node/_local/_config and so reports the server as unconfigured from
# outside. Expected — that config is declarative in
# services/dev/obsidian-livesync.nix and is not the plugin's to patch.
#
# `flush_interval -1` is required, not tuning: replication rides a
# continuous _changes feed, which caddy would otherwise buffer — sync then
# stalls until the buffer fills (same reason vpn.mgaction.town sets it).
#
# No netcup edge-firewall change: this rides the 443 the other vhosts
# already use, unlike gitea's :2222.
# `flush_interval -1` is required, not tuning — replication rides a continuous
# _changes feed that caddy would otherwise buffer, stalling sync.
services.caddy.virtualHosts."notes.mgaction.town".extraConfig = ''
@livesync path_regexp ^/(_session|[a-z][a-z0-9_$()+-]*)?(/.*)?$
handle @livesync {
+11 -18
View File
@@ -14,13 +14,11 @@
sops.secrets.darman_password.neededForUsers = true;
users.users.darman.hashedPasswordFile = config.sops.secrets.darman_password.path;
# Authentik takes a single systemd EnvironmentFile (services/identity/authentik.nix).
# No `owner` here on purpose: systemd reads EnvironmentFile as root before
# dropping to the service's DynamicUser, so root:root 0400 is what we want.
#
# AUTHENTIK_SECRET_KEY signs sessions/tokens — rotating it logs everyone out.
# The BOOTSTRAP_* vars only take effect on the very first start, where they
# create the `akadmin` superuser; they're inert on every boot after that.
# Authentik takes a single systemd EnvironmentFile (services/identity/authentik.nix);
# no `owner` here on purpose, since systemd reads it as root before dropping to
# DynamicUser. AUTHENTIK_SECRET_KEY signs sessions (rotating it logs everyone out);
# the BOOTSTRAP_* vars only matter on the very first start (create `akadmin`) and are
# inert after.
sops.secrets.authentik_secret_key = { };
sops.secrets.authentik_bootstrap_password = { };
sops.secrets.authentik_bootstrap_email = { };
@@ -38,17 +36,12 @@
ACME_EMAIL=${config.sops.placeholder.caddy_acme_email}
'';
# Headplane: cookie_secret_path takes a path natively (no store leak).
# oidc.client_secret + the headscale API key are still REPLACE_ME
# placeholders (see services/vpn/headplane.nix) until Authentik/headscale are
# actually deployed and those get created for real.
#
# owner: unlike authentik's EnvironmentFile above, headscale and headplane
# open these paths themselves, already running as the headscale user — so
# the root:root 0400 default would fail and each needs an explicit owner.
#
# headscale's OIDC client is a SEPARATE Authentik application from
# headplane's (services/vpn/headscale.nix), hence the second client secret.
# Headplane's cookie_secret_path takes a path natively (no store leak); oidc.client_secret
# and the headscale API key are still REPLACE_ME placeholders (services/vpn/headplane.nix)
# until Authentik/headscale are deployed for real. Unlike authentik's EnvironmentFile,
# headscale/headplane open these paths themselves as the headscale user, so each needs
# an explicit owner — and headscale's OIDC client is a separate Authentik app from
# headplane's, hence the second client secret.
sops.secrets.headscale_oidc_client_secret.owner = "headscale";
sops.secrets.headplane_cookie_secret.owner = "headscale";