Commit Graph
102 Commits
Author SHA1 Message Date
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 19c65e1921 feat(pihole): wildcard *.jupiter.sol -> jupiter (VM-verified) 2026-07-14 13:24:22 +02:00
erik 9feecd8daa docs: add CLAUDE.md (layout, deploy commands, secrets model, gotchas) 2026-07-14 13:19:35 +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 20a16fda99 fix(pihole): pin image to 2026.07.2 (sed failed in prior commit) 2026-07-14 00:11:08 +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
erik c2ac7e3af4 fix(pihole): local DNS records for mercury.sol + jupiter.sol (dns.hosts)
mercury is the DHCP server (no lease of its own) so its name wasn't resolvable.
Add explicit dns.hosts A records. VM-verified: both resolve, external unaffected.
2026-07-13 22:58:41 +02:00
erik b0ad211a92 fix(mercury): sops age key on root fs (/var/lib/sops-nix/age.txt)
Pi's vfat partition isn't mounted at runtime (u-boot reads it pre-boot), so
/boot/firmware doesn't exist -> keyFile moved to the always-mounted root fs.
deploy flash now drops it on the ext4 root partition.
2026-07-13 22:53:43 +02:00
darman dd6e093ca4 chore: set mercury secrets 2026-07-13 22:42:20 +02:00
erik 22a3eee85b feat(mercury): pihole web password from sops via FTLCONF env override
pihole.toml is nix-managed read-only so 'pihole setpassword' fails. pihole-FTL
reads FTLCONF_* env vars (override the toml) — render an env file from the sops
secret pihole_webpassword and feed it via EnvironmentFile. Password stays out of
repo/store. VERIFIED in VM: FTLCONF_webserver_api_password -> API login works.
2026-07-13 22:40:12 +02:00
erik 750821f43a test: add mercury-vm (x86 qemu) to validate pihole+unbound before SD flash 2026-07-13 22:02:34 +02:00
erik 70a851c71d feat(mercury): switch to pihole-ftl (declarative static leases)
- services/pihole.nix: DNS adblock + DHCP, upstream unbound, .sol domain,
  jupiter static lease 00:e0:4c:3c:a3:1f -> 10.0.0.20 (declarative!)
- drop services/adguardhome.nix; mercury imports pihole
- admin password set via 'pihole setpassword' post-boot (not in repo)
2026-07-13 21:56:08 +02:00
erik 2f89a5c5e7 feat(deploy flash): auto-install sops age key onto the SD boot partition
- after dd, if ~/.config/homelab/<config>/age.txt exists, mount the FAT boot
  partition and drop it as sops-age.txt (mercury). Key stays off-repo + out of
  the store + out of the image; no manual mount step.
2026-07-13 21:02:34 +02:00
erik 1937d59b2c feat: per-host darman passwords via sops; mercury sops; AdGuard pw via UI
- per-host darman_password (distinct hash) in secrets/{jupiter,vps,mercury}.yaml
  -> hashedPasswordFile; different console password per host (ssh still key-only)
- mercury: dedicated age key (on boot partition post-flash), sops-nix wired
- AdGuard: module has no secret hook + writable config -> mutableSettings=true,
  admin password set via web setup on first boot (never in repo/store)
