Commit Graph
15 Commits
Author SHA1 Message Date
darmanandClaude Opus 4.8 d9ea6a9ecc mercury: join the tailnet
mercury was the only host with no tailscale at all -- no module import,
no secret, no key in its sops file. It had been enrolled before the NixOS
migration and silently dropped off the tailnet when it was reflashed with
a config that omitted it.

--accept-dns=false, as on neptun and for a sharper reason: headscale
pushes override_local_dns, so accepting MagicDNS would repoint the LAN's
own DNS server at 100.100.100.100 and make house-wide name resolution
depend on tailscaled being up. This host has already deadlocked once on
boot-time DNS (see CLAUDE.md).

darman_password is also rotated: the account had "!" in /etc/shadow,
because on mercury's first boot the secret wasn't readable yet and
update-users-groups.pl falls back to a locked account. mutableUsers is
true, so no later rebuild ever revisited it and the lock was permanent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:41:39 +02:00
darmanandClaude Opus 4.8 8aa3dac4de deploy: prompt for the sudo password on switch/boot/test
common.nix now sets security.sudo.wheelNeedsPassword = true, but
--use-remote-sudo is deprecated and only prefixes commands with sudo --
it never prompts, so every remote rebuild failed. --ask-sudo-password is
the alias for --elevate=sudo --ask-elevate-password, which asks once and
feeds it via sudo --stdin.

This should have gone in with the wheelNeedsPassword change itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:41:39 +02:00
darmanandClaude Opus 4.8 82122a964d neptun: record post-install hardware config, rotate darman's password
hardware-configuration.nix as regenerated by nixos-anywhere during the
install, replacing the placeholder. The detected initrd modules differ
from what the placeholder guessed (ata_piix, uhci_hcd), but the virtio
modules pinned in configuration.nix merge in regardless, so root mounts
either way.

darman_password is rotated because the previous hash's plaintext was not
recorded anywhere. Combined with wheelNeedsPassword = true and
PermitRootLogin = "no" that left no way to escalate on the box, and
recovery needed netcup's rescue system to edit /etc/shadow directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:45:29 +02:00
darmanandClaude Opus 4.8 3671841eca headscale: run our own DERP relay instead of Tailscale's
By default headscale fetches https://controlplane.tailscale.com/derpmap/default
at startup and treats failure as fatal, so it cannot boot when that URL is
unreachable. A self-hosted control plane that will not start without
Tailscale's infrastructure rather misses the point of self-hosting -- and
it crash-looped for exactly that reason while neptun had no DNS.

Enable the embedded DERP server on region 999 and drop the upstream map.
The relay rides Caddy on :443, which is why that vhost already sets
flush_interval -1; only STUN needs a port of its own.

Verified against headscale 0.28.0 before committing: it starts clean with
urls = [], registers "DERP region: {RegionID:999 ...}" pointing at
vpn.mgaction.town with DERPPort 443, and brings up STUN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:45:29 +02:00
darmanandClaude Opus 4.8 4fedc80bb4 neptun: serve the vhosts that are actually in production
Probing the live Debian VPS turned up three mismatches between what it
serves and what this config declares:

- git.mgaction.town had no vhost at all. Gitea's web UI and HTTPS clones
  are public today; only its SSH side (the :2222 socat forward) had been
  ported, so a deploy would have taken the web side offline.
- Audiobookshelf is served as abs.mgaction.town, not the longer
  audiobookshelf.mgaction.town this config used. The mobile app is
  configured with the short name.
- The apex returns 200 from Caddy. Left unserved deliberately, so it now
  gets Caddy's default 404; noted in a comment so it doesn't look like an
  oversight next time.

Gitea's ROOT_URL was http:// while Caddy terminates TLS for that name.
Gitea builds absolute URLs from it, so clone buttons, redirects and
webhooks were handing out downgraded links.

Also record that defaultGateway6 is confirmed rather than assumed --
`ip -6 route show default` on the VPS gives "default via fe80::1 dev
eth0 metric 1024 onlink".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 08:24:35 +02:00
darmanandClaude Opus 4.8 ac42f231f5 neptun: replace Zitadel with Authentik as the OIDC provider
nixpkgs only carries Zitadel 2.71, which predates the login-v2 split and
cannot take a v3/v4 database (its migrations are forward-only), so the
instance running on the old Debian VPS could never have moved onto it.
authentik-nix ships 2026.5.4 and tracks upstream closely.

The authentik-nix input deliberately does not follow our nixpkgs, per
upstream's warning that overriding it breaks their pinned python
dependency set. That costs a second nixpkgs in the lock, so add
nix-community's Cachix to common.nix -- without it the closure is ~400
local derivations (npm, rust, python). The laptop that runs
scripts/deploy needs the same two lines in /etc/nix/nix.custom.conf.

Authentik's own module creates the database and orders its units against
postgresql.target, and recent versions need no redis, so the wiring is
just the module plus a secret. Pin postgresql explicitly so that editing
system.stateVersion can never silently demand a pg_upgrade of the
identity store.

