From 4679afa505ce2151546871bc6f34b700625954f3 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Mon, 20 Jul 2026 06:04:27 +0200 Subject: [PATCH] Trim comments across configs and services Shorten verbose multi-paragraph comments to essentials, and drop a stale claim in common.nix that jupiter kept its own copy of the base config (it now imports common.nix directly). Co-Authored-By: Claude Sonnet 5 --- common.nix | 1 - hosts/mercury/configuration.nix | 3 --- hosts/neptun/configuration.nix | 23 +++++++++-------------- services/cinephage.nix | 14 ++++++-------- services/gitea.nix | 22 +++++++--------------- services/headplane.nix | 23 ++++++++++------------- services/headscale.nix | 11 ++++------- services/mediamanager.nix | 7 +++---- services/prowlarr.nix | 23 ++++++++--------------- services/sabnzbd.nix | 11 +++++------ services/samba.nix | 10 +++++----- services/seerr.nix | 17 +++++------------ services/unbound.nix | 8 +++----- services/zitadel.nix | 14 ++++++-------- 14 files changed, 71 insertions(+), 116 deletions(-) diff --git a/common.nix b/common.nix index ed00dcd..88593a2 100644 --- a/common.nix +++ b/common.nix @@ -1,7 +1,6 @@ { pkgs, ... }: # Shared base for all hosts: user, SSH hardening, nix settings, packages. -# (jupiter still carries its own copy in services.nix; neptun uses this.) { # ---- User ---- users.users.darman = { diff --git a/hosts/mercury/configuration.nix b/hosts/mercury/configuration.nix index 41fc9e8..9384491 100644 --- a/hosts/mercury/configuration.nix +++ b/hosts/mercury/configuration.nix @@ -33,9 +33,6 @@ # host resolves during boot without depending on its own pihole/unbound. networking.nameservers = [ "1.1.1.1" "9.9.9.9" ]; - # DNS adblock + DHCP (pihole) and the recursive resolver (unbound) come from - # the imported service modules. pihole forwards to unbound at 127.0.0.1:5335. - # ---- pihole web admin password (from sops) ---- # The pihole container reads FTLCONF_* env vars. Render an env file from the # sops secret and feed it to the container — password stays out of repo/store. diff --git a/hosts/neptun/configuration.nix b/hosts/neptun/configuration.nix index f4471cd..d61b0a6 100644 --- a/hosts/neptun/configuration.nix +++ b/hosts/neptun/configuration.nix @@ -60,14 +60,11 @@ ''; # ---- Headscale + Headplane (tailnet control server + its web UI) ---- - # Both run locally on neptun (see services/{headscale,headplane}.nix), path - # -routed on the same vhost: Headplane owns /admin* (its whole app, - # including static assets and the OIDC callback, lives under that prefix — - # `handle` keeps the prefix in the forwarded path, unlike `handle_path`, - # since Headplane needs to see it), everything else goes to headscale - # itself (tailnet client traffic). headscale's node-update endpoint is a - # long-poll, hence `flush_interval -1` — without it Caddy buffers the - # response and clients see stale state. + # Path-routed on one vhost: Headplane owns /admin* (uses `handle`, not + # `handle_path`, since it needs the prefix kept in the forwarded path for + # its own assets + OIDC callback); everything else goes to headscale. + # `flush_interval -1`: headscale's node-update endpoint is a long-poll and + # Caddy would otherwise buffer it, showing clients stale state. services.caddy.virtualHosts."vpn.mgaction.town".extraConfig = '' handle /admin* { reverse_proxy http://localhost:3000 @@ -80,12 +77,10 @@ ''; # ---- Gitea SSH forward ---- - # Caddy only proxies HTTP; git-over-ssh needs a raw TCP forward. Gitea's - # own built-in SSH server runs on jupiter:2222 (see services/gitea.nix — - # not :222, the unpriv gitea user can't bind <1024). Forward this VPS's - # public :2222 to it over the tailnet, so `ssh://git@git.mgaction.town:2222/...` - # reaches gitea. Needs a matching inbound-2222 rule in netcup's *edge* - # firewall panel too (separate from this box's own, and not managed by Nix). + # Caddy only proxies HTTP; forward :2222 over the tailnet to gitea's own + # SSH server on jupiter (services/gitea.nix), so + # `ssh://git@git.mgaction.town:2222/...` works. Also needs a matching + # inbound-2222 rule in netcup's edge firewall panel (not managed by Nix). systemd.services.gitea-ssh-forward = { description = "Forward :2222 to jupiter's gitea SSH server over tailscale"; after = [ "network-online.target" "tailscaled.service" ]; diff --git a/services/cinephage.nix b/services/cinephage.nix index 9ab9a4a..677c548 100644 --- a/services/cinephage.nix +++ b/services/cinephage.nix @@ -1,14 +1,12 @@ { config, ... }: # Cinephage — indexer search + streaming/library manager. Runs the official -# container image, not upstream's nix flake module: their flake's -# npmDepsHash is stale against their own package-lock.json (fixed-output -# hash mismatch), and past that, a transitive dep's postinstall hard-enforces -# pnpm (`only-allow pnpm`), which fails under nix's network-sandboxed npm -# build regardless. Docker is their actually-maintained deployment path. -# Paired sops secret in hosts/jupiter/secrets.nix — BETTER_AUTH_SECRET signs -# sessions/encrypts stored API keys; losing it invalidates all sessions and -# makes saved keys unrecoverable, so it must be static, not app-generated. +# container image, not upstream's nix flake module: its npmDepsHash is stale +# against its own package-lock.json, and a transitive dep hard-enforces pnpm, +# breaking the nix-sandboxed npm build regardless. Docker is the actually- +# maintained path. BETTER_AUTH_SECRET (paired sops secret in +# hosts/jupiter/secrets.nix) signs sessions/encrypts stored API keys — must +# be static, not app-generated, or losing it invalidates everything. { virtualisation.oci-containers.containers.cinephage = { image = "ghcr.io/moldytaint/cinephage:latest"; diff --git a/services/gitea.nix b/services/gitea.nix index 466890f..4423c45 100644 --- a/services/gitea.nix +++ b/services/gitea.nix @@ -1,22 +1,14 @@ { ... }: # Gitea — self-hosted git. stateDir/repositories were migrated from the old -# ZimaOS docker instance (single user/org "darman", sqlite db, 4 repos, no -# LFS objects yet) into stateDir's default layout (data/gitea.db, -# data/{indexers,avatars,sessions,...}, repositories/), so no import step is -# needed on first boot — NixOS just needs to chown it to the gitea user -# (see below). -# -# HTTP is reverse-proxied through Caddy (see hosts/jupiter/configuration.nix -# for the vhost). SSH clone/push uses gitea's own built-in SSH server on -# :2222, kept separate from the host's OpenSSH on :22. NOT :222 (the old -# docker setup's external port) — gitea runs as the unpriv "gitea" user with -# an empty capability set, so it can't bind <1024: "listen tcp :222: bind: -# permission denied". -# -# After first deploy, fix ownership of the migrated data (it currently -# belongs to darman:users from the CIFS copy): +# ZimaOS docker instance straight into stateDir's default layout, so no +# import step is needed — just chown it to the gitea user after first deploy +# (currently darman:users from the CIFS copy): # chown -R gitea:gitea /mnt/data/AppData/gitea +# +# HTTP is reverse-proxied through Caddy (hosts/jupiter/configuration.nix). +# SSH uses gitea's own built-in server on :2222 (not the host's :22, and not +# :222 — the unpriv gitea user can't bind <1024). { services.gitea = { enable = true; diff --git a/services/headplane.nix b/services/headplane.nix index 1b79626..17389f5 100644 --- a/services/headplane.nix +++ b/services/headplane.nix @@ -1,21 +1,18 @@ { config, ... }: # Headplane — web UI for headscale (services/headscale.nix; must be enabled -# first). Runs as headscale's own OS user via "native process integration", -# so it can restart headscale when settings change from the UI. +# first). Runs as headscale's own OS user so it can restart headscale when +# settings change from the UI. # -# Served at vpn.mgaction.town/admin (path-routed alongside headscale itself -# by Caddy — see hosts/neptun/configuration.nix), not its own subdomain. -# base_url is the site root WITHOUT the /admin prefix — Headplane appends -# that itself, including for the OIDC callback (.../admin/oidc/callback). +# Served at vpn.mgaction.town/admin (path-routed alongside headscale itself, +# see hosts/neptun/configuration.nix). base_url is the site root WITHOUT the +# /admin prefix — Headplane appends that itself, including for the OIDC +# callback. # -# Auth is Zitadel (services/zitadel.nix), via OIDC. client_id isn't secret -# (it's a public identifier) so it's a plain string here, but it — along -# with client_secret and the headscale API key — can't be known until -# Zitadel/headscale are actually deployed and running. Until then these are -# placeholders; direct headscale-API-key login (disable_api_key_login stays -# false) still works as a fallback so this doesn't lock anyone out. Once -# live: +# Auth is Zitadel (services/zitadel.nix) via OIDC. client_id, client_secret, +# and the headscale API key can't be known until Zitadel/headscale are +# actually deployed, so they're placeholders below; direct API-key login +# still works as a fallback until then. Once live: # 1. In Zitadel: create a project + a Web application for Headplane, with # redirect URI https://vpn.mgaction.town/admin/oidc/callback. Copy the # generated client ID into oidc.client_id below. diff --git a/services/headscale.nix b/services/headscale.nix index 527d570..e11879c 100644 --- a/services/headscale.nix +++ b/services/headscale.nix @@ -3,14 +3,11 @@ # Headscale — self-hosted control server for the tailnet. Every host's # services/tailscale.nix points --login-server at https://vpn.mgaction.town # (this host). MagicDNS base_domain "hosts.mgaction.town" matches the -# "jupiter.hosts.mgaction.town" style names already used throughout this -# repo's Caddy vhosts (e.g. hosts/neptun/configuration.nix) — don't change -# one without the other. +# "jupiter.hosts.mgaction.town" names used in this repo's Caddy vhosts +# (e.g. hosts/neptun/configuration.nix) — don't change one without the other. # -# TLS terminates at Caddy (see the host's configuration.nix for the vhost, -# proxying with `flush_interval -1` since headscale's node-update endpoint -# is a long-poll and needs unbuffered responses); headscale itself only -# listens on localhost. +# TLS terminates at Caddy (see the host's configuration.nix); headscale +# itself only listens on localhost. { services.headscale = { enable = true; diff --git a/services/mediamanager.nix b/services/mediamanager.nix index 91efe3c..8f00424 100644 --- a/services/mediamanager.nix +++ b/services/mediamanager.nix @@ -1,10 +1,9 @@ { config, ... }: # MediaManager — media request/library manager. Module comes from the -# community flake input `mediamanager-nix` (wired into jupiter's module list -# in flake.nix, NOT nixpkgs). Paired sops secret declared in -# hosts/jupiter/secrets.nix — without it the module mints+discards a random -# auth token_secret on every service restart, logging everyone out. +# community flake input `mediamanager-nix`, not nixpkgs. Paired sops secret +# in hosts/jupiter/secrets.nix — without it the module mints+discards a +# random auth token_secret on every restart, logging everyone out. # Port 8010: 8000 is taken by audiobookshelf on this host. { services.media-manager = { diff --git a/services/prowlarr.nix b/services/prowlarr.nix index ae43e79..01b42e2 100644 --- a/services/prowlarr.nix +++ b/services/prowlarr.nix @@ -1,23 +1,16 @@ { ... }: # Prowlarr — indexer manager (usenet + torrent), feeds SABnzbd/MediaManager. -# services.prowlarr.dataDir is left at its module default (/var/lib/prowlarr) -# on purpose: passing a *custom* dataDir makes the upstream module bind-mount -# it and force-reset the outer dir to 0700 root:root via a tmpfiles rule on -# every boot (nixos/modules/services/misc/servarr/prowlarr.nix) — that stomps -# DynamicUser's access to pre-existing content and causes intermittent -# "unable to open database file". -# -# Instead we bind-mount the real (migrated-from-ZimaOS) config dir straight -# onto the module's own default path, so prowlarr never sees a "custom" -# dataDir and none of that logic triggers. DynamicUser+StateDirectory then -# recursively chowns the pre-existing content to its assigned uid on first -# activation, same as it does for a fresh install — no manual chown needed. +# dataDir is left at the module default: a *custom* dataDir makes the +# upstream module force-reset it to 0700 root:root on every boot, stomping +# DynamicUser's access ("unable to open database file"). Instead bind-mount +# the real (migrated-from-ZimaOS) config dir onto the default path, so +# DynamicUser+StateDirectory chowns it on first activation like a fresh +# install — no manual chown needed. # # Mount onto /var/lib/private/prowlarr, NOT the public /var/lib/prowlarr: -# DynamicUser+StateDirectory keeps real data at .../private/ and makes -# the public path a symlink to it; binding onto the public path turns it into -# a mountpoint systemd then can't rename during its migrate-on-start dance +# StateDirectory symlinks the public path to .../private/; binding +# onto the public path itself blocks systemd's migrate-on-start rename # ("Device or resource busy", exit 238/STATE_DIRECTORY). { services.prowlarr.enable = true; diff --git a/services/sabnzbd.nix b/services/sabnzbd.nix index 43d1196..7ce40a8 100644 --- a/services/sabnzbd.nix +++ b/services/sabnzbd.nix @@ -16,12 +16,11 @@ # Write access to the shared downloads dir (owned darman:users on disk). users.users.sabnzbd.extraGroups = [ "users" ]; - # SABnzbd hardcodes completed job folders to 0700 on every single job — - # independent of the ini's `umask` setting (that only covers files - # created during unpack, not the enclosing per-job dir). setgid on - # Downloads keeps the *group* as "users", but group perm bits still come - # back zeroed, locking out cinephage/mediamanager. Sweep it clean instead - # of fighting SABnzbd's own behavior. + # SABnzbd hardcodes completed job folders to 0700 on every job, ignoring + # the ini's `umask` (that only covers files during unpack, not the job + # dir itself). setgid on Downloads keeps the group as "users" but perm + # bits still come back zeroed, locking out cinephage/mediamanager — sweep + # it clean instead of fighting SABnzbd. systemd.services.fix-downloads-perms = { description = "Fix group perms SABnzbd resets on completed downloads"; serviceConfig.Type = "oneshot"; diff --git a/services/samba.nix b/services/samba.nix index 9622338..57132d7 100644 --- a/services/samba.nix +++ b/services/samba.nix @@ -22,11 +22,11 @@ }; }; - # Samba keeps its own NTLM password DB, separate from the system password. - # `services.samba` never sets it, so logins fail until provisioned. - # This runs AFTER samba-smbd so its state dir (/var/lib/samba/private) exists - # — an activation script runs too early and smbpasswd fails to init the passdb. - # Reads a single-line password from the first file that exists: + # Samba keeps its own NTLM password DB, separate from the system password; + # `services.samba` never sets it, so logins fail until provisioned. Runs + # AFTER samba-smbd so its state dir exists — an activation script runs too + # early and smbpasswd fails to init the passdb. Reads a single-line + # password from the first file that exists: # Real host: /run/secrets/samba_password (sops-nix, see secrets.nix) # VM test: /etc/samba/smb-password (plaintext, see vm.nix) # smbpasswd prompts new + confirm, so the value is fed twice. diff --git a/services/seerr.nix b/services/seerr.nix index 6871769..9738da9 100644 --- a/services/seerr.nix +++ b/services/seerr.nix @@ -3,18 +3,11 @@ # Seerr (formerly Jellyseerr) — request manager for Jellyfin, talks to # Sonarr/Radarr to fulfill requests. Fresh install, no migrated data. # -# configDir is left at the module default (/var/lib/seerr) on purpose: -# systemd's StateDirectory= auto-chown for DynamicUser only applies to that -# exact default path, not to an arbitrary custom one (same class of issue as -# prowlarr.nix, worked around the same way) — bind-mount AppData onto the -# default path instead of overriding configDir, so it stays on the RAID array -# and survives an OS-disk reinstall like every other service's data. -# -# Mount onto /var/lib/private/seerr, NOT the public /var/lib/seerr: -# DynamicUser+StateDirectory keeps real data at .../private/ and makes -# the public path a symlink to it; binding onto the public path turns it into -# a mountpoint systemd then can't rename during its migrate-on-start dance -# ("Device or resource busy", exit 238/STATE_DIRECTORY). +# configDir stays at the module default; bind-mount AppData onto it instead +# of overriding configDir, so data lives on the RAID array and survives an +# OS-disk reinstall (same DynamicUser/StateDirectory issue as prowlarr.nix — +# see that file for why, and why the mount targets /var/lib/private/seerr +# rather than the public path). { services.seerr.enable = true; diff --git a/services/unbound.nix b/services/unbound.nix index e48eafb..baaefda 100644 --- a/services/unbound.nix +++ b/services/unbound.nix @@ -8,11 +8,9 @@ { services.unbound = { enable = true; - # Do NOT point the host's resolv.conf at unbound: it listens on :5335, not - # :53, so that would leave the host with no working resolver until pihole - # binds :53 (a boot-time deadlock — can't pull images / build lists). The - # host resolves via networking.nameservers (upstream) instead; pihole - # forwards to unbound explicitly at 127.0.0.1#5335. + # Do NOT point resolv.conf at unbound: it listens on :5335, not :53, so + # that leaves the host with no resolver until pihole binds :53 — a + # boot-time deadlock. Host resolves via networking.nameservers instead. resolveLocalQueries = false; # NixOS manages the DNSSEC root trust anchor (unbound-anchor). settings.server = { diff --git a/services/zitadel.nix b/services/zitadel.nix index 6775adb..e7c08b2 100644 --- a/services/zitadel.nix +++ b/services/zitadel.nix @@ -2,15 +2,13 @@ # Zitadel — self-hosted identity/OIDC provider. Local Postgres (peer-authed # over the unix socket, no password anywhere) since Zitadel is latency- -# sensitive to its DB. TLS is terminated at Caddy (see the host's -# configuration.nix for the public vhost); Zitadel itself only listens on -# localhost:8080. +# sensitive to its DB. TLS terminates at Caddy; Zitadel itself only listens +# on localhost:8080. # -# The "zitadel" Postgres role doubles as both Database.postgres.User (normal -# runtime queries) and .Admin (bootstrap: creates the db/extensions on first -# start) — granted createdb+createrole instead of using the real postgres -# superuser, matching Zitadel's own guidance to keep bootstrap privileges -# scoped to a dedicated role. +# The "zitadel" Postgres role doubles as both User (runtime queries) and +# Admin (bootstrap: creates db/extensions on first start) — granted +# createdb+createrole rather than using the postgres superuser, per +# Zitadel's own guidance to scope bootstrap privileges to a dedicated role. # # Needs, wired via sops in the host's secrets.nix: # - masterKeyFile: 32 raw bytes, e.g. `openssl rand -hex 16`