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