Commit Graph

87 Commits

Author SHA1 Message Date
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
root
e8c2ccf7bb wiki: document homelab-context distribution system
Adds infrastructure/homelab-context.md as the architecture reference for
the cross-client context + MCP + secrets-issuance system. Updates:

- 105-apps.md: two new ## Stacks sections (homelab-mcp, secrets-issuance)
  with their deploy pipelines + a row each in the public-hostname table;
  changelog entry.
- auto-deploy.md: both new pipelines added to the table (one repo, two
  webhooks, same push); per-pipeline notes covering the clone-per-service
  pattern and the deploy.sh self-restart caveat; changelog entry.
- README.md: link to the new infrastructure page.

Operational walkthrough already lives at operations/agent-enrollment.md;
this commit is the architecture side of the same story.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:34:45 +02:00
root
0b6be9f42d homelab client add --finalize-pubkey: grant shared secrets atomically
Setting the age_pubkey is half the enrollment; the new client also needs
to be a recipient on shared secrets (hello.yaml, gitea-pat.yaml) to
actually use them. Now --finalize-pubkey:

  1. writes hosts.<name>.age_pubkey
  2. appends the pubkey to each shared-secret rule in .sops.yaml
     (preserving comments via line-by-line edit, not yaml round-trip)
  3. runs sops updatekeys -y on each shared file
  4. commits inventory + hosts/ + .sops.yaml + secrets/ as one commit

Also: cmd_client_add now re-execs via sudo when invoked as a regular
user (matches the pattern in cmd_secret + cmd_refresh_creds).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:30:22 +02:00
root
65ece6f447 secrets: distribute write-scoped Gitea PAT + homelab refresh-creds
Adds secrets/gitea-pat.yaml (SOPS-encrypted, dtoro PAT with read+write
scopes) so any enrolled client can push to dtoro/Homelab-Docs — not just
where I have SSH. Recipient set = hello.yaml's (hubris, apps, republic);
expand alongside hello.yaml when enrolling new clients.

bin/homelab gains 'refresh-creds': decrypts gitea-pat.yaml, rewrites
/etc/homelab-context/git-credentials with the write token, repoints
git's --system credential helper. Re-execs via sudo for non-root callers
(same pattern as 'homelab secret').

After this lands, 'homelab client add/remove' and wiki edits can run
from any client. The initial bootstrap still needs an operator-supplied
read-only PAT (chicken-and-egg); 'refresh-creds' upgrades the client
to write afterwards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:25:36 +02:00
root
90a65bd5a1 noop: verify webhook auto-deploy 2026-05-20 18:16:57 +02:00
root
ce25e73625 bootstrap.sh: store gitea cred helper in /etc/gitconfig, not /root/.gitconfig
The systemd sync timer runs git without HOME set, so git config --global
(which writes /root/.gitconfig) is invisible to the timer's process — the
timer fails with 'could not read Username' silently. Switching to
--system writes to /etc/gitconfig which is HOME-agnostic.

Migration for already-bootstrapped hosts captured in agent-enrollment.md
troubleshooting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:01:47 +02:00
root
ae7eb8c649 bootstrap.sh: symlink homelab CLI instead of copy
The 5-min sync pulls /opt/homelab-context but does not re-install the
CLI. A copy at /usr/local/bin/homelab therefore goes stale after every
CLI fix until someone re-runs bootstrap. Symlinking points
/usr/local/bin/homelab directly at the synced source, so updates land
on the next pull. Doc updated with the one-line migration for hosts
bootstrapped before this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:54:24 +02:00
root
df6aca888c homelab: re-exec 'secret' via sudo for non-root users
/etc/age/key.txt is 0600 root and /etc/age is 0700 root, so the CLI's
Path.exists() check was returning False under regular users — making the
subcommand look broken when bootstrap had actually written the key fine.
Re-exec via sudo preserves the existing UX (one password prompt, then
plaintext) without loosening the key's perms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:48:32 +02:00
root
58bd4df3b1 client-enrol: republic-laptop pubkey + hello.yaml recipient
Phase 2 first workstation enrolled. age1vf8... is republic-laptop's
issued pubkey; added as a recipient on hello.yaml so the post-bootstrap
decrypt test works there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:34:30 +02:00
root
44d4d00aff operations: add agent-enrollment.md — bootstrap doc for new clients
Captures the full enrollment flow validated during Phase 2 rollout: per-OS
dep install (dnf/apt/brew), Gitea PAT prerequisite, DNS gotchas, the
bootstrap command, post-bootstrap verification, the homelab client add
ceremony for new inventory entries, secret grant/revoke, and a
troubleshooting table mapping every failure mode we hit during validation
to the commit that fixed it.

