Commit Graph

217 Commits

Author SHA1 Message Date
e615599a2c revert test 2026-06-01 23:41:19 +02:00
19b2109efc test 2026-06-01 23:41:06 +02:00
10fdb0fe09 revert: test commit 2026-06-01 23:34:32 +02:00
946ded6d08 test credential 2026-06-01 23:34:19 +02:00
95a3ae75c3 hermes-agent: update model to deepseek/deepseek-v4-flash (tool-use support) 2026-06-01 23:28:43 +02:00
9be6f7f31d openrouter-api-key: seed real key 2026-06-01 23:24:36 +02:00
root
5f929ce3ee openrouter-api-key: seed real key 2026-06-01 23:09:41 +02:00
root
76bbe99798 client-add: mac-mini (finalize age_pubkey + grant shared + hermes secrets) 2026-06-01 22:50:36 +02:00
543745cad9 network: retire Slate AX; add SODOLA switch migration plan and network topology doc
- infrastructure/network.md: permanent topology reference — VLAN 1 (main LAN)
  + VLAN 10 (homelab 192.168.8.0/24), Fritz!Box 7590 as VLAN router,
  SODOLA 5-Port 2.5Gbit as homelab switch
- plans/2026-06-01-slate-ax-to-sodola-migration.md: full migration runbook
  (pre-flight, Fritz!Box second-network config, SODOLA VLAN table, cutover
  procedure, verification commands). Gateway 192.168.8.1 is unchanged —
  no LXC/VM config edits needed. Fritz!Box DHCP for VLAN 10 hands out
  192.168.8.2 (Technitium CT 107) as DNS, closing the outstanding item in dns.md.
- plans/index.md: establishes plans/ convention (Planned → In Progress → Done)
- CONTRIBUTING.md: plan page template + linking discipline for plans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 22:48:02 +02:00
ce6df85bb5 Merge pull request 'hermes-agent: onboard Nous-Hermes-on-Goose to homelab clients' (#1) from hermes-onboarding into main
Reviewed-on: dtoro/Homelab-Docs#1
2026-06-01 22:27:09 +02:00
f25d9e0648 hermes-agent: onboard Nous-Hermes-on-Goose to homelab clients
`bootstrap.sh --with-hermes` installs the Goose CLI, drops a Goose
config pinning the OpenRouter provider + Nous Hermes model + the
homelab MCP extension, symlinks `bin/hermes` and HERMES.md, and links
HERMES.md as `.goosehints` so the persona is injected as the system
prompt every session.

`bin/hermes` decrypts `secrets/openrouter-api-key.yaml` via the existing
`homelab secret` flow and execs `goose session`.

`homelab client add --with-hermes` grants the new sops secret to the
host's age_pubkey at finalize time (parallel to the existing
shared-secrets grant). `client remove` revokes it.

`operations/hermes-agent.md` covers the end-to-end flow, verification,
troubleshooting, and queues one follow-up: the MCP server still runs
SSE-only but Goose 1.x deprecated SSE — the Goose config targets
`streamable_http` and the `homelab` extension won't connect until
`mcp/server.py` migrates. The `developer` extension (shell + edit +
`homelab` CLI) carries the agent in the meantime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 22:27:09 +02:00
563dbe21b1 arch-migration P2: Technitium dns LXC (107); retire LXC 124
- containers/107-dns.md: new Technitium DNS server page
- dns.md: dnsmasq -> Technitium changelog; flags 3 overlapping DNS sources + router DHCP action
- 124-authentik: marked RETIRED (shut down)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:10:18 +02:00
4b713d7805 arch-migration P1: document sso.hubris.network outpost callback domain
Domain-level forward-auth needs its own external_host domain when the IdP core
and outpost are on different hosts. sso.hubris.network -> Caddy -> LAN outpost.
Includes the redirect_uris-regeneration gotcha. Carry the DNS record into
Technitium in DNS Phase 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:45:25 +02:00
2762dcbdb8 arch-migration P1: dedicated forward-auth outpost (LXC 106)
- containers/106-auth-outpost.md: new LAN outpost page
- 124-authentik: forward-auth cut over, Authentik stopped (DNS-only now)
- investigation: forward-auth outpost marked RESOLVED

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:33:26 +02:00
8dbba323c1 authentik: migrate from LXC 124 to VPS; eliminate mesh bootstrap deadlock
Moved Authentik (2026.2.2 -> 2026.5.2, +Redis, dedicated auth Docker net)
off LXC 124 onto the VPS so netbird-mgmt's OIDC dependency no longer requires
the mesh it authenticates. depends_on: service_healthy makes the deadlock
structurally impossible. Full Postgres DB migrated (users/apps/passwords/groups).

