Commit Graph
59 Commits
Author SHA1 Message Date
luna 806cec77e8 mars: add generic Gitea webhook relay 2026-08-23 01:45:18 +00:00
darmanandClaude Sonnet 5 3c1f3e5fc3 mars: give L.U.N.A. direct git+tea access to the homelab repo
Provisions a dedicated PR-tier gitea account (luna) with branch protection
restricting master push/merge/approve to darman only, then wires git and
tea directly into the hermes-agent container (mounted from the host's Nix
store, credential-store + tea login set up by a host-side prepare oneshot,
repo cloned inside Hermes's own writable sandbox root at
/opt/data/workspace/homelab). Replaces an earlier standalone MCP-server
approach, scrapped in favor of direct CLI access for simplicity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
2026-08-22 20:50:35 +02:00
darmanandClaude Sonnet 5 dc83e8c156 add node_exporter host vitals + quickshell HUD
Prometheus node_exporter enabled on every host, plus a quickshell widget
(SUPER+CTRL+V on terra) to view live CPU/mem/disk/net/uptime without a
separate dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
2026-08-22 20:50:16 +02:00
darmanandClaude Sonnet 5 60bef752cb home-manager: enable for darman on every host, not just terra
terra was the only host with real ~/.zshrc/.zshenv (via home-manager),
so it never hit the plain-zsh zsh-newuser-install wizard that shows up
on first login everywhere else. Wire home-manager.nixosModules.home-manager
into jupiter/neptun/mars/mercury (+ mercury-vm/jupiter-vbox test targets)
and give darman terra's shared zsh baseline via home/common.nix, imported
from common.nix. terra's own home.nix now only carries its
desktop/dev-specific profile (Hyprland, alacritty, git identity, direnv,
dev packages) layered on top via home-manager.users.darman.imports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du1WQRk1F8DenrPhf8TofF
2026-08-22 05:03:43 +02:00
darmanandClaude Sonnet 5 a1cd6ae6f1 mars: add hermes CLI shell alias
darman's own podman is rootless while the container runs under root's
(system) podman, so plain `podman exec` couldn't see it. Alias runs it
with sudo against the right socket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du1WQRk1F8DenrPhf8TofF
2026-08-22 05:03:30 +02:00
darmanandClaude Sonnet 5 99501ce7d2 mars: drop idle-timeout on the /mnt/jupiter cifs mount
podman-hermes-agent.service RequiresMountsFor /mnt/jupiter, but the mount
option copied from terra's (read-only, nothing depends on it) browsing
mount included x-systemd.idle-timeout=60 — confirmed on real hardware,
this killed the container ~60-70s after every start with no crash or
error, just an idle auto-unmount taking the dependent service down with
it. Keep the lazy x-systemd.automount (so boot doesn't stall if jupiter's
down) but drop the timeout now that something needs the mount to persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
2026-08-22 03:09:21 +02:00
darmanandClaude Sonnet 5 7d63ba95df add mars host, move Hermes Agent there from jupiter
New on-site host mars runs Hermes Agent as its sole service: joins the
tailnet, mounts jupiter's samba share at /mnt/jupiter (doubling as
Hermes's shared dropbox), and hosts state locally under /var/lib/hermes.
Same Authentik OIDC app/Telegram bot as before, just relocated — neptun's
hermes.mgaction.town vhost now points at mars.orbit.sol instead of jupiter.