Linked from README under Operations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:27:56 +02:00
root
c5fa756c7d Use mcp/secrets.hubris.network for the canonical endpoints
Caddy + split-horizon DNS now resolve these to LXC 105 (via 121).
Workstations off-LAN reach them via Netbird (192.168.8.0/24 is a
network resource routed through hubris).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:17:19 +02:00
root
a7d2a7a08c bootstrap.sh: emit dnf install hint on Fedora/RHEL clients
Was apt-only on Linux; republic-laptop is Nobara so installs use dnf and
yaml is python3-pyyaml (not python3-yaml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:12:13 +02:00
root
6848640fa4 mcp/server: disable FastMCP DNS-rebinding protection
We're nftables-gated to mesh+LAN; the browser-attack threat doesn't
apply, and the default whitelist (127.0.0.1/localhost/[::1] only) blocks
every LAN/mesh client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:43:35 +02:00
root
1dc40ca715 mcp/server: bind 0.0.0.0:9810 (FastMCP default is 127.0.0.1:8000)
The MCP service unit expects port 9810 per the inventory; FastMCP only
binds correctly when we set mcp.settings.host/port before run().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:42:30 +02:00
root
bc578df39b secrets: add encrypted hello.yaml (bootstrap verification)
Encrypted to hubris + apps; expand recipients as new clients enrol via
'sops updatekeys -y secrets/hello.yaml'. Tests the full sops + age path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:39:33 +02:00
root
1ae328f04d inventory: record age pubkeys for hubris + apps (Phase 3a)
First two clients have age keys issued by secrets-issuance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:38:44 +02:00
root
582823a89c secrets-issuance: trust LAN subnet 192.168.8.0/24
Hubris is Netbird-only and LXC 105 is Tailscale-only; they share LAN
but not a mesh, so on-host \`curl http://192.168.8.205:9820/issue\`
arrives with source IP 192.168.8.77. In a homelab LAN with no
untrusted devices the trust boundary is reasonable; if that changes
later, narrow this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:36:09 +02:00
root
3c9d2f975d bootstrap.sh: require python3 + PyYAML (used by homelab CLI)
The homelab CLI imports yaml; missing on a fresh LXC. Preflight now
checks and emits the right install hint per OS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:31:28 +02:00
root
de6f8bec42 bootstrap.sh: preserve URL scheme in credentials file
git credential helper does exact prefix match including scheme. Hardcoding
https:// breaks for in-LAN clones using http://192.168.8.121:3000 (which
LXC 105 needs because its DNS doesn't have the split-horizon override).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:30:17 +02:00
root
385deb6dec bootstrap.sh: add --gitea-token / HOMELAB_GITEA_TOKEN
Each non-hubris client needs HTTPS auth against gitea for the initial
context clone (chicken-and-egg: a PAT stored in SOPS can't be fetched
until after the clone exists). Adds a --gitea-token flag that writes
credentials to /etc/homelab-context/git-credentials and points git's
credential.helper at it, so the clone and all future pulls succeed
without prompting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:24:31 +02:00
root
3c25f936d3 Phase 1: cross-client homelab context + MCP scaffolding
Add the foundation for distributing homelab context to every client
(LXCs, VMs, workstations including republic-laptop, mac-mini, ludo-mini)
with a single source of truth, structured query layer (MCP), and per-client
age-key issuance for secrets:

- inventory.yaml — canonical topology (hosts, services, mesh addresses)
- hosts/*.yaml — per-host identity files generated from inventory by
  mcp/build_host_files.py; do not edit by hand
- AGENTS.md — orientation doc symlinked to /root/AGENTS.md on every client
- bootstrap.sh — one-shot enroll (Linux + macOS), clones repo, fetches age
  key from issuance, installs sync timer/launchd job, drops the homelab CLI
- bin/homelab — single-binary Python CLI: whoami, list, ssh, pct, logs,
  restart, open, status, secret, sync, mcp, client add/remove, nuke
- mcp/server.py — FastMCP server: context tools + read-only management
  tools (no mutations exposed); shell-outs use mcp-reader restricted ssh key
- mcp/deploy/ — claudio-monitor-style gitea webhook deploy scaffold for the
  MCP service on LXC 105 (ports 9810 mcp, 9811 webhook)
- secrets-issuance/ — per-client age key auto-provisioning over the mesh;
  source-IP gated against inventory, with denylist for revoked clients
  (ports 9820 issue, 9821 webhook)
- secrets/, .sops.yaml — SOPS recipient scaffolding; the operator fills in
  age public keys after Phase 3a generates them
- scripts/sync/ — systemd timer (Linux) + launchd plist (macOS) pulling
  /opt/homelab-context every 5 min

Mesh: both Netbird (preferred, 100.122.0.0/16) and Tailscale accepted
during the in-flight migration; no client is gated on completing the move.

Plan reference: /root/.claude/plans/lets-make-a-plan-fluttering-trinket.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:47:48 +02:00
Claudio
8f598a0e7a 127-mule-photos-new: deploy.sh fix for OIDC state-cookie rotation
PhotoPrism rotates its session HMAC key on every container start, so any
auto-deploy that recreated pp-app invalidated in-flight OIDC logins. The
deploy script was force-recreating + image-pulling on each push; pinned
both so pp-app survives a routine code deploy.
2026-05-18 21:36:29 +02:00
root
bb063d6f94 127-mule-photos-new: pre-transcode pass + perf characterisation
Measured cold-cache behaviour: thumbnails ~2ms, HEVC video playback
12-21s TTFB because libx264 transcodes inline and serialised one
ffmpeg at a time. Sibling thumbs unaffected (~2ms during transcode).
With 11/45 .mov files already pre-baked, ~75% of clicks were cold.
Ran `photoprism convert` to bring sidecar coverage to 45/45; previously
cold videos now serve in ~2ms TTFB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:12:14 +02:00
root
3bf8175d97 100-zimaos: fix UI delete by remounting NFS at /media/library
NFS was mounted at /DATA/library, so the icewhale-files trash regex
(^/media/([^/]+)) derived drive=ZimaOS-HD and tried to rename into
/media/ZimaOS-HD/.trash on local ext4 -- cross-device EXDEV from NFS.
Remounting at /media/library makes 'library' its own /media/<name>
segment, so trash resolves to /media/library/.trash on the NFS itself.
2026-05-18 10:11:19 +02:00
Claudio
a154070e03 LXC 127: capture OIDC bridge, indexer caps, folder fixes, gitea webhook gotcha 2026-05-18 00:12:12 +02:00
Claudio
eaa014d37a Add LXC 127 mule-photos-new (PhotoPrism M0 test on @new branch) 2026-05-17 21:52:31 +02:00
Claudio
5dda267013 118-elementsynapse: install suppress_redactions push-rule for @admin
Stops redaction events (Signal disappearing messages, deletes,
etc.) from bumping the mobile notification badge in the
future.
2026-05-15 21:59:37 +02:00
Claudio
565453773c 118-elementsynapse: phantom-notification cleanup + DB fact-fix
Synapse runs on SQLite (not Postgres — Postgres only hosts the
mautrix bridge dbs). Documented the post-disk-full
event_push_actions cleanup that took @admin'\''s phantom count
from 125 to 4.
2026-05-15 21:56:35 +02:00
Claudio
c9eb7fdab3 118-elementsynapse: document disk-full fix + bridges
Rootfs grew to 16 GiB on 2026-05-15. Added bridges section
(mautrix slack/signal/meta/linkedin/whatsapp), synapse-admin
port, Postgres backend note, and operational tips for future
disk growth.
2026-05-15 21:48:58 +02:00
Claudio
685d6a9d48 Decommission LXC 109 (syncthing)
Container had been stopped since 2026-04-21 and was never re-enabled.
pct destroy 109 --purge cleaned up vm-109-disk-0 on local-lvm and the
config file. /mnt/library/syncthing subtree was already empty at the
time of destruction and is retained as an empty dir (no real data to
migrate or back up).

- README.md, containers/index.md: removed row, moved to "recently
  destroyed" table
- hosts/hubris.md: dropped from /mnt/library subtree list, updated
  containers/index summary line, added changelog entry
- infrastructure/media-permissions.md: dropped from membership table
  and onboarding example, generalised pct-exec gotcha hostname,
  added changelog
- vms/100-zimaos.md: dropped from "existing fleet" enumeration
- containers/102-nfs-export.md: dropped from bind-mount sibling list
  (7 LXCs now, not 8)
- containers/109-syncthing.md: deleted

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:12:32 +02:00
Claudio
aca2ff836e hubris: network performance baseline 2026-05-14
WAN 113/20 Mbit, vmbr0 intra-host ~34.7 Gbit/s, eno1 1G negotiated.
Flag: eno1 rx_errors at ~1.7% over 14d uptime — recheck trend.
2026-05-14 22:49:26 +02:00