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:
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user