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, ... }:
# 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 = {
-3
View File
@@ -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.
+9 -14
View File
@@ -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" ];
+6 -8
View File
@@ -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";
+7 -15
View File
@@ -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;
+10 -13
View File
@@ -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.
+4 -7
View File
@@ -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;
+3 -4
View File
@@ -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 = {
+8 -15
View File
@@ -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/<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
# StateDirectory symlinks the public path to .../private/<name>; 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;
+5 -6
View File
@@ -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";
+5 -5
View File
@@ -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.
+5 -12
View File
@@ -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/<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).
# 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;
+3 -5
View File
@@ -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 = {
+6 -8
View File
@@ -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`