- investigations/2026-05-31-authentik-vps-migration.md: full writeup + lessons
- 124-authentik: migration banner + changelog (now legacy; dnsmasq stays)
- dns: auth.hubris.network -> 82.165.190.79; NetBird resolver cache gotcha
- ingress: auth served by local container via Docker-provider labels (not cert-mirror)
- mesh: follow-up entry superseding the morning band-aid; bootstrap note obsolete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:17:00 +02:00
root
04d1f39e7b mesh: document 2026-05-31 recovery + auth.hubris.network Traefik route
Management crash-loop root cause and fix documented.
Phase 6 (Authentik as Netbird IdP) marked complete and live.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:05:55 +02:00
b34d362c19 photos: mule-image cutover to PhotoPrism stack on 120, retire 127
Mulimage 2.0 merge (dtoro/mule-image 70dc1b6) replaces the legacy
FastAPI/Celery/Postgres stack with PhotoPrism + Go sidecar + MariaDB +
SvelteKit. LXC 120 now runs that architecture in production; LXC 127
(the M0 evaluation host) was destroyed in the same session.

- inventory.yaml: drop mule-photos-new host + photos_new service.
- containers/index.md: bump 120's row (6 cores / 12 GiB / iGPU
  passthrough), move 127 to Recently-destroyed.
- containers/120-mule-images.md: rewrite stack + library-access +
  auth sections; add 2026-05-22 cutover changelog entry.
- containers/127-mule-photos-new.md: add closing changelog entry and
  decommission banner; rest left intact as archaeology.