Secret ownership is not uniform and the difference matters: authentik
and caddy take a systemd EnvironmentFile, which PID 1 reads as root
before dropping privileges, so root:root 0400 is correct. Headplane
opens its secret paths itself while already running as the headscale
user, so those three need an explicit owner or they fail to start.

Also on neptun:

- Pass Caddy's ACME account email through the same EnvironmentFile
  mechanism and reference it with the Caddyfile {$VAR} placeholder.
  services.caddy.email would render the address into the world-readable
  store.
- Stop accepting MagicDNS from our own control server. headscale pushes
  override_local_dns, so joining the tailnet would point neptun's
  resolv.conf at a MagicDNS served by the tailscaled neptun itself hosts
  -- a tailscaled failure would then also take out DNS, ACME renewal and
  finally the certs for the control server every other node needs in
  order to recover.
- Give headplane a writable DNS extra-records file. Its view of
  headscale's config stays read-only, which is the right outcome for a
  declarative box; records are data rather than config.
- Require a password for sudo. Deploys become interactive, but darman's
  key is otherwise the only thing between the public internet and root.
- Enable zram (8 GB, and disko leaves no room for a swap device), and let
  tailscaled-autoconnect retry instead of failing permanently when the
  control server isn't up yet on a first boot.

networking.hosts still carries a PLACEHOLDER address for jupiter --
replace it from `headscale nodes list` once jupiter first enrols.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 07:50:39 +02:00
darmanandClaude Sonnet 5 d7a66f3e3b Reorganize services/ into category subfolders
Group service modules by category (media, network, vpn, identity,
dev, desktop) to make the growing services/ dir easier to navigate.
containers.nix stays at the top level since it's a shared backend,
not a single-category service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 06:08:43 +02:00
darmanandClaude Sonnet 5 4679afa505 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>
2026-07-20 06:04:27 +02:00
darmanandClaude Sonnet 5 c4702b577c neptun: serve Headplane at vpn.mgaction.town/admin, auth via Zitadel OIDC
Path-route Headplane under /admin on the same vhost as headscale instead of
its own subdomain - Caddy handle blocks split on the prefix, headscale gets
everything else. base_url drops to the site root since Headplane appends
/admin (and the OIDC callback path) itself.

Wire Zitadel as the OIDC provider. client_id/client_secret/the headscale
API key can't be real until Zitadel and headscale are actually deployed and
an application/key exist, so those are REPLACE_ME placeholders for now
(documented in services/headplane.nix) - direct API-key login stays enabled
as a fallback so this can't lock anyone out in the meantime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 22:42:03 +02:00
darmanandClaude Sonnet 5 5507dfac0a neptun: add Headscale + Headplane
Headscale is the tailnet control server every host's services/tailscale.nix
already points at (--login-server=https://vpn.mgaction.town). MagicDNS
base_domain "hosts.mgaction.town" matches the "jupiter.hosts.mgaction.town"
names already used in this repo's Caddy vhosts.

Headplane is its web UI, running as headscale's own user (native process
integration, no container). No OIDC wired up - log in with a headscale API
key generated on the box. Both proxied through Caddy; headscale's vhost
needs flush_interval -1 since its node-update endpoint is a long-poll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 22:36:40 +02:00
darmanandClaude Sonnet 5 ced2b56764 neptun: add Zitadel (identity/OIDC provider)
Local Postgres, peer-authed over the unix socket (the "zitadel" role is
granted createdb+createrole and doubles as both the runtime and bootstrap
DB user - no password anywhere). TLS terminates at Caddy; Zitadel listens
on localhost:8080 and is proxied at auth.mgaction.town.

Master key and admin bootstrap password come from sops - the admin
password specifically needs the sops.templates -> rendered-file route
(services.zitadel.steps would leak it into the world-readable Nix store),
same pattern as mercury's pihole.env.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 22:30:11 +02:00
darmanandClaude Sonnet 5 84da4bacc5 neptun: forward :2222 to jupiter's gitea SSH server
Caddy only proxies HTTP; git-over-ssh to gitea needs a raw TCP forward
since gitea's built-in SSH server (jupiter:2222) isn't otherwise reachable
from the public internet. socat forwards the VPS's public :2222 over the
tailnet. Matches what's now live on the (still-Debian) VPS - ready to drop
in once neptun gets migrated to this NixOS config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 22:22:39 +02:00
darmanandClaude Sonnet 5 69a63dc615 Add *arr media stack + Gitea to jupiter
- sabnzbd, prowlarr, sonarr, radarr, clonarr, seerr, cinephage, mediamanager
  services, wired into jupiter with LAN Caddy vhosts.
- Gitea: migrated the old ZimaOS docker instance's data (sqlite db, 4 repos,
  no LFS objects) into the NixOS module's default stateDir layout. HTTP via
  Caddy; git SSH on its own built-in server at :2222 (not :222 - the unpriv
  gitea user can't bind <1024).
- mediamanager-nix flake input for the mediamanager service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 21:27:50 +02:00
darman fe5b363b73 Added Jellyfin service to jupiter 2026-07-17 23:12:04 +02:00
darman dd6e093ca4 chore: set mercury secrets 2026-07-13 22:42:20 +02:00