2026-07-13 20:52:06 +02:00
erik b989490333 fix(deploy flash): validate device before building the image 2026-07-13 20:44:38 +02:00
erik bcd2e6ebf2 feat(deploy): add image (build SD) + flash (build+dd to device) subcommands 2026-07-13 20:44:04 +02:00
erik 5a23638f75 feat(mercury): AdGuard Home DNS adblock + DHCP, forwards to unbound
- services/adguardhome.nix: upstream unbound, .sol local domain, DHCP 50-200
- nix-owned config (mutableSettings=false); adlists + dns declared
- mercury imports adguard + unbound; drop the pick-one placeholder block
- TODO: bcrypt admin password, jupiter static lease via UI (needs its MAC)
2026-07-13 20:13:01 +02:00
erik 587fcbfa3d feat(mercury): set real IP 10.0.0.10; add services/unbound.nix recursive resolver 2026-07-13 20:01:55 +02:00
erik 894f88c71e feat: scaffold mercury (rpi 3b+ aarch64 SD image) for DNS/DHCP
- nixosConfigurations.mercury: aarch64, sd-image-aarch64, imports common.nix
- static net placeholders (CHANGE-ME), hostname mercury
- DNS service left undecided: commented pihole (services.pihole-ftl) + adguardhome
- no sops yet (add with the service if it needs a secret)
2026-07-13 19:49:45 +02:00
erik b6c393ff98 refactor: move host configs under hosts/{jupiter,vps}
- git-mv jupiter/ vps/ into hosts/; fix ../ -> ../../ for common/services/secrets
- flake.nix + deploy point at hosts/<config>/
- README structure updated
- verified: jupiter/vps/vbox all eval
2026-07-13 19:34:27 +02:00
erik fb782cb9fe refactor: split services into reusable services/ modules
- services/{samba,avahi,audiobookshelf,containers,caddy,tailscale}.nix
- common.nix grows firewall base + timezone; hosts import what they need
- jupiter/vm/vps import service modules; drop the jupiter/services.nix monolith
- each module opens its own firewall ports; caddy/tailscale shared by hosts
- verified: jupiter/vps/vbox eval + jupiter builds, config equivalent
2026-07-13 19:13:11 +02:00
erik 2b170af346 chore(scripts): move deploy+edit_secrets to scripts/, resolve repo root via git
- scripts now find the flake root regardless of cwd or where they live on PATH
- gitignore .env
2026-07-13 18:58:25 +02:00
erik ec309c8fe1 refactor(deploy): generalize to any config, all params mandatory (no defaults) 2026-07-13 01:05:28 +02:00
erik 946c44f1d3 feat: scaffold netcup vps host (disko/vda, static net, tailscale, caddy)
- new nixosConfigurations.vps: UEFI systemd-boot, disko on /dev/vda
- static IPv4 159.195.64.117/22 gw .1 (+ IPv6), eth0 pinned, public DNS
- tailscale via headscale + sops authkey (secrets/vps.yaml, own host key)
- caddy public reverse proxy: audiobookshelf.mgaction.town -> jupiter tailnet:8000
- shared common.nix (user/ssh/nix) ; .sops.yaml per-host rules
2026-07-13 01:01:53 +02:00
erik 25c6982cea feat(audiobookshelf): bind 0.0.0.0 so it's reachable over tailscale (VPS proxy) 2026-07-13 00:26:16 +02:00
erik 5dddc9d2fb feat: tailscale via headscale (vpn.mgaction.town) + fix audiobookshelf extraGroups syntax
- services.tailscale auto-registers with headscale using a sops pre-auth key
- trust tailscale0 so LAN services are reachable over the tailnet
- fix missing semicolon on audiobookshelf extraGroups
2026-07-13 00:17:41 +02:00
erik 101bb948ce feat: add audiobookshelf (native service + caddy vhost) 2026-07-13 00:05:54 +02:00
erik d83de7e37c fix(boot): reboot=pci — warm reset hangs at firmware on this board 2026-07-12 23:54:01 +02:00
erik 1f5728b7ee fix(nix): trust @wheel so remote deploy can push unsigned closures 2026-07-12 23:17:21 +02:00
erik 9db059df5a feat: USB recovery installer ISO (ssh key baked in) for headless reinstall 2026-07-12 22:42:29 +02:00
erik d105aa5908 fix(boot): pin eMMC initrd modules in configuration.nix (survive hw-config regen)
Installed initrd lacked mmc_block -> can't mount root on eMMC -> no boot.
generate-config runs in the RAM installer and misses mmc modules, and it
overwrites hardware-configuration.nix, so pin them where they merge instead.
2026-07-12 22:40:59 +02:00
erik 7200ab5836 fix(deploy kexec): ship static cpio+gzip (ZimaOS lacks them); single-password multiplex
kexec/run rebuilds an initrd via cpio|gzip from PATH — absent on ZimaOS, so run
aborted before kexec and the box stayed on ZimaOS. Ship GNU static cpio + busybox
(as gzip) into /tmp/bin, prepend PATH. SSH ControlMaster = one password prompt.
2026-07-12 22:25:58 +02:00
erikandClaude Opus 4.8 f581203b57 feat: custom kexec installer for headless install on read-only-root (ZimaOS)
- add nixos-images input; nixosConfigurations.kexec bakes in the ssh login key
- build via config.system.build.kexecInstallerTarball
- deploy: ./deploy kexec <host> streams the installer to /tmp and kexecs
- works around ZimaOS RO root where nixos-anywhere ssh-copy-id fails

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:14:35 +02:00
erik 95e945fa6f feat: authorize erik ssh key + add deploy script (install/switch) 2026-07-12 21:45:14 +02:00
erik ba31568efc fix(hardware): add eMMC (sdhci/mmc) initrd modules so root mounts on ZimaBlade 2026-07-12 21:37:55 +02:00
erik 5239f1ce71 chore: set real samba + darman secrets; edit_secrets zeditor support 2026-07-12 21:35:58 +02:00
erik 98c6390ede fix(edit_secrets): guarantee a working editor (bundle nano, --wait for GUI) 2026-07-12 21:32:12 +02:00
erikandClaude Opus 4.8 dd6c4829ad feat: darman login password via sops (hashedPasswordFile)
- add darman_password (sha-512 hash) to secrets/jupiter.yaml
- neededForUsers so it lands in /run/secrets-for-users before user setup
- real host uses hashedPasswordFile; VM keeps its test initialPassword

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:22:00 +02:00
erik 7a97abeb25 chore: add edit_secrets helper for sops 2026-07-12 21:10:56 +02:00
erikandClaude Opus 4.8 94b1460bc7 feat: wire ZimaBlade real disks (eMMC OS + md0 RAID0 data)
- disko OS target = internal eMMC /dev/disk/by-id/mmc-C9A551_0xaa057ad0
- mount existing ext4 on mdadm RAID0 (md0) at /mnt/data by fs-uuid, nofail
- boot.swraid.enable to assemble the array; data disk kept out of disko

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:59:25 +02:00
erikandClaude Opus 4.8 f9358375b5 feat: sops decrypts via pre-generated SSH host key (works on boot #1)
- pre-generate jupiter SSH host key off-repo (~/.config/homelab/jupiter)
- derive age recipient via ssh-to-age, add to .sops.yaml, re-encrypt secret
- secrets.nix: sops.age.sshKeyPaths = host key (drop separate keyFile)
- gitignore private-key patterns
- README: ship host key via nixos-anywhere --extra-files; secret decrypts boot #1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:48:33 +02:00
erikandClaude Opus 4.8 10387fdbee feat: sops-nix for samba password secret
- add sops-nix input + module (jupiter only, not the VM)
- secrets/jupiter.yaml: age-encrypted samba_password (safe to commit)
- .sops.yaml: encryption rule for admin age key
- secrets.nix: decrypt samba_password to /run/secrets on the host
- provisioning oneshot reads sops secret (host) or plaintext (VM), single value
- .sops private key stays off-repo (~/.config, /var/lib/sops-nix on host)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:16:17 +02:00
erikandClaude Opus 4.8 9fb32fd454 feat: disko OS-disk layout + nixos-anywhere install flow
- add disko input; jupiter partitions/formats OS disk declaratively
- hardware-configuration.nix carries kernel modules only (disko owns fileSystems)
- data disk stays a plain unformatted mount, out of disko
- vbox unchanged (virtualbox-image supplies its own disk)
- README: nixos-anywhere remote install + daily rebuild loop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:44:05 +02:00
erikandClaude Opus 4.8 27e9aaf787 chore: drop build-ova.sh (Docker build path, superseded by native nix build)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:41:56 +02:00