hosts/jupiter/hermes-agent.nix and its three sops secrets are removed;
jupiter's Caddy vhost for it is gone too. Also refreshes tailscale_authkey
across all hosts and fixes two stale "erik@laptop" keys in flake.nix's
kexec/installer-iso images (leftover from a previous laptop, already
swapped out of common.nix back in 2fd5752) to darman@terra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
2026-08-22 03:00:24 +02:00
darmanandClaude Sonnet 5 9403122888 jupiter: fix Hermes cron scheduler defaulting to UTC
The container has no host /etc/localtime bind-mount, so hermes_time.py's
timezone resolution fell through to UTC. HERMES_TIMEZONE is its
highest-priority source (checked before config.yaml's timezone key).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 23:00:55 +02:00
darmanandClaude Sonnet 5 713d91d5fc terra: finish removing Hermes Agent (module import + secrets)
Follow-up to e5ba1bf — that commit only staged the deleted module file.
Drops the flake module import, opencode_go_api_key/telegram_bot_token
secrets, and the stale hermes-agent.nix cross-reference in ollama's
context_length comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 22:49:50 +02:00
darmanandClaude Sonnet 5 b5fa599671 jupiter: add isolated Hermes Agent instance
A separate instance from terra's, deliberately locked down harder given
jupiter's much bigger blast radius (irreplaceable immich photos on an
unredundant RAID0, gitea/CI tokens, the whole media stack): its own
dedicated "hermes" system user rather than darman (who is in jupiter's
root-equivalent docker group), container.enable = true for whole-process
containment rather than native/bare-metal, its own Telegram bot + explicit
allowlist, and no volume access to /mnt/data or this repo. stateDir/
workingDirectory live on the array (off the 29G eMMC) for future coding-task
state, guarded by RequiresMountsFor like the rest of jupiter's array-backed
services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 22:46:32 +02:00
darmanandClaude Sonnet 5 0fa567245a jupiter: GC on every boot, silence mdadm warning, migrate sabnzbd off reused ini
nix.gc (common.nix) is weekly, too slow to catch a switch pinning the old
generation's closure on a 29G eMMC — add a full nix-collect-garbage on every
boot instead. Also set boot.swraid.mdadmConf so eval stops warning that
mdmon will crash (dormant here: the RAID0 array uses native superblocks, so
mdmon never actually runs).

sabnzbd.configFile is deprecated by the module; move to services.sabnzbd.settings
with credentials (web login, api/nzb keys, eweka.nl server) sourced from sops via
secretValues instead of living in a plaintext ini. admin_dir/log_dir are pinned
absolute at their original /mnt/data location so the existing download
queue/history isn't reset by the ini moving to /var/lib/sabnzbd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:58:40 +02:00
darmanandClaude Sonnet 5 969bd69d8d terra: add Hermes Agent, wired to local ollama
Points Nous Research's Hermes Agent at terra's own ROCm ollama server
(gemma4:12b) as a custom OpenAI-compatible provider instead of a cloud
key. Native systemd mode via the hermes-agent flake's own NixOS module
— simpler than container mode, avoids the podman-rootful-sudo dance
its docs call out.