- hosts/mule-photos-new.yaml: removed (regenerated from inventory.yaml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 00:53:34 +02:00
0260f01b74 render-vps-configs: drop _comment from management.json template
netbird mgmt might reject unknown top-level JSON keys depending on
parser flags. The rendering note stays in turnserver.conf (# comments
are valid INI syntax). After this, the rendered management.json will
be byte-identical to what's currently live on the VPS, so the dry-run
will show /opt/management.json as unchanged.
2026-05-21 22:30:46 +02:00
b12f80933d render-vps-configs: print masked unified diff in --dry-run
Lets the operator see exactly what would change on the VPS before
applying. Secret values from the decrypted sops files are masked as
<REDACTED> so the diff is safe to paste into chat/PRs.
2026-05-21 22:29:10 +02:00
8ef17dba3d sops-encrypt TURN password + Authentik client secret; homelab render-vps-configs
The IONOS netbird VPS held two credentials in plaintext that were the last
holdouts from the homelab's sops+age secrets pattern:
  - /root/turn-pass.txt  (coturn long-term-credential password)
  - PKCEAuthorizationFlow.ProviderConfig.ClientSecret inline in
    /opt/management.json (Authentik OIDC client secret)

This commit moves both into sops-encrypted YAML in the repo and adds a render
command that recreates the VPS config files from templates + decrypted secrets:

* secrets/turn-shared-secret.yaml      — encrypted `password: <coturn pwd>`
* secrets/netbird-authentik-oidc.yaml  — encrypted `client_secret: <...>`
  Both recipients = hubris + apps + republic-laptop (same 3 as hello.yaml).
* vps/turnserver.conf.tmpl + vps/management.json.tmpl — templates with
  {{TURN_PASSWORD}} + {{AUTHENTIK_CLIENT_SECRET}} placeholders.
* bin/homelab new subcommand `render-vps-configs`:
  - Decrypts both secrets locally (works on any recipient).
  - Substitutes placeholders into templates.
  - Diffs against current VPS state via ssh, prompts, applies atomically
    (write `.new` then mv), restarts coturn + netbird-mgmt.
  - --dry-run + -y flags. Hops through hubris when not running on hubris
    itself, since VPS sshd is locked to hubris's pubkey.
* inventory.yaml adds the VPS as `kind: external` (new kind; reserved for
  ssh-managed hosts that aren't homelab clients themselves — no age key,
  no /opt/homelab-context). hosts/netbird-vps.yaml regenerated.
* SHARED_SECRETS list includes both new secrets so re-keys on enrollment
  changes pick them up automatically.

After this lands + the 5-min sync propagates to hubris, run from hubris PVE
shell (or any client; hubris just skips the extra ssh hop):

  homelab render-vps-configs --dry-run    # see plan, no changes
  homelab render-vps-configs -y           # apply + restart services

Once verified working, the plaintext `/root/turn-pass.txt` should be deleted
on the VPS (the rendered /etc/turnserver.conf no longer needs it as a
reference).
2026-05-21 22:21:06 +02:00
21063015c7 homelab CLI: per-host ssh.user + LAN→mesh fallback; wiki for netbird-ssh JWT issuer fix
Three coordinated changes addressing follow-ups from the 2026-05-21 netbird vanilla
migration, plus a related off-LAN ergonomics fix:

bin/homelab:
- New ssh_target(name, force_mesh=False) helper resolves (addr, port, user)
  from inventory, honoring ssh.netbird_port (forces mesh path) and ssh.user
  (default "root"). Falls back to the netbird FQDN when LAN IP fails a
  cached 1.5s TCP probe — helps off-LAN operators on VPN/symmetric-NAT
  paths where the netbird subnet route doesn't reach 192.168.8.0/24.
- New ssh_base() builds the full `ssh ... user@addr` invocation; hubris_ssh()
  is now a back-compat shim. cmd_ssh, cmd_logs, cmd_restart, cmd_nuke
  refactored to use it — no more hardcoded "root@" anywhere.

inventory.yaml:
- New ssh.user convention (root by default, explicit per workstation).
- hubris.ssh.user=root (explicit, documents convention).
- republic-laptop, mac-mini: ssh.user=dtoro. ludo-mini left default (TODO).
- Comment block in the header explains the field + why it exists (netbird-ssh
  defaults to LOCAL username; "user not found" on LXCs is the gotcha).
- hosts/*.yaml regenerated from build_host_files.py.

infrastructure/mesh.md:
- Migration changelog entry updated: Device Code Stage is now configured
  (was "NOT yet" — landed in d41d73f); --setup-key no longer required.
- New subsection documenting the post-migration JWT-issuer cache bug:
  client/internal/engine_ssh.go's updateSSH() bails out when sshServer is
  already running, so systemctl restart and netbird down/up don't refresh
  the SSH JWT validator. Full daemon stop/start is the fix.
- Companion username gotcha (`netbird ssh` defaulting to local username).

operations/agent-enrollment.md:
- Three new troubleshooting rows: JWT-issuer cache, user-not-found, and
  LAN-unreachable-from-mesh-peer (the new homelab CLI behavior).

Verification: ssh_target resolution against the live inventory yields
- hubris → ssh -p 22022 root@proxmox-server.netbird.selfhosted (mesh-forced)
- jellyfin/gitea → ssh root@192.168.8.x (LAN reachable, probe passed)
- republic-laptop/mac-mini → ssh dtoro@<fqdn> (per ssh.user)
- ludo-mini → ssh root@<fqdn> (default)
2026-05-21 21:57:19 +02:00
d41d73f323 device-code onboarding: bootstrap auto-installs deps + netbird; Authentik device flow wired
Two changes that together collapse new-workstation onboarding from ~7 steps
to ~2 commands:

* bootstrap.sh:
  - Dep-check now AUTO-INSTALLS missing tools (apt/dnf/brew) instead of
    printing instructions and exiting. Re-verifies after install.
  - New pre-mesh-check block: if netbird isn't installed, installs it
    from the netbird apt/dnf repo (or `brew install --cask netbird` on
    Darwin), then if mgmt isn't connected, runs `netbird up
    --management-url=https://netbird.hubris.network --ssh-jwt-cache-ttl 86400`.
    Operator clicks the device-code URL inline. Waits up to ~30s for
    Management: Connected before continuing. Skipped on --no-secrets +
    --dry-run.

* containers/124-authentik.md: replaces the "KNOWN MISSING — Device Code
  Stage" subsection with a working recipe — Authentik 2026.2 routes
  /device via a BRAND-level "Device code flow" field, not a provider
  field. Documented stage bindings for a `default-device-code-flow`
  flow (identification → password → user-login → consent) and the
  brand-level binding step.

* operations/agent-enrollment.md: Path B (interactive `netbird up`) is
  now the default; Path A (setup-key) demoted to "headless/scripted"
  alternative. "Install dependencies" section collapsed into a note
  that bootstrap handles it, with the manual recipes kept in a
  collapsible <details> block for air-gapped use.

The flow uniquely available to lab owners (single Authentik user today)
still relies on the first-time-owner sqlite promotion documented in
124-authentik.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:27 +02:00
b42a986cc0 wiki: document 2026-05-21 netbird vanilla migration
Updates to four pages reflecting the combined → vanilla mgmt+signal+relay+coturn
cutover and the IONOS-3478-firewall-exception discovery:

* infrastructure/mesh.md — rewrites the ICE/STUN section to cover the new
  TURN endpoint, the IONOS upstream TCP-3478 filtering (load-bearing,
  undocumented before today), and the verification probe. New changelog
  entry covering the migration outcome + Device Code Stage gap.

* infrastructure/vps-hardening.md — "At a glance" lists the new 6-service
  docker stack + host coturn. Firewall section notes the new
  `iifname ens6 tcp dport 3478 accept` rule plus the IONOS upstream
  exception. New changelog entry.

* containers/124-authentik.md — replaces the "Netbird IdP integration —
  DEFERRED" section with the LANDED state: Provider details (Public
  client type — Confidential breaks PKCE on the dashboard SPA), the
  first-time owner-promotion sqlite recipe, the missing Device Code
  Stage gap + workaround (setup-keys), and a note that the old 2026-04-22
  pre-work Provider/App is now obsolete and safe to delete. Updated
  changelog (Phase 6 landed).

* operations/agent-enrollment.md — new "Getting onto Netbird" subsection
  explaining the setup-key path (currently the only working flow until
  Device Code Stage lands) and why direct OIDC from the public internet
  fails (auth.hubris.network is mesh-only-reachable). Prerequisites table
  row updated to point at the new section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:48:01 +02:00
7a062bdb6b docs: dpkg-interrupted runbook + apt-fleet ops + non-apt-binary pattern + Claude Code settings + chat-sudo gotcha
Bundles the documentation slice of the apt-sweep backlog:

* operations/runbook-dpkg-interrupted.md (NEW) — Path A (ssh-reachable
  recovery) + Path B (PVE web Shell when the netbird mesh broke
  alongside the dpkg state, as happened during Wave 6 on hubris).
  Closes B2.

* operations/commands.md — new "Fleet apt operations" section
  documenting `homelab apt-audit` and `homelab apt-upgrade`
  (--status / --safe / --force). Adds the dpkg-interrupted runbook to
  Related.

* operations/agent-enrollment.md —
  - new "Claude Code permissions for fleet ops" section with the
    `permissions.allow` snippet (`Bash(ssh -p 22022 *)`,
    `Bash(homelab *)`) for `~/.claude/settings.json`. Closes A3.
  - two new Troubleshooting rows: chat-mode `!` sudo no-tty gotcha
    (G2) and the cosmetic netbird DNS-probe warning.

* infrastructure/auto-deploy.md — new "Custom-built binaries that
  overlap apt-managed paths" section describing the two acceptable
  patterns (epoch-versioned .deb à la caddy 1:2.11.3-hubris1; or
  apt-mark hold) and the discovery path via `homelab apt-audit`'s
  NONAPT column. Closes D3.

Remaining backlog after this commit: A4 (upstream OpenSSH/netbird mux
bug), D1 (apt-mark hold caddy in caddy-conf bootstrap — superseded
in practice by the epoch .deb), E1/E2 (LXC DNS fallback for
tailscale-managed resolv.conf), F1/F2 (vzdump fallback doc; F3 already
shipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:32:06 +02:00
8f76338b05 bootstrap: install mcp CLI via pipx on workstation enrollment
G1 from the apt-sweep backlog. After ssh ControlMaster setup, install
the `mcp[cli]` python package via pipx so `homelab mcp <tool>` works
out-of-the-box on new workstations. Skipped on LXCs / VMs.

Idempotent (`command -v mcp` guard), respects --dry-run, falls back
through brew (Darwin) → dnf → apt for pipx itself if not already
installed. Runs the install as $SUDO_USER (not root) so the binary
lands in the user's pipx env.

This closes one of the "discovered missing dep" gaps from the 2026-05-21
fleet sweep: republic-laptop had pipx-installed homelab CLI but no `mcp`
binary, so `homelab mcp <tool>` died with an instructional message
instead of just working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:25:58 +02:00
306c397ce1 homelab apt-upgrade: add --safe (pct snapshot + vzdump fallback)
F3 from the apt-sweep backlog. `homelab apt-upgrade --safe ...` takes a
pre-upgrade snapshot per LXC before launching the apt run, so rollback is
trivial if anything regresses.

Snapshot strategy per target:
- LXC: try `pct snapshot <id> preupgrade_<timestamp>` first (CoW, near
  instant). If that refuses ("snapshot feature is not available" — the
  failure mode for LXCs with host bind-mounts like `mp0: /mnt/library`),
  fall back to `vzdump <id> --mode snapshot --storage local --compress
  zstd`. Slower but works on bind-mounted LXCs.
- hubris (PVE host): skipped intentionally; no host-level snapshot in v1.

If any snapshot fails the entire run refuses unless --force is passed.
Snapshot rollback hints are printed after launch so the operator has the
recovery path one copy/paste away.

Validated 2026-05-21: py_compile clean; --status mode works on bind-
mounted LXCs (skips snapshot path). Live snapshot test deferred to next
real fleet sweep to avoid leaving stale artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:24:45 +02:00
2ff7263f3d homelab: add apt-audit + apt-upgrade subcommands; fix hubris_ssh
Two new subcommands (B1+H1 + C1+C2 from the apt-sweep backlog):

* `homelab apt-audit [--target HOST]`
  Per-host pre-flight table reporting dpkg-interrupted state, holds,
  upgradable count, non-apt binaries in system paths, and DNS health.
  Exits nonzero if any target has unconfigured packages (would catch the
  latent dpkg-interrupted state we found on caddy + nextcloud before
  touching them).

* `homelab apt-upgrade [--target HOST | --all] [--status] [--force]`
  Launches `apt update && apt upgrade` inside a transient systemd unit
  (`apt-upgrade-<hostname>`) on each target via `systemd-run --collect`,
  so the run survives ssh teardown. Output tee'd to
  `/var/log/homelab-apt-upgrade.log`. Apt configured with
  `Acquire::Retries=3` + `ForceIPv4=true` to absorb transient mirror
  blips (would have prevented the jellyfin ffmpeg7 fetch failure that
  rolled back 131 packages mid-Wave-3).
  Pre-flight audit gate refuses to run on hosts with dpkg-interrupted
  state; override with --force.
  `--status` mode prints each target's unit state and tails the upgrade
  log; safe to run before, during, and after.

Also fixes a latent bug in `hubris_ssh()`: the function joined hubris's
`lan_ip` (192.168.8.77) with the netbird-only port 22022. Port 22022
listens on hubris's netbird interface ONLY — not on the LAN interface,
not even via the netbird subnet route. Result: every off-LAN client
hitting `homelab pct/logs/restart/status` got "Connection refused".
Now uses the netbird FQDN when netbird_port is in use; falls back to
host_address otherwise.

Targets exclude VMs and workstations (they have their own update flows).
Standard apt targets = hubris + every LXC (kind=lxc with a pve_id).

Validated 2026-05-21 on republic-laptop:
- apt-audit reported clean dpkg + 40 upgradable on hubris (the still-
  deferred PVE/kernel packages) + 0 on every LXC, DNS ok across the
  fleet
- apt-upgrade --target mule-photos-new launched the unit, ran apt to
  completion (0 upgrades pending), log captured with =START= / =END
  rc=0= markers, --status reports unit + log correctly
- --all --status fans out across all 16 targets without issue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:08:25 +02:00
35c688c56f bootstrap: install ssh ControlMaster block for netbird peers
After preflight detects MESH_CONNECTED=netbird and the host yaml says
kind != lxc (i.e. workstation, vm, proxmox-host), drop a Host block into
the enrolling user's ~/.ssh/config:

    Host *.netbird.selfhosted
        ControlMaster auto
        ControlPath ~/.ssh/cm/%C
        ControlPersist 2h

This is the real workaround for netbird's SSH JWT cache being flaky in
0.71.2 — we discovered that --ssh-jwt-cache-ttl can leave the daemon in
a state where stale cached tokens get sent and rejected with no fallback
to fresh SSO. ssh ControlMaster bypasses netbird-ssh-proxy entirely for
subsequent sessions: one SSO at the start of a working window covers all
back-to-back ssh / scp / `pct exec` ops until ControlPersist expires.

Validated 2026-05-21 on republic-laptop: ssh #1 prompted SSO once,
sshes #2 and #3 ran in ~1.1s each with no prompt.

Idempotent (sentinel comment check); writes ~/.ssh/cm/ with 700; uses
SUDO_USER's home when invoked via sudo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:53:24 +02:00
dd4072f1d4 bootstrap: enable netbird ssh-jwt-cache-ttl on workstation enrollment
After preflight detects MESH_CONNECTED=netbird, run `netbird down && netbird up
--ssh-jwt-cache-ttl=86400` so ssh into mesh peers (e.g. `ssh -p 22022
root@proxmox-server.netbird.selfhosted ...`) stops triggering device-code SSO
on every connection.

Validated 2026-05-21 on republic-laptop: after one SSO, subsequent ssh
sessions within 24h skip the device-code flow and run instantly. Fleet
operations (e.g. pct exec through hubris into LXCs) reuse the cached JWT.

Notes:
- Flag is supported in netbird 0.71.x+ (netbirdio/netbird#4015). A version
  probe (`netbird up --help | grep ssh-jwt-cache-ttl`) skips the section on
  older clients.
- Flag belongs on `netbird up` (client config), NOT on the daemon's
  ExecStart — putting it there crashes the daemon with "unknown flag".
- Runs LAST in bootstrap, after secrets issuance + MCP wiring, so the brief
  mesh down/up doesn't disrupt earlier steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:12:55 +02:00
root
4c16de8102 secrets-issuance/backup.sh: chmod +x 2026-05-20 21:40:10 +02:00
root
419ab475b1 secrets-issuance backup + homelab doctor smoke-test
Two additions:

1. secrets-issuance backup: daily timer snapshots /var/lib/secrets-issuance
   to /mnt/library/.secrets-issuance-backup/ as a date-stamped tar.gz,
   keeping the last 14 days. Closes the catastrophic-fail-mode where an
   LXC 105 loss wipes every client's age key with no recovery path.
   Caveat: privileged LXCs that mount /mnt/library can read the backup
   (root-uid maps to host root); encrypted-tarball variant is a future
   refinement.

2. homelab doctor: 10 invariant checks for an enrolled client — clone
   present, sync timer/launchd job active, age key perms, CLI symlinked,
   AGENTS.md linked, inventory entry exists, MCP reachable, secrets
   /health responds, sops canary decrypts, git creds present. Returns
   nonzero on any 'fail'. Useful after enrollment or whenever something
   smells off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:37:42 +02:00
root
b4ca21b2b3 inventory: zimaos lan_ip 192.168.8.195 2026-05-20 21:31:36 +02:00
root
dd92c5fd88 inventory: haos has lan_ip 192.168.8.101
Discovered via ARP (MAC 02:E1:73:18:EA:49 from qm config). The Tailscale
FQDN 'homeassistant' is fine for tailscale peers but unreachable from
Netbird peers like republic. lan_ip works from both — Netbird routes
192.168.8.0/24 through hubris.

zimaos (VM 100) remains without lan_ip because its IP wasn't in the
hubris ARP table at audit time and we don't want a network scan. The
zimaos service is still reachable via caddy at zimaos.hubris.network
(verified in 'homelab status' SERVICE column).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:31:03 +02:00
root
36a686d953 inventory: real mesh state per host (no more placeholder netbird FQDNs)
Audit against actual netbird+tailscale peer lists:
  - hubris is the only LXC-host on Netbird; only workstations + hubris
    have netbird entries
  - 10 LXCs+VMs have real Tailscale FQDNs: apps, jellyfin, paperless,
    gitea, nextcloud, elementsynapse, sophia, mule-images→muleimage,
    arriman→arr, haos→homeassistant
  - 7 hosts are LAN-only (no mesh block): nfs-export, caddy, claudio-bot,
    authentik, plato, mule-photos-new, zimaos
  - mac-mini's netbird FQDN corrected to the actual peer name
    (mac-mini-234-17.netbird.selfhosted)

Also: bin/homelab host_address() now prefers lan_ip first — universally
reachable from any LAN client and from any Netbird peer via the
192.168.8.0/24 network resource routed through hubris. Mesh FQDNs are
fallbacks for roaming workstations without a fixed lan_ip.

This makes 'homelab status' from republic show all backends 'ok' instead
of falsely reporting them 'down' against unresolvable netbird FQDNs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:28:03 +02:00
root
e7a74f795d bootstrap.sh: auto-call refresh-creds when the client is a PAT recipient
End of enrollment, if secrets/gitea-pat.yaml is decryptable with the
just-issued age key (i.e. the operator has already run --finalize-pubkey
from another client), upgrade /etc/homelab-context/git-credentials from
the read-only bootstrap PAT to the write-scoped one. Best-effort: fails
silently if not yet a recipient, with a clear hint about what to do next.

Removes the post-bootstrap manual 'homelab refresh-creds' step from the
common flow; falls back to the documented one-liner for first-bootstrap
clients.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:14:03 +02:00
root
6ed04dd1b0 inventory: declare systemd_unit for the underscore-named services
Service keys in inventory use _ for python-attribute friendliness but the
actual systemd units use dashes. Explicit systemd_unit field disambiguates
for MCP tail_log / get_service_status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:24:31 +02:00
root
a5ee017291 homelab-mcp.service: SSH as root, the restricted shell is the boundary
Unit was hard-coding HOMELAB_MCP_SSH_USER=mcp-reader, overriding the new
code's 'root' default. No mcp-reader user exists on hubris — the key is
authorized for root, with a strict command= wrapper. Also pin
HOMELAB_MCP_HUBRIS_HOST + KNOWN_HOSTS env so the service doesn't depend
on the python defaults staying in sync with the unit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:23:49 +02:00
root
3f41416805 mcp/server: log ssh failures so silent empties are debuggable 2026-05-20 20:23:03 +02:00
root
2d2446b36e mcp/server: pre-populated known_hosts for the restricted SSH
The systemd unit's ProtectHome=true blocks ~/.ssh access. SSH then had
no place to write known_hosts (StrictHostKeyChecking=accept-new) and
silently produced empty results. Use /etc/homelab-mcp/known_hosts (which
ProtectSystem=strict still allows reading) and StrictHostKeyChecking=yes.

Operator pre-populates the file via:
  ssh-keyscan -t ed25519 192.168.8.77 > /etc/homelab-mcp/known_hosts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:20:25 +02:00
root
b5dfbb68ae mcp: restricted-shell SSH proxy through hubris for management tools
The 5 management tools (get_service_status, tail_log, list_lxcs,
get_lxc_state, ping_service) were registered with sse but all SSH calls
went to per-host targets via a 'mcp-reader' user that didn't exist
anywhere. New design routes every management call through ONE channel:
LXC 105 -> hubris (SSH key + restricted authorized_keys command), then
hubris pct-execs into the right LXC where needed.

Adds mcp/mcp-reader-shell — a strict allowlist wrapper read from
$SSH_ORIGINAL_COMMAND. Rejects shell metacharacters up front and then
matches against a fixed set of read-only patterns (systemctl is-active/
is-enabled, journalctl -u, pct list/status/config, pct exec for the
same subset). Logged to syslog tag mcp-reader.

Authorized_keys line on hubris:
  command="/usr/local/bin/mcp-reader-shell",restrict ssh-ed25519 ... mcp-reader@homelab-mcp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:18:07 +02:00
root
2599c28104 homelab client add/remove: surgical inventory edits (preserve comments)
yaml.safe_load + safe_dump stripped every comment from inventory.yaml
on each enrollment, eroding the file's documentation value. New
_inventory_set_age_pubkey / _inventory_remove_host / _inventory_append_host
helpers do line-based edits so comments outside the modified region
survive. inventory.yaml gets its top-of-file conventions block back.

build_host_files.py round-trips cleanly (--check returns 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:14:44 +02:00
root
aed977aa56 client-remove: test-victim 2026-05-20 19:29:47 +02:00
root
ea3100f091 client-add: test-victim (finalize age_pubkey + grant shared secrets) 2026-05-20 19:29:36 +02:00
root
5a8d6cfd4f homelab: stop sops-policy scan resetting state on the next rule
Critical bug — the loop reset age_block_last_idx unconditionally on
every '- path_regex:' line. So after finding the target rule's age
block, encountering the NEXT rule wiped the result and the function
returned False. _grant_shared_secrets / _revoke_shared_secrets both
silently no-op'd because of this.

Fix: break out of the scan once we've collected the target rule's
data. Refactored the remove helper to share a find_target_age_lines
inner so the dangling-trailing-comma fixup uses the same logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:29:35 +02:00
root
098d4cfd6e client-add: test-victim (finalize age_pubkey + grant shared secrets) 2026-05-20 19:27:22 +02:00
root
e016f512b2 client-add: test-victim 2026-05-20 19:26:35 +02:00
root
4316acadaa homelab client remove: also revoke pubkey from .sops.yaml rules
The remove flow ran 'sops updatekeys' but never edited .sops.yaml first,
so the removed client stayed a recipient on every shared secret —
exactly the opposite of what 'remove' should do. Adds the
_remove_recipient_from_sops_policy / _revoke_shared_secrets helpers
(inverse of the grant-side ones from the previous commit); cmd_client_remove
now resolves the pubkey from inventory before deletion and feeds it
through that pipeline.

Also adds the sudo re-exec pattern so 'homelab client remove' works
from a non-root user (matching cmd_secret / cmd_refresh_creds /
cmd_client_add).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:26:14 +02:00
root
047138a81d client-add: claudio-bot (finalize age_pubkey + grant shared secrets) 2026-05-20 18:39:47 +02:00
root
af8961d194 bootstrap.sh: LAN probe uses GET not HEAD (issuance only handles GET) 2026-05-20 18:39:11 +02:00
root
ab6b8fabc4 bootstrap.sh: accept LAN reachability as a mesh-equivalent
LXCs without a mesh CLI sit on 192.168.8.0/24 which is in the
issuance service's MESH_SUBNETS — they should be able to bootstrap
without netbird/tailscale installed. New third path probes the
issuance /health endpoint directly; if reachable, treat that as
satisfying the mesh precondition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:38:41 +02:00