Also bumps OLLAMA_CONTEXT_LENGTH (and Hermes' mirrored model.context_length)
from ollama's ~4k default to 131072, load-tested with real multi-ten-
thousand-token prompts rather than just idle `ollama ps` checks — chosen
as the practical ceiling where VRAM headroom and prefill throughput both
start visibly degrading, not just the largest number that technically fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 01:28:34 +02:00
darmanandClaude Sonnet 5 6c8046bac8 jupiter: VAAPI hardware transcoding for jellyfin, move heavy state off the eMMC
Enables hardware.graphics + intel-media-driver for the Apollo Lake's
Gen9 iGPU (VAAPI only — QSV needs an insecure/EOL runtime on this
chip) and adds jellyfin's service user to video/render for the DRI
card node. 4K HDR still can't be tone-mapped on this hardware; those
files need to direct-play or be kept as 1080p SDR.

Also relocates podman's container storage and immich's postgres
cluster to /mnt/data/AppData, after a deploy holding two ~9G closures
at once filled the 29G eMMC and postgres died mid-write.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 00:38:22 +02:00
darmanandClaude Sonnet 5 914a7e5105 terra: drop kicad flatpak
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 00:38:15 +02:00
darmanandClaude Sonnet 5 7904433d34 terra: ollama (ROCm) + LibreChat with local persistent memory
Local LLM server on the 6800 XT (ollama-rocm, gfx1030 needs no
HSA_OVERRIDE_GFX_VERSION) fronted by a LibreChat web UI, talking to it
over the OpenAI-compatible /v1 route. Also wires up LibreChat's
persistent-memory feature, which needed its own agent+model plus a
custom extraction prompt: the default 3b model couldn't reliably tell
the user's stated facts apart from its own boilerplate, and even a
tuned prompt didn't fix that — so memory extraction now reuses
gemma4:12b, the same model as the daily-driver chat endpoint.

flake.lock bump pulls in the ollama and librechat NixOS modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 00:38:07 +02:00
darmanandClaude Opus 5 0ec3c6413c jupiter: keep booting when the data array is missing
After the rack move one of the RAID0 disks failed to enumerate, and jupiter
boot-looped into an emergency shell nobody could use — root is locked, so
sulogin offers a prompt with no answer, and there is no ssh from there:

  Timed out waiting for device /dev/disk/by-uuid/dadbff6f-...
  Dependency failed for /mnt/data.
  Dependency failed for /var/lib/private/prowlarr.
  Dependency failed for Local File Systems.
  local-fs.target: Job local-fs.target/start failed with result 'dependency'
  Reached target Emergency Mode.

`nofail` on /mnt/data did not help, because the prowlarr and seerr bind
mounts layered on top of it had none: without it a mount is RequiredBy
local-fs.target, so those two failed the target on the array's behalf. Give
them `nofail` too and let them fail alone. `systemd.enableEmergencyMode =
false` then keeps a bad array from costing a reachable box at all — far more
useful on a headless host than a console prompt.

Booting further is only safe if nothing quietly relocates onto the 29G eMMC,
so pin the array-backed services to the mount. systemd derives
RequiresMountsFor from a unit's own paths, which for these is somewhere under
/var/lib (eMMC) — nothing pointed immich at mediaLocation or sabnzbd at its
configFile, so with the array gone they would have started and written to the
OS disk, into directories that go invisible the moment /mnt/data mounts over
them. jellyfin, sonarr, radarr and gitea already had a real dependency and
are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:32:42 +02:00
darmanandClaude Opus 5 5a4588c532 gitea: provision a ci-bot account with repo + branch-protection access
Workflows push as a dedicated ci-bot account rather than a human one, so its
PAT can be scoped, rotated and revoked on its own. Adding a repo to
`ciBotRepos` and redeploying is all it takes to grant access.

Collaborator access and branch-protection push-whitelisting exist only on
gitea's HTTP API — no CLI, no config-file surface — so this one part stays
imperative: a oneshot that PUT/PATCHes the API into the desired state. It
runs on deploys where the script changed, which means it won't self-heal a
revert done through the web UI unless the unit is restarted too.

Two secrets, deliberately distinct:
- gitea_provisioning_token is darman's own token (write:repository +
  write:user). Only an owner-scoped token clears reqOwnerCheck on the
  collaborator and branch-protection endpoints, and write:user is what lets
  it write the Actions secret below. ci-bot cannot grant itself access.
- gitea_ci_bot_token is ci-bot's push token, generated once by hand (the
  command is in the comment) and pushed into gitea as a user-level Actions
  secret CI_BOT_TOKEN. Gitea has no instance-wide secret scope, and every
  repo here is owned by darman directly rather than an org, so a user-level
  secret is the closest thing — repo-level lookups fall back to it.

Branch protection is applied to the default branch plus `develop`, since
version-bump.yml pushes there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:32:28 +02:00
darmanandClaude Opus 5 c17524e358 terra: hyprchrome glow/shadow, borderless windows, warmer accents
Now that the plugin draws its own outline and glow, hyprland's own border is
redundant — border_size 0 and let hyprchrome own the window edge (outline_size
2 in fg_color). Fill in the rest of its knobs: glow 12/0.85, shadow 24 offset
{4,8} in bg_color.

Accents go warm: bg_accent to a muted red (963c38) and a new fg_accent_alt
(ff9d42) so the active-border gradient runs amber->orange instead of
amber->background. Colour literals lowercased for consistency.

Also re-pick the placeholder wallpaper, and drop the comment explaining
hyprland's lua gradient table format — the surviving call site is now the
only one and reads plainly enough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:32:16 +02:00
darmanandClaude Opus 5 2abd842e97 terra: tome, rootless podman for GPU containers, direnv, kicad
- install tome from pkgs/tome.nix, built against the re-added flake input
- import services/containers.nix and put darman in `render`/`video`:
  /dev/dri/renderD128 is root:render 0660, so a rootless container can only
  reach the GPU if the host user is in the group. Needed by the Vulkan
  whisper.cpp/llama.cpp containers in content-trigger-scanner.
- point DOCKER_HOST at the podman *user* socket and add docker-compose.
  dockerCompat gives a `docker` CLI shim, but compose v2 is its own binary
  talking to a socket, and rootless podman's socket is the user one under
  /run/user/1000 — not root's /var/run/docker.sock.
- direnv + nix-direnv, so per-repo devShells load in the shell and in Rider
  via its direnv plugin, instead of hand-wiring a toolbox SDK per repo
- kicad as a flatpak, alongside the other flatpak desktop apps

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:32:06 +02:00
darman faaf24ddc0 terra: cosmic portals, jupiter smb mount, tea CLI, launcher/theme polish
- swap gtk portal/apps for cosmic (xdg-desktop-portal-cosmic, cosmic-files,
  cosmic-settings) and drop dolphin/protonplus/bambu-studio (bambu-studio
  moved to flatpak alongside the other comms/gaming flatpaks)
- mount jupiter's samba share at /mnt/jupiter (automount, credentials from
  the same samba_password secret jupiter itself uses)
- add tea (gitea's remote API CLI) for talking to git.mgaction.town from
  terra without SSHing into jupiter
- new dark icon themes (Amy, Azure Glassy, Slot Beauty) vendored from
  gnome-look.org tarballs, packaged since pling download links expire
- rishot: fix Qt5Compat.GraphicalEffects QML import (was missing qt6.qt5compat
  on QML_IMPORT_PATH, so quickshell failed at config-load)
- launcher widgets: stop LauncherConsole/LauncherDock from reserving
  compositor space (ExclusionMode.Ignore, they're overlays not real docks);
  bump LauncherCorner app icon size 28->34
- comms script: launch telegram/discord via flatpak, not native binaries
- nix-ld + boot.binfmt aarch64 emulation (for building/flashing mercury
  from terra)
2026-07-29 21:43:43 +02:00
darman 29ddd0cb7c neptun: stop processing router advertisements on eth0
Addressing is fully static, but netcup's router still sends periodic RAs on
this segment; the kernel then tries (and fails, since the static route
already exists) to install its own default route from them, spamming
"ndisc_router_discovery failed to add default route" on the console.
2026-07-29 21:43:11 +02:00
darman 63ca6f8409 jupiter: enable gitea Actions + register a jupiter runner
Runner registers against the same gitea instance and runs jobs in podman
containers (services/containers.nix), one image per runs-on label using the
catthehacker act-compatible images. Registration token comes from gitea
itself (gitea actions generate-runner-token) and is stored in
secrets/jupiter.yaml, rendered into a TOKEN=... env file via sops.templates
since gitea-actions-runner takes an EnvironmentFile, not a raw secret path.
2026-07-29 21:43:05 +02:00
darman 78dcdb6f57 jupiter: cap systemd-boot generations at 2 (eMMC space)
common.nix's cap of 5 comes from this box's own 34-generation incident, but
at ~5G free on a 29G eMMC even 5 is too many.
2026-07-29 21:42:59 +02:00
Erik SimonandClaude Sonnet 4.6 e6c6685d96 terra: flatpak, unstable packages, GTK dark theme, comms workspace
- nix-flatpak input; discord, telegram, qbz as Flathub flatpaks; removes
  qbz and proton-pass-cli flake inputs
- proton-pass-cli and claude-code from nixpkgs-unstable via extraSpecialArgs
- dconf color-scheme = prefer-dark replaces per-session gsettings call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 01:29:25 +02:00
Erik Simon 3295fbbf0b terra: desktop setup, flatpak, unstable packages, key management
- Hyprland workspace rules: start-communications.sh launches telegram +
  discord into special:communications; qbz/discord/telegram switched to
  flatpak (nix-flatpak, Flathub) — removes qbz and proton-pass-cli flake
  inputs
- proton-pass-cli and claude-code sourced from nixpkgs-unstable; unstable
  pkgs set threaded into home-manager via extraSpecialArgs
- GTK/libadwaita dark theme fixed: dconf color-scheme = prefer-dark written
  declaratively instead of a per-session gsettings call
- scripts/keys: store/restore SSH host keys and sops age keys via Proton
  Pass (ssh_host#<config> / age#<config> / age#admin naming)
2026-07-25 01:26:52 +02:00
darman ffeb6c1007 fix 2026-07-24 21:12:17 +02:00
darman 4b3f790cd0 terra btrfs 2026-07-24 20:38:36 +02:00
darmanandClaude Sonnet 5 eac20f5e0a terra: add proton-pass-cli via flake input
Not in nixpkgs; packaged by github:tomsch/proton-pass-cli-nix. Used by
./scripts/deploy to autofill sudo/ssh passwords from the "HomeLab" vault.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:34:12 +02:00
darmanandClaude Sonnet 5 6e9d588f00 terra: migrate quickshell config into repo, add quickshell + opencode packages
Config was symlinked from ~/.dots/quickshell (separate dotfiles repo); now
tracked here and applied via home-manager xdg.configFile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 23:22:03 +02:00
darman 67b12cb96b terra: add Ryzen 9 5900X desktop (Hyprland, tailnet, dev tools)
Replaces CachyOS on the OS SSD (Kingston SA400, disko-managed). Dev-data
disks (sdc ext4 /mnt/hdd_01, LVM vg_ssd /mnt/ssd_01) stay out of disko and
are mounted as plain filesystems so they're never wiped. Desktop split into
services/desktop/desktop-hyprland.nix (session: compositor, greeter, audio,
portals) and desktop-apps.nix (things darman actually launches, including
claude-code — allowlisted alongside the other unfree desktop apps).
2026-07-23 23:14:36 +02:00
darman a4c7768625 immich: fix OIDC clientId, redirect logout to immich's own login page
clientId was still the placeholder "immich" instead of Authentik's actual
generated id, and the sops secret it points at (immich_oauth_client_secret)
was never declared on jupiter. Wire both up, and set endSessionEndpoint so
logout lands back on immich instead of Authentik's "logged out" page.
2026-07-23 23:14:11 +02:00
darman 7bcea764f6 Added immich VHOST to neptun 2026-07-21 00:53:29 +02:00
darman 6ce61ab519 immich: add the service and import the ZimaOS library
jupiter had a leftover docker-compose Immich on the RAID (/mnt/data/Immich,
9.9G) that survived the NixOS install. Native module now, media at
/mnt/data/AppData/immich, caddy vhost on 2283 with a 50GB body limit
(caddy's default rejects video uploads).

The package comes from nixpkgs-unstable, the module from the 26.05 pin:
26.05 ships immich 2.7.5, but that database was last written by 3.0.0 and
migrations only run forward --

  corrupted migrations: previously executed migration
  1776217577402-DropAuditTable is missing

Safe because the two module files are byte-identical at these revisions;
services/media/immich.nix carries the diff command to re-check on a bump.
Drop the input once the stable pin ships >= 3.0.0.

immich needs group "users" only to traverse /mnt/data/AppData (drwx--x---);
its own dir stays 0700 immich:immich. mediaLocation is outside /var/lib, so
the module's tmpfiles entry only ADJUSTS it -- add a rule that creates it.

scripts/immich-import-legacy-db does the database half: boots a copy of the
legacy PGDATA under the matching image (PG14 + vchord 0.3.0 + pgvector
0.8.1), dumps it with the local pg_dump 17, restores into a scratch DB,
fixes ownership, and only swaps after confirmation. Never touches the
original. The old cluster ran VectorChord, not pgvecto.rs, so the smart
search and face embeddings survive -- no ML re-run.

Imported: 666 assets, 25 people, 647 clip + 359 face embeddings, 2 users.
2026-07-21 00:51:10 +02:00
darmanandClaude Opus 4.8 0995a5fe2f headscale: move the tailnet to orbit.sol, route all DNS through pihole
Three connected changes, all triggered by the same outage.

base_domain leaves mgaction.town. That zone has a wildcard A+AAAA pointing
at neptun, and DNS wildcards match multi-label names, so
jupiter.hosts.mgaction.town resolved publicly to NEPTUN and Caddy proxied
to itself -- a silent loop rather than a lookup failure. Nesting the
tailnet inside the LAN domain as orbit.sol keeps the theme and resolves
unambiguously, since tailscale matches routes by longest suffix.

override_local_dns = true with pihole as the only global nameserver, so
roaming devices get ad blocking and .sol names off-LAN. With it false,
globalResolvers land in the netmap's FallbackResolvers, which a phone
with carrier DNS never consults. No public fallback is listed on purpose:
tailscale treats the list as a set, so a second entry would let queries
slip past the filter whenever mercury is slow. The cost is that mercury
is now a single point of failure for tailnet DNS.

neptun and mercury opt out individually. mercury would otherwise resolve
through itself. neptun must not depend on a Pi behind a domestic line to
renew the certificates for the control server every other node needs --
and it is circular besides, since tailscaled has to resolve
vpn.mgaction.town to connect at all. Instead neptun runs a dnsmasq stub
forwarding just orbit.sol to MagicDNS on 100.100.100.100, which tailscaled
answers whenever it is running regardless of --accept-dns. That resolves
jupiter live, so the hardcoded /etc/hosts pin is gone.

Also sets dns.nameservers.split explicitly: nixpkgs renders its own
dns.split option one level too high, but headscale reads
dns.nameservers.split (hscontrol/types/config.go:722) and so does
headplane, whose DNS page dies on the missing key with "Cannot convert
undefined or null to object". The module's option is dead as written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:01:47 +02:00
darmanandClaude Opus 4.8 564dfb16b8 headscale: add OIDC login, stop overriding clients' local DNS
Two changes to the control server, plus the fallout on the hosts.

OIDC via Authentik, so `tailscale up --login-server ...` opens a browser
instead of needing a pre-auth key. This is a second Authentik application,
separate from headplane's, with headscale's own /oidc/callback redirect.
Headless hosts keep using pre-auth keys. Note that headscale runs OIDC
discovery at startup and a failure is FATAL -- pointing `issuer` at an
application that does not exist yet means the control server will not
boot, so verify the discovery document before deploying.

override_local_dns = false, because the upstream default of true replaces
resolv.conf with 100.100.100.100 on every node. That silently broke the
LAN's `.sol` names -- pihole serves those and the global nameservers
return NXDOMAIN for them -- and took ad blocking down with them. It also
made each node's entire DNS depend on tailscaled, which is what had
forced --accept-dns=false onto neptun and mercury individually; both of
those workarounds are now removed, and with MagicDNS resolving properly
again neptun no longer needs its hardcoded /etc/hosts pin for jupiter.

Also serves jellyfin and seerr from jupiter, matching the ports they
already use on its LAN vhosts, and rotates the tailnet pre-auth keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:12:31 +02:00
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 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 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
erik ac05d948b6 refactor: rename vps host -> neptun (solar-system theme)
git-mv hosts/vps->neptun, secrets/vps.yaml->neptun.yaml; update flake, .sops.yaml
rules, hostName, sops paths, README/CLAUDE. Off-repo host key dir renamed too.
2026-07-14 13:32:44 +02:00
erik a6e6f0d5d4 feat(mercury): static IPv6 (fd18:df17:9078:0::10) for IPv6 DNS 2026-07-14 13:07:37 +02:00
erik fd3ccf5f07 feat(mercury): pihole via container (native FTL segfaults on aarch64)
- services/pihole.nix: official pihole/pihole:2026.07.2 via podman, host net,
  caps NET_ADMIN/NET_RAW/SYS_NICE/CHOWN; FTLCONF_* env config (upstream unbound,
  DHCP 50-200, static lease jupiter, .sol domain, local records)
- unbound: resolveLocalQueries=false (was hijacking resolv.conf to :53 -> boot
  DNS deadlock; the real root cause of the earlier failures too)
- password via sops FTLCONF env file; /var/lib/pihole created via tmpfiles
- VM-verified: mercury.sol/jupiter.sol/external all resolve, 0 restarts
2026-07-14 00:10:43 +02:00