23 Commits

Author SHA1 Message Date
a45e4f6f29 docs(teddycloud): correct IP, port, disable boxCertAuth, ESP32 onboarding
Real IP is 192.168.8.243 (not .214). Web UI is on port 8443 HTTPS,
not 8080. Caddy backend uses tls_insecure_skip_verify. boxCertAuth
disabled so ESP32 Toniebox connects without client cert extraction.
Added SD card onboarding steps for Leon's ESP32 box.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 22:13:11 +02:00
77b3a6f677 docs(teddycloud): provision LXC 131 with TeddyCloud
New privileged LXC (131) running TeddyCloud via Docker Compose.
Content bind-mounted from /mnt/library/cloud/leon. Management UI
at teddy.hubris.network via Caddy; Toniebox device traffic routed
direct to LXC:443 via Technitium prod.de.bb-online.com override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 21:38:56 +02:00
41df77fb20 docs(runbook): update budget-from-csv with API corrections and Cookie Share calc
- Fix subscription path to /budget/subscriptions (not /subscriptions/)
- Document RRULE rejection; use cycle_interval instead
- Add GET /budget/ vs ?month= behavior note
- Add full category/payment-method ID tables
- Add Cookie Share recommendation: 2,650 €/month based on 6-mo analysis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 11:45:34 +02:00
b67389b137 docs(operations): add budget-from-csv runbook for Yuvomi
Step-by-step procedure to import N26 CSV transactions into Yuvomi's
Budget and Subscriptions modules using the yuvomi-mcp tools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 10:55:25 +02:00
45b7813eb7 chore(grimmory): finalize age_pubkey for LXC 130
Fills in the age public key issued by secrets-issuance after
homelab client enrollment completed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 09:17:31 +02:00
8f6f43eeb1 docs(grimmory): fix IP assignment — PVE static config, not DHCP reservation
All LXCs use ip=<addr>/24 in pve net0 config directly. No Fritz!Box
lease needed. Updated container doc and migration runbook accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:52:34 +02:00
c2c257aa12 docs(grimmory): correct Authentik OIDC setup notes
OIDC config migrated from Booklore DB dump — Confidential client (not
PKCE), credentials intact. offline_access scope added to Authentik
provider. Backchannel logout URL set to permanent IP 192.168.8.213.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 08:48:52 +02:00
41fe34a3c3 migrate: Booklore → Grimmory on new LXC 130
Grimmory (grimmory-tools/grimmory) is the community fork/successor of
Booklore. Moving it off the shared apps LXC (105) onto a dedicated
container (130, 192.168.8.213) to avoid the Portainer-managed stack
footgun that wiped mariadb in April.

- containers/130-grimmory.md: new container doc with compose layout,
  Authentik OIDC notes (PKCE/Public client), media-GID setup
- plans/2026-06-29-grimmory-migration.md: step-by-step runbook for
  DB dump/restore, LXC provisioning, Caddy cutover, Authentik update
- inventory.yaml: add grimmory LXC 130
- containers/105-apps.md: remove Booklore section + hostname entry
- containers/index.md: update 105 row; add 130 row
- infrastructure/media-permissions.md: add LXC 130; update Booklore→Grimmory refs
- README.md: update 105 row; add 130 row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 01:11:00 +02:00
b6c9184a7b decommission: destroy plato (LXC 126)
LXC 126 stopped and destroyed on hubris. Remove all live references:
inventory, container doc, host file, README, containers index, auto-deploy
pipeline, DNS entry, SSH access table, nfs-export mount list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 22:32:59 +02:00
root
636c90f99c secrets: populate hermes-house-users with household Signal numbers
+4917622791635 → Yuvomi user 1 (Taco)
+491726924525  → Yuvomi user 3 (Steffi)
2026-06-28 16:50:51 +02:00
root
cef1b836c2 sops: add yuvomi-api-token + hermes-house-users secrets (house + workstations) 2026-06-28 16:02:13 +02:00
root
335498d069 client-add: house (finalize age_pubkey + grant shared + hermes secrets) 2026-06-28 15:59:47 +02:00
root
867998e9c0 hosts: regenerate — add house, mac-mini, ludo-mini; remove stale orphans 2026-06-28 15:58:36 +02:00
cb0c1a5c0d house: add Google Calendar OAuth config to docs 2026-06-27 14:40:29 +02:00
ddf541ebd3 house: switch Paperless to native DMS connector (remove WebDAV bridge) 2026-06-26 19:57:50 +02:00
4472e73ae1 Yuvomi deployment: LXC 129 house.hubris.network, Docker, Caddy, DNS, VPS traefik, Authentik OIDC, Paperless WebDAV bridge 2026-06-26 19:06:49 +02:00
59261c7ef1 hosts: generate trmnl.yaml (prereq for SOPS enrollment of LXC 128) 2026-06-24 23:41:29 +02:00
41f22c2d85 trmnl (128): auto-deploy pipeline + LAN DNS record wired
- auto-deploy.md: dtoro/terminalito pipeline (webhook 12, :9797), ALLOWED_HOST_LIST
  += 192.168.8.211, changelog
- 128-trmnl.md: auto-deploy now wired, Technitium A record done, changelog
- 107-dns.md: A record trmnl -> 192.168.8.175 (changelog)
- 104-gitea.md: webhook id 12 + allowed host (changelog)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:06:56 +02:00
b7cfc350b3 trmnl (128): public path verified live; root-cause the mesh outage
Root cause of the provision-time 504s: netbird home-lab-network (192.168.8.0/24)
had no active routing peer — mac-mini routing peer's netbird daemon was down, so
all home-backed public services (artifacto/blog/trmnl) 504'd at the VPS edge.
netbird up on mac-mini restored it; verified trmnl public 200/401, artifacto 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:38:57 +02:00
fffa5560eb trmnl (128): provisioned — container page, inventory, ingress
LXC 128 trmnl hosts the TRMNL plugins middleware (dtoro/terminalito), polled by
TRMNL cloud. trmnl-plugins.service on :9851; Caddy block + LE cert; VPS traefik
router trmnl-public + cert mirror. Public path pending VPS<->home netbird route
recovery (was "No networks available" at provision time, artifacto/blog 504 too).
LAN Technitium record + SOPS enrollment + Google/MVG creds pending. Plan -> In Progress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:24:31 +02:00
d6bfd7b8c9 plans: TRMNL plugins LXC (128) + middleware deploy pipeline
Pre-flight runbook for a dedicated trmnl LXC hosting self-hosted middleware for
TRMNL e-ink plugins (TRMNL cloud polls /<plugin>/dashboard). First consumer:
munich-home. Source repo dtoro/terminalito; this documents the fabric wiring
(provision via homelab client add, Shape-B auto-deploy, SOPS secret, caddy + VPS
traefik exposure). Status: Planned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:59:03 +02:00
b71402aaed add auth-outpost (106) and dns (107) to inventory + regenerate host files 2026-06-22 09:31:04 +02:00
2a7876c711 add caveman compression mode to agent bootstrap
AGENTS.md now instructs every agent to communicate
tersely. CAVEMAN.md holds the full rules. Agents
apply from first message in each session.
2026-06-21 23:36:06 +02:00
59 changed files with 1901 additions and 348 deletions

View File

@@ -24,7 +24,8 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/gitea-pat\.yaml$ - path_regex: ^secrets/gitea-pat\.yaml$
# Write-scoped Gitea PAT (dtoro user). Same recipient list as hello.yaml # Write-scoped Gitea PAT (dtoro user). Same recipient list as hello.yaml
@@ -34,7 +35,8 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/gitea-tokens\.yaml$ - path_regex: ^secrets/gitea-tokens\.yaml$
# Workstations only. # Workstations only.
@@ -55,7 +57,8 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/netbird-authentik-oidc\.yaml$ - path_regex: ^secrets/netbird-authentik-oidc\.yaml$
# Authentik OIDC client secret for the netbird-dashboard provider. # Authentik OIDC client secret for the netbird-dashboard provider.
@@ -65,7 +68,8 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/netbird-pat\.yaml$ - path_regex: ^secrets/netbird-pat\.yaml$
# NetBird API Personal Access Token. Consumed by the dns-sync job on the # NetBird API Personal Access Token. Consumed by the dns-sync job on the
@@ -86,5 +90,23 @@ creation_rules:
age: >- age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/yuvomi-api-token\.yaml$
# Named Bearer token for the Yuvomi REST API, consumed by yuvomi-mcp on
# LXC 129 (house).
age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
- path_regex: ^secrets/hermes-house-users\.yaml$
# Signal number → Yuvomi user_id mapping (PII). Consumed by hermesd on LXC 129.
age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
# webhook noop 2026-05-20T18:16:57+02:00 # webhook noop 2026-05-20T18:16:57+02:00

File diff suppressed because one or more lines are too long

View File

@@ -19,8 +19,8 @@ the operator to run `homelab client add <hostname>` from an existing client.
- `/opt/homelab-context/inventory.yaml` — every host, LXC, VM, and workstation - `/opt/homelab-context/inventory.yaml` — every host, LXC, VM, and workstation
with their mesh addresses, roles, and service mappings. Treat this file as with their mesh addresses, roles, and service mappings. Treat this file as
authoritative; anything you read in narrative pages should agree with it. authoritative; anything you read in narrative pages should agree with it.
- `/opt/homelab-context/infrastructure/mesh.md` — Netbird mesh state. - `/opt/homelab-context/infrastructure/mesh.md` Tailscale → Netbird state.
Tailscale was decommissioned 2026-06-21; the fleet is Netbird-only. Both meshes are accepted today; Netbird is preferred for new traffic.
- `/opt/homelab-context/infrastructure/dns.md` — split-horizon DNS via - `/opt/homelab-context/infrastructure/dns.md` — split-horizon DNS via
dnsmasq on LXC 124. `*.hubris.network` resolves to 192.168.x.x on the LAN dnsmasq on LXC 124. `*.hubris.network` resolves to 192.168.x.x on the LAN
and to mesh addresses off-LAN. and to mesh addresses off-LAN.
@@ -83,7 +83,12 @@ Grep is fine for browsing or when MCP is unreachable.
- **Wiki updates**: same-session rule applies to any meaningful state change - **Wiki updates**: same-session rule applies to any meaningful state change
this client makes. this client makes.
## 6. Auto-setup mechanism ## 6. Communication mode
Read and apply `/opt/homelab-context/CAVEMAN.md` (if present). It defines the lab's
terse-communication standard — drop filler, keep substance, use fragments.
## 7. Auto-setup mechanism
The homelab-context repo ships tooling that gets automatically installed The homelab-context repo ships tooling that gets automatically installed
on every client after `git pull`. This is handled by `tools/post-pull.sh` on every client after `git pull`. This is handled by `tools/post-pull.sh`
@@ -104,7 +109,7 @@ commit and push. All enrolled clients pick it up within 5 minutes.
To trigger sync manually: `sudo homelab sync` or wait for the 5-min timer. To trigger sync manually: `sudo homelab sync` or wait for the 5-min timer.
## 7. When in doubt ## 8. When in doubt
Run `homelab mcp search_docs <query>` or `homelab mcp get_host <name>`. Run `homelab mcp search_docs <query>` or `homelab mcp get_host <name>`.
The clone is the fallback; MCP is the index. The clone is the fallback; MCP is the index.

33
CAVEMAN.md Normal file
View File

@@ -0,0 +1,33 @@
# CAVEMAN.md — communication mode for homelab agents
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Levels
- **lite** — no filler/hedging. Keep articles + full sentences. Professional but tight.
- **full** (default) — drop articles, fragments OK, short synonyms. Classic caveman.
- **ultra** — abbreviate prose words (DB/auth/config/req/res), strip conjunctions, arrows (X → Y). Code symbols/API names/errors: never abbreviate.
Switch: `/caveman lite|full|ultra`. Stop: "normal mode".
## Auto-Clarity
Drop caveman for: security warnings, irreversible actions, multi-step sequences where fragments risk misread, user confused/repeating. Resume after clear part done.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
---
Source: https://github.com/JuliusBrussee/caveman
Copy to `~/.hermes/skills/` for Hermes Agent, or `~/.claude/projects/<name>/SKILL.md` for Claude Code.

View File

@@ -22,7 +22,7 @@ See the full table in [`containers/index.md`](containers/index.md). Quick links:
| 102 | [nfs-export](containers/102-nfs-export.md) | 192.168.8.200 | NFSv4 re-export of /mnt/library for ZimaOS | | 102 | [nfs-export](containers/102-nfs-export.md) | 192.168.8.200 | NFSv4 re-export of /mnt/library for ZimaOS |
| 103 | [paperless](containers/103-paperless.md) | 192.168.8.130 | Document mgmt | | 103 | [paperless](containers/103-paperless.md) | 192.168.8.130 | Document mgmt |
| 104 | [gitea](containers/104-gitea.md) | 192.168.8.121 | Git server | | 104 | [gitea](containers/104-gitea.md) | 192.168.8.121 | Git server |
| 105 | [apps](containers/105-apps.md) | 192.168.8.205 | Docker host (Artifacto / Booklore / PlantUML / Portainer / WriteFreely) | | 105 | [apps](containers/105-apps.md) | 192.168.8.205 | Docker host (Artifacto / PlantUML / Portainer / WriteFreely) |
| 114 | [nextcloud](containers/114-nextcloud.md) | 192.168.8.224 | Personal cloud | | 114 | [nextcloud](containers/114-nextcloud.md) | 192.168.8.224 | Personal cloud |
| 118 | [elementsynapse](containers/118-elementsynapse.md) | 192.168.8.239 | Matrix Synapse | | 118 | [elementsynapse](containers/118-elementsynapse.md) | 192.168.8.239 | Matrix Synapse |
| 119 | [sophia](containers/119-sophia.md) | 192.168.8.157 | Sophia | | 119 | [sophia](containers/119-sophia.md) | 192.168.8.157 | Sophia |
@@ -30,12 +30,12 @@ See the full table in [`containers/index.md`](containers/index.md). Quick links:
| 121 | [caddy](containers/121-caddy.md) | 192.168.8.175 | Reverse proxy | | 121 | [caddy](containers/121-caddy.md) | 192.168.8.175 | Reverse proxy |
| 122 | [arriman](containers/122-arriman.md) | 192.168.8.132 | Docker host (\*arr stack) | | 122 | [arriman](containers/122-arriman.md) | 192.168.8.132 | Docker host (\*arr stack) |
| 124 | [authentik](containers/124-authentik.md) | 192.168.8.180 | SSO + split-horizon DNS | | 124 | [authentik](containers/124-authentik.md) | 192.168.8.180 | SSO + split-horizon DNS |
| 126 | [plato](containers/126-plato.md) | 192.168.8.190 | Plato (notes/discovery workspace) | | 130 | [grimmory](containers/130-grimmory.md) | 192.168.8.213 | Digital library (Grimmory — fork of Booklore) |
### Cross-cutting infrastructure ### Cross-cutting infrastructure
- [DNS — split-horizon](infrastructure/dns.md) - [DNS — split-horizon](infrastructure/dns.md)
- [Ingress — Caddy + VPS traefik](infrastructure/ingress.md) - [Ingress — Caddy + VPS traefik](infrastructure/ingress.md)
- [Mesh — Netbird overlay](infrastructure/mesh.md) - [Mesh — Tailscale → Netbird migration](infrastructure/mesh.md)
- [Monitoring — Hermes health watchdog](infrastructure/monitoring.md) - [Monitoring — Hermes health watchdog](infrastructure/monitoring.md)
- [Media permissions — `media` GID 10000](infrastructure/media-permissions.md) - [Media permissions — `media` GID 10000](infrastructure/media-permissions.md)
- [SSH access](infrastructure/ssh-access.md) - [SSH access](infrastructure/ssh-access.md)

View File

@@ -8,7 +8,7 @@ Dedicated, single-purpose LXC that re-exports `/mnt/library` over NFSv4 to clien
- **LAN DNS:** `nfs-export.hubris.network``192.168.8.200` (direct, no Caddy) - **LAN DNS:** `nfs-export.hubris.network``192.168.8.200` (direct, no Caddy)
- **Privilege:** privileged (`unprivileged: 0`) + `lxc.apparmor.profile: unconfined` — required for `nfs-kernel-server` - **Privilege:** privileged (`unprivileged: 0`) + `lxc.apparmor.profile: unconfined` — required for `nfs-kernel-server`
- **Resources:** 1 core / 512 MiB RAM / 2 GiB rootfs / 256 MiB swap - **Resources:** 1 core / 512 MiB RAM / 2 GiB rootfs / 256 MiB swap
- **Mounts:** host `/mnt/library` ↔ container `/mnt/library` (same path on both sides — matches the bind-mount convention used by jellyfin, paperless, arriman, nextcloud, mule-images, plato, apps) - **Mounts:** host `/mnt/library` ↔ container `/mnt/library` (same path on both sides — matches the bind-mount convention used by jellyfin, paperless, arriman, nextcloud, mule-images, apps)
## What it does ## What it does

View File

@@ -45,6 +45,9 @@ LXC has `/etc/hosts` override mapping `auth.hubris.network → 192.168.8.175` (r
## Changelog ## Changelog
### 2026-06-24 — terminalito deploy webhook (id 12)
Push webhook on `dtoro/terminalito``http://192.168.8.211:9797/deploy` ([trmnl (128)](128-trmnl.md)); `app.ini` `ALLOWED_HOST_LIST` extended with `192.168.8.211`. See [auto-deploy](../infrastructure/auto-deploy.md).
### 2026-04-28 — wiki entry created ### 2026-04-28 — wiki entry created
Initial documentation. Initial documentation.

View File

@@ -1,6 +1,6 @@
# 105 — `apps` # 105 — `apps`
Docker host for everything that doesn't justify its own LXC. Currently runs Artifacto, Booklore, PlantUML server, Portainer (and historically WriteFreely / blog), plus the [homelab-context distribution services](../infrastructure/homelab-context.md) (MCP + secrets-issuance) since 2026-05-20. Docker host for everything that doesn't justify its own LXC. Currently runs Artifacto, PlantUML server, Portainer (and historically WriteFreely / blog), plus the [homelab-context distribution services](../infrastructure/homelab-context.md) (MCP + secrets-issuance) since 2026-05-20. Booklore migrated to [grimmory (130)](130-grimmory.md) on 2026-06-29.
## At a glance ## At a glance
- **Hostname:** `apps` - **Hostname:** `apps`
@@ -15,7 +15,6 @@ Docker host for everything that doesn't justify its own LXC. Currently runs Arti
| Hostname | Container | Backend port | Notes | | Hostname | Container | Backend port | Notes |
| --------------------------------- | ---------------- | ------------ | ----- | | --------------------------------- | ---------------- | ------------ | ----- |
| `docker.hubris.network` | Portainer | `:9443` | Native OAuth2 via Authentik. Trusted-origins requires hostname only (no scheme/port). | | `docker.hubris.network` | Portainer | `:9443` | Native OAuth2 via Authentik. Trusted-origins requires hostname only (no scheme/port). |
| `books.hubris.network` | Booklore | `:6060` | Native OIDC. Redirect URI `/oauth2-callback`. |
| `artifacto.hubris.network` | Artifacto | `:3100` | Public `/p/*`, `/static/*`, `/healthz` exposed via [VPS traefik](../infrastructure/ingress.md). | | `artifacto.hubris.network` | Artifacto | `:3100` | Public `/p/*`, `/static/*`, `/healthz` exposed via [VPS traefik](../infrastructure/ingress.md). |
| `blog.hubris.network` | WriteFreely | `:8080` | Native OIDC via `[oauth.generic]`. | | `blog.hubris.network` | WriteFreely | `:8080` | Native OIDC via `[oauth.generic]`. |
| `git.hubris.network/_plantuml/*` | PlantUML server | `:8079` | Same-origin route from [gitea (104)](104-gitea.md). | | `git.hubris.network/_plantuml/*` | PlantUML server | `:8079` | Same-origin route from [gitea (104)](104-gitea.md). |
@@ -46,11 +45,6 @@ Receiver at `/opt/artifacto-deploy/` (outside the app repo): `deploy.sh` + `webh
### Portainer ### Portainer
Native OAuth2 (Settings → Authentication → OAuth → Custom). Manual endpoints (no OIDC discovery). Uses `portainer-uid` custom-claim scope from Authentik. Container is **not** compose-managed — safe to `docker run` recreate; data lives in named volume `portainer_data`. CLI flag: `--trusted-origins docker.hubris.network` (hostname only — `IsTrustedOrigin` rejects strings containing `://`). Native OAuth2 (Settings → Authentication → OAuth → Custom). Manual endpoints (no OIDC discovery). Uses `portainer-uid` custom-claim scope from Authentik. Container is **not** compose-managed — safe to `docker run` recreate; data lives in named volume `portainer_data`. CLI flag: `--trusted-origins docker.hubris.network` (hostname only — `IsTrustedOrigin` rejects strings containing `://`).
### Booklore
Native OIDC via Authentik (Settings → OIDC). Redirect URI `/oauth2-callback` (NOT `/api/oidc`). Container needs `extra_hosts: auth.hubris.network:192.168.8.175`. **Edit via Portainer UI** if it's a Portainer-managed stack.
> ⚠️ **Never `docker compose up` Portainer-managed stacks from the host shell.** Portainer's compose state lives at `/var/lib/docker/volumes/portainer_data/_data/compose/<N>/`. Running `docker compose up -d <svc>` from the host triggers recreates of OTHER services in the stack and silently destroys bind-mounted data. **This wiped Booklore's mariadb data on 2026-04-22.** Use the Portainer UI editor for compose changes. See [mesh migration](../infrastructure/mesh.md#critical-never-docker-compose-up-portainer-managed-stacks) for the full warning.
### homelab-mcp (`/opt/homelab-mcp/`) ### homelab-mcp (`/opt/homelab-mcp/`)
FastMCP server (Python venv at `/opt/homelab-mcp/.venv`). Reads from FastMCP server (Python venv at `/opt/homelab-mcp/.venv`). Reads from
`/opt/homelab-context/` (this LXC is itself an enrolled `/opt/homelab-context/` (this LXC is itself an enrolled
@@ -87,10 +81,9 @@ same key. Mesh+LAN source-IP gated via the `MESH_SUBNETS` env in
- `/revoke` is admin-token-gated by `/etc/secrets-issuance/admin-token`; - `/revoke` is admin-token-gated by `/etc/secrets-issuance/admin-token`;
shreds the local key file and adds the hostname to the denylist. shreds the local key file and adds the hostname to the denylist.
Called by `homelab client remove`. Called by `homelab client remove`.
- Trust subnets today: `100.122.0.0/16` (Netbird), `192.168.8.0/24` (LAN). - Trust subnets today: `100.122.0.0/16` (Netbird), `100.64.0.0/10`
(The legacy Tailscale CGNAT range `100.64.0.0/10` was removed from (Tailscale), `192.168.8.0/24` (LAN). Tighten if the LAN gets
`secrets-issuance`'s `MESH_SUBNETS` on 2026-06-21 when Tailscale was untrusted devices.
decommissioned; service restarted.) Tighten if the LAN gets untrusted devices.
#### Auto-deploy pipeline (secrets-issuance) #### Auto-deploy pipeline (secrets-issuance)
Receiver at `/opt/secrets-issuance/secrets-issuance/deploy/webhook/`, Receiver at `/opt/secrets-issuance/secrets-issuance/deploy/webhook/`,
@@ -114,6 +107,9 @@ Native OIDC via `[oauth.generic]` in `config/config.ini`. `host = https://auth.h
## Changelog ## Changelog
### 2026-06-29 — Booklore migrated to Grimmory on LXC 130
Booklore stack removed from Portainer. MariaDB dump taken first, then restored into [grimmory (130)](130-grimmory.md)'s fresh MariaDB. `books.hubris.network` Caddy backend updated to `192.168.8.213:6060`. Authentik OIDC provider updated to Public client type (PKCE) for Grimmory compatibility.
### 2026-05-20 — homelab-mcp + secrets-issuance live ### 2026-05-20 — homelab-mcp + secrets-issuance live
Two new services from the [homelab-context distribution plan](../infrastructure/homelab-context.md): Two new services from the [homelab-context distribution plan](../infrastructure/homelab-context.md):
`homelab-mcp.service` on `:9810` (MCP read+management surface) and `homelab-mcp.service` on `:9810` (MCP read+management surface) and

View File

@@ -24,14 +24,12 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur
- API: `http://192.168.8.2:5380/api/...` (token via `/api/user/login`). Zone was built via the API. - API: `http://192.168.8.2:5380/api/...` (token via `/api/user/login`). Zone was built via the API.
## Who points here ## Who points here
- **NetBird mesh peers:** resolve `hubris.network` by **forwarding to Technitium** via the `home-lab-dns` nameserver group (`→ 192.168.8.2`, domain `hubris.network`, applied to all peers). The **NetBird managed DNS zone was removed 2026-06-21 (Phase 4)** — Technitium is now the single DNS source for the mesh too. This works because: (a) roaming peers (`Core`) have the `192.168.8.0/24` route to reach `192.168.8.2` (added 2026-06-21), and (b) clients run NetBird **0.71.x** — on the old 0.68.3 client, forwarding reported `Available` but didn't serve queries, and the resolver cache (`100.122.255.254`) wouldn't clear on `down/up`; a `netbird service restart` (or app toggle) clears it. See [dns.md changelog 2026-06-21](../infrastructure/dns.md). - **NetBird mesh peers:** resolve via the **NetBird managed DNS zone**, kept in sync *from* this Technitium (see dns-sync below). The `home-lab-dns` nameserver group (`→ 192.168.8.2`) is a thin fallback forwarder.
- **Homelab DHCP clients:** Technitium's own DHCP scope hands out `192.168.8.2` as the DNS server for `192.168.8.x` leases (see DHCP section below). - **Homelab DHCP clients:** Technitium's own DHCP scope hands out `192.168.8.2` as the DNS server for `192.168.8.x` leases (see DHCP section below).
- **Plain LAN clients (`192.168.178.x`):** Fritz!Box DHCP still hands out Fritz!Box itself (`192.168.178.1`) as DNS, **but** the Fritz!Box now *forwards* upstream to Technitium — DNSv4 server set to `192.168.8.2` (Internet → Filter → DNS Server, 2026-06-17). So household clients get split-horizon `*.hubris.network` answers via Fritz!Box→Technitium, with **no NetBird dependency**. (This is the change that decoupled the on-prem tier from the mesh — see [dns.md](../infrastructure/dns.md) 2026-06-17.) - **Plain LAN clients (`192.168.178.x`):** Fritz!Box DHCP still hands out Fritz!Box itself (`192.168.178.1`) as DNS — no split-horizon for non-mesh clients. Changing this requires a secondary DNS fallback, which Fritz!OS 8.x doesn't expose in a single DHCP field.
## dns-sync (RETIRED 2026-06-21 — Phase 4 complete) ## dns-sync (Technitium = authoring source)
**The managed-zone sync is no longer scheduled.** `/opt/dns-sync/sync.py` reconciled this zone's named A-records → the NetBird managed DNS zone; the `*/10` cron (`/etc/cron.d/dns-sync`) was **removed 2026-06-21** when the managed zone was retired. Technitium is now the **single** DNS source — mesh peers forward to it (see "Who points here" above), LAN/household clients query it directly. `/opt/dns-sync/sync.py` (cron `*/10`, logs `/var/log/dns-sync.log`) reconciles this zone's named A-records → the NetBird managed DNS zone via the NetBird API (`/api/dns/zones/{id}/records`). Token at `/opt/dns-sync/netbird-token` (mode 600; source of truth in sops `secrets/netbird-pat.yaml`). **Edit DNS only here**; the sync propagates to the mesh. It deletes NetBird records absent from Technitium. Tracked: [scripts/dns-sync.py](../scripts/dns-sync.py). *Why this exists:* NetBird won't forward to Technitium for mesh peers (self-IP / nameserver-group quirks), so we sync into the managed zone instead — see [dns.md](../infrastructure/dns.md).
The script + token + a pre-deletion record backup remain at `/opt/dns-sync/` **as an emergency-restore tool only**: running `python3 /opt/dns-sync/sync.py` once re-creates the managed zone from Technitium (used during the Phase 4 rollback). Do not re-add the cron unless reverting Phase 4. Tracked: [scripts/dns-sync.py](../scripts/dns-sync.py).
## DHCP ## DHCP
@@ -50,6 +48,9 @@ Replaces the DHCP that was previously served by the Slate AX router. Static-IP L
## Changelog ## Changelog
### 2026-06-24 — A record `trmnl.hubris.network → 192.168.8.175`
Added for [trmnl (128)](128-trmnl.md) (LAN path via [Caddy (121)](121-caddy.md)); propagated to the NetBird managed zone by `dns-sync`.
### 2026-06-06 — dns-sync cron installed (had been missing since deployment) ### 2026-06-06 — dns-sync cron installed (had been missing since deployment)
Although the 2026-06-03 changelog claimed "cron */10", **no crontab was actually configured** on the LXC. The sync was running only via ad-hoc manual invocations during incident debugging. Fixed by adding `/etc/cron.d/dns-sync`. Although the 2026-06-03 changelog claimed "cron */10", **no crontab was actually configured** on the LXC. The sync was running only via ad-hoc manual invocations during incident debugging. Fixed by adding `/etc/cron.d/dns-sync`.

View File

@@ -1,108 +0,0 @@
# 126 — `plato`
Docker host for [Plato](https://git.hubris.network/dtoro/Plato) — a cross-linked notes workspace (SvelteKit SPA embedded into a Go HTTP server, SQLite-backed). LAN+mesh only, no public ingress.
## At a glance
- **Hostname:** `plato`
- **IP:** `192.168.8.190`
- **Privilege:** privileged
- **Resources:** 2 cores / 2 GiB RAM / 8 GiB rootfs / 1 GiB swap
- **Mounts:** host `/mnt/library/documents/plato` ↔ container `/opt/plato/data`
- **Public hostname:** [`plato.hubris.network`](../infrastructure/dns.md) → [caddy (121)](121-caddy.md) → `192.168.8.190:8080`
## Stack
Single-container deploy. The repo's `Dockerfile` is a three-stage build (Node → Go → distroless/static-debian12:nonroot, ~23 MiB final image). The container exposes `:8080` and writes its SQLite db to `/data`.
- **Checkout:** `/opt/plato/app` (clone of `http://192.168.8.121:3000/dtoro/Plato.git`, using the cached gitea PAT in `/root/.git-credentials` — same pattern as [caddy (121)](121-caddy.md)).
- **Data:** host `/mnt/library/documents/plato` (owned `65532:65532` to match the distroless nonroot UID) bind-mounted into the LXC at `/opt/plato/data`, then bound into the container at `/data` via a `docker-compose.override.yml`:
```yaml
services:
plato:
volumes: !override
- /opt/plato/data:/data
restart: unless-stopped
```
- **`.env`** at `/opt/plato/app/.env` (optional, untracked) — LLM provider keys (`OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`, etc.) and `PLANTUML_BASE_URL` override. Absent by default; LLM features stay greyed out, PlantUML defaults to the public service.
- **Run / update:** push to `dtoro/Plato` (auto-deploys, see below) or `cd /opt/plato/app && git pull && docker compose up -d --build` for a manual rebuild.
## Auto-deploy
Push to `dtoro/Plato` `main` triggers a rebuild — same Shape B pattern as [Artifacto / mule-image](../infrastructure/auto-deploy.md). Webhook receiver at `/opt/plato-deploy/`, systemd unit `plato-deploy-webhook.service`, port `9799`, gitea hook id 8.
- Receiver: `http://192.168.8.190:9799/deploy`, signed payload (HMAC-SHA256, secret in `/etc/plato-deploy/secret`).
- Logs: `journalctl -u plato-deploy-webhook -f`.
- Health: `curl http://127.0.0.1:9799/health`.
- Manual deploy: `/opt/plato-deploy/deploy.sh`.
- Gitea's `app.ini` `ALLOWED_HOST_LIST` was extended with `192.168.8.190` to allow this delivery.
## Fresh-DB bootstrap workaround
The `schema` constant in `backend/internal/views/store.go` (as of commit `e0542c0`) creates the `views` table without `project_id`, then immediately runs `CREATE UNIQUE INDEX … ON views(project_id, lower(title))`. On a fresh DB this fails (no such column) and Plato crash-loops with `open views store: SQL logic error: no such column: project_id`. `ensureProjectIDColumn()` adds the column on subsequent migrations, but the schema apply happens first.
Until the upstream fix lands, pre-seed the DB before first start:
```
docker compose stop
rm -f /opt/plato/data/plato.db
python3 - <<'PY'
import sqlite3
c = sqlite3.connect('/opt/plato/data/plato.db')
c.executescript("""
CREATE TABLE views (
id TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'document',
title TEXT NOT NULL,
aliases TEXT NOT NULL DEFAULT '[]',
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX views_project_title_lower ON views(project_id, lower(title));
CREATE INDEX views_project_id ON views(project_id);
""")
c.commit()
PY
chown 65532:65532 /opt/plato/data/plato.db
docker compose up -d
```
Once the column exists, every subsequent boot's `IF NOT EXISTS` clauses no-op. Once Plato is fixed upstream (remove the `CREATE UNIQUE INDEX` line from the boot `schema` constant — `ensureTitleIndexPerProject()` already re-creates it after the migration), this preseed becomes unnecessary.
## Why privileged
Matches the docker-host convention used by [120 mule-images](120-mule-images.md) and [122 arriman](122-arriman.md). Distroless nonroot's UID `65532` on the host bind mount maps directly through; unprivileged would shift the UID by the idmap offset and the container couldn't write `/data` without extra plumbing.
## Caddy
```
plato.hubris.network {
tls {
dns ionos {env.IONOS_AUTH_API_TOKEN}
}
reverse_proxy 192.168.8.190:8080
}
```
No Authentik forward-auth — Plato has no auth model yet; access control is "be on the LAN or the mesh".
## DNS
dnsmasq entry on [124-authentik](124-authentik.md):
```
address=/plato.hubris.network/192.168.8.175
```
## Related
- [Caddy (121)](121-caddy.md)
- [DNS (split-horizon)](../infrastructure/dns.md)
- [Media permissions](../infrastructure/media-permissions.md)
## Changelog
### 2026-05-13 — auto-deploy wired
Shape B pipeline added (`/opt/plato-deploy/`, port `9799`, gitea hook id 8). `ALLOWED_HOST_LIST` in gitea `app.ini` extended with `192.168.8.190`. See [auto-deploy](../infrastructure/auto-deploy.md#plato).
### 2026-05-13 — container created, Plato deployed
LXC 126 stood up on Debian 12 standard, privileged, docker-ce installed. Plato cloned from `dtoro/Plato`, built and started. Caddy site and dnsmasq split-horizon entry added. Recycled the IP/ID slot freed earlier the same day by the [decommissioned Seafile experiment (LXC 125)](index.md#recently-destroyed-kept-for-archaeology). Hit the [fresh-DB bootstrap bug](#fresh-db-bootstrap-workaround) on first boot; worked around by pre-seeding the SQLite schema.

48
containers/128-trmnl.md Normal file
View File

@@ -0,0 +1,48 @@
# 128 — `trmnl`
Self-hosted middleware for TRMNL e-ink plugins. TRMNL cloud polls it; it fetches/shapes live data into JSON the plugin's Liquid template renders.
## At a glance
- **Hostname:** `trmnl`
- **IP:** `192.168.8.211`
- **Privilege:** unprivileged
- **Resources:** 1 core / 768 MiB RAM / 8 GiB rootfs (Debian 13)
- **Mounts:** none
- **Public hostname:** `trmnl.hubris.network` (via [VPS ingress](../infrastructure/ingress.md))
## Role
Runs one FastAPI aggregator (`server.app:app`, port 9851) that mounts a router per plugin from the `dtoro/terminalito` repo. First consumer: `munich-home` (`/munich-home/dashboard`) — weather (Open-Meteo), MVG transit, Google Calendar, plus server-side Kita/quote logic. Talks out to the public internet for those APIs; TRMNL cloud polls it inbound every 15 min. Bearer-token gated (`TRMNL_POLL_TOKEN`); `/health` is open.
## Service / port map
| Service | Listen | Notes |
|---------|--------|-------|
| `trmnl-plugins` | `0.0.0.0:9851` | uvicorn aggregator; `EnvironmentFile=/etc/trmnl-plugins/env` |
## Storage / config paths
- `/opt/terminalito` — git checkout (origin = internal gitea `http://192.168.8.121:3000/dtoro/terminalito.git`)
- `/opt/terminalito/server/.venv` — venv
- `/etc/trmnl-plugins/env``TRMNL_POLL_TOKEN` (+ Google/MVG creds once enrolled)
- `/etc/systemd/system/trmnl-plugins.service`
## Auto-deploy
Wired — [auto-deploy](../infrastructure/auto-deploy.md) Shape B, webhook id 12 on `dtoro/terminalito``http://192.168.8.211:9797/deploy` (`terminalito-deploy.service`). Push to `main``server/deploy/deploy.sh` (`git pull` + pip + reinstall units + restart `trmnl-plugins`). Secret `/etc/terminalito-deploy/secret`; git creds `/etc/terminalito-deploy/git-credentials` wired as a repo-local `credential.helper`. Manual: `pct exec 128 -- /opt/terminalito/server/deploy/deploy.sh`.
## Secrets
Not yet SOPS-enrolled. The poll token is set directly in `/etc/trmnl-plugins/env`. Google Calendar + MVG creds are pending: enroll via `homelab client add trmnl` + bootstrap, add `secrets/trmnl-oauth.yaml`, then `server/deploy/render-env.sh` builds the env from `homelab secret trmnl-oauth`. Until then calendar/transit cards degrade to empty; weather works.
## Related
- [Caddy (121)](121-caddy.md) — LAN reverse proxy (`trmnl.hubris.network → 192.168.8.211:9851`)
- [VPS ingress](../infrastructure/ingress.md) — public edge (cert mirror + traefik router)
- [DNS (107)](107-dns.md) — Technitium A record `trmnl → 192.168.8.175` (LAN path via Caddy)
- [Gitea (104)](104-gitea.md) — source repo `dtoro/terminalito`
- [Plan: 2026-06-24 TRMNL plugins LXC](../plans/2026-06-24-trmnl-plugins-lxc.md)
## Changelog
### 2026-06-24 — auto-deploy + LAN DNS wired
Gitea Shape-B deploy pipeline (webhook id 12, `:9797`) — push to `dtoro/terminalito` redeploys; verified end-to-end. Technitium A record `trmnl.hubris.network → 192.168.8.175` added on [dns (107)](107-dns.md) (propagated to the NetBird managed zone via dns-sync), so LAN clients take the short path through [Caddy (121)](121-caddy.md). See [auto-deploy](../infrastructure/auto-deploy.md).
### 2026-06-24 — public path live
Verified end-to-end from the internet: `https://trmnl.hubris.network/munich-home/dashboard` → 200 with token, 401 without; `/health` 200. The provision-time outage was the netbird `home-lab-network` (192.168.8.0/24) route having no active routing peer — the **mac-mini routing peer's netbird daemon was down** (artifacto/blog were 504 too). Bringing netbird up on mac-mini restored the route; the edge recovered with no config change. See [ingress](../infrastructure/ingress.md) / [mesh](../infrastructure/mesh.md).
### 2026-06-24 — provisioned
LXC 128 created (Debian 13, unprivileged, `192.168.8.211`). Deployed `trmnl-plugins.service` on :9851 from `dtoro/terminalito`. Caddy block added (`dtoro/caddy-conf`) + LE cert via IONOS DNS-01; verified `/health` 200 and `/munich-home/dashboard` (live weather) through Caddy. Cert mirrored to VPS (`trmnl.fullchain.crt`/`trmnl.privkey.key`) + traefik router `trmnl-public``192.168.8.211:9851` added to `/opt/traefik-dynamic.yaml`. **Public path pending**: VPS↔home netbird route was down at provision time (`No networks available`, 3/6 peers — artifacto/blog also 504); resolves when the mesh route recovers. **LAN pending**: Technitium A record not yet added. Not SOPS-enrolled; Google/MVG creds pending.

51
containers/129-house.md Normal file
View File

@@ -0,0 +1,51 @@
# 129 — `house`
Yuvomi family planner (formerly Oikos). Self-hosted family planner with 14 modules: calendar, tasks, meals, groceries, budget, documents, notes, contacts, birthdays, housekeeping, recipes, reminders.
## At a glance
- **Hostname:** `house`
- **IP:** `192.168.8.212` (static)
- **Privilege:** unprivileged
- **Resources:** 1 core / 1344 MiB RAM / 8 GiB rootfs (Debian 13)
- **Mounts:** none
- **Public hostname:** [`house.hubris.network`](../infrastructure/ingress.md) → VPS traefik → Caddy
## Service / port map
| Service | Listen | Notes |
|---------|--------|-------|
| `oikos` (Yuvomi) | `0.0.0.0:3000` | Docker Compose at `/opt/yuvomi/`, image `ghcr.io/ulsklyc/yuvomi` |
## Integrations
- **Authentik SSO (OIDC):** Provider `Provider for Yuvomi` (PK 31) in Authentik on VPS. Env vars in `/opt/yuvomi/.env`: `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`. Redirect URI: `https://house.hubris.network/auth/oidc/callback`.
- **Paperless DMS connector (native):** Yuvomi connects directly to Paperless-ngx API at `http://192.168.8.130:8000/`. API token stored in SQLite `dms_accounts` table. Search, link, and upload documents from Yuvomi to Paperless via Settings → Documents → DMS.
- **Weather widget:** Open-Meteo (free, no API key). Munich coordinates set.
- **Google Calendar:** OAuth configured via env vars (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`). Redirect URI: `https://house.hubris.network/api/v1/calendar/google/callback`. Authorize in Settings → Calendar → Connect Google Calendar.
## Config paths
- `/opt/yuvomi/docker-compose.yml` — downloaded from upstream
- `/opt/yuvomi/.env` — config including secrets (untracked)
- `/opt/yuvomi/data/` — SQLCipher SQLite DB (`oikos.db`)
- `/opt/yuvomi/backups/` — auto backups
- `/opt/yuvomi/modules/` — Yuvomi modules (empty for now)
## Related
- [Caddy (121)](121-caddy.md) — LAN reverse proxy (`house.hubris.network → 192.168.8.212:3000`)
- [VPS ingress](../infrastructure/ingress.md) — public edge (cert mirror + traefik router)
- [DNS (107)](107-dns.md) — Technitium A record `house → 192.168.8.175` (LAN path via Caddy)
- [Paperless (103)](103-paperless.md) — native DMS connector (API at `:8000`)
- [TRMNL (128)](128-trmnl.md) — Google Calendar tokens source
- [Deployment plan](../plans/2026-06-25-yuvomi-deployment.md)
## Changelog
### 2026-06-27 — Google Calendar OAuth env vars configured
`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` set in `.env`. New OAuth client ID (`-bho4iq..`).
### 2026-06-26 — provisioned
LXC 129 created (Debian 13, unprivileged, `192.168.8.212`). Docker installed. Yuvomi container running on `:3000` from `ghcr.io/ulsklyc/yuvomi:latest`. Caddy block + DNS A record + VPS traefik router `house-public` for public access. Authentik OIDC provider created (PK 31). WebDAV document bridge on paperless LXC (103) at `:8088` for Paperless auto-import.

View File

@@ -0,0 +1,65 @@
# 130 — `grimmory`
Self-hosted digital library (eBooks, comics, audiobooks). Community fork/successor of Booklore, with smart shelves, metadata enrichment, Kobo/KOReader sync, OPDS, and a built-in EPUB/PDF reader. Migrated from [apps (105)](105-apps.md) on 2026-06-29.
## At a glance
- **Hostname:** `grimmory`
- **IP:** `192.168.8.213` (static, set in PVE `net0` config — same pattern as all other LXCs)
- **Privilege:** privileged (UID = host UID for `/mnt/library` media GID)
- **Resources:** 1 core / 2 GiB RAM / 16 GiB rootfs (Debian 13)
- **Mounts:** `/mnt/library`
- **Public hostname:** `books.hubris.network`
## Service / port map
| Service | Listen | Notes |
|---------|--------|-------|
| Grimmory | `192.168.8.213:6060` | Docker Compose at `/opt/grimmory/` |
| MariaDB | internal only | Sidecar in the same compose stack |
## Compose
Located at `/opt/grimmory/docker-compose.yml`. Key points:
- Image: `ghcr.io/grimmory-tools/grimmory:latest`
- Books library: `/mnt/library/books``/books` (read-write; media GID 10000 via `GROUP_ID=10000`)
- Bookdrop (watched folder for auto-import): `/opt/grimmory/bookdrop``/bookdrop`
- App data (covers, DB config): `/opt/grimmory/data``/app/data`
- MariaDB config: `/opt/grimmory/mariadb/config``/config` (linuxserver/mariadb image)
- `extra_hosts: auth.hubris.network:192.168.8.175` — routes Authentik OIDC discovery to Caddy from inside the container
- `FORCE_DISABLE_OIDC=false` — OIDC stays enabled; provider configured via Grimmory admin UI
Credentials live in `/opt/grimmory/.env` (untracked):
- `DATABASE_PASSWORD` / `MYSQL_PASSWORD` — MariaDB Grimmory user password
- `MYSQL_ROOT_PASSWORD` — MariaDB root password
## Authentik OIDC
Uses Confidential client (client secret stored in Grimmory's DB — migrated from Booklore). The OIDC config carried over in the database dump; no manual re-entry needed.
- **Authentik provider:** `Provider for Grimmory` (renamed from `Provider for Booklore` on migration)
- **Client ID:** `L1u0eFsNhbKgiIvvFeIr2mvZdbtFyzidCq2h6thL`
- **Client type:** Confidential (client secret in `oidc_provider_details` in MariaDB `app_settings`)
- **Redirect URI:** `https://books.hubris.network/oauth2-callback`
- **Scopes:** openid, profile, email, offline_access
- **Back-channel logout:** `http://192.168.8.213:6060/api/v1/auth/oidc/backchannel-logout`
- **Application slug:** `booklore` → Issuer URI: `https://auth.hubris.network/application/o/booklore/`
## Media permissions
LXC is privileged → in-container UID = host UID. Docker container gets media GID via `GROUP_ID=10000` env var (Grimmory/linuxserver pattern). The `/mnt/library/books` subtree is owned `:media` mode `2775` (setgid). See [media-permissions](../infrastructure/media-permissions.md).
## Related
- [apps (105)](105-apps.md) — previous host (Booklore)
- [Caddy (121)](121-caddy.md) — `books.hubris.network → 192.168.8.213:6060`
- [Authentik (124)](124-authentik.md) — OIDC provider `Grimmory`
- [DNS (107)](107-dns.md) — `books.hubris.network → 192.168.8.175` (unchanged from Booklore)
- [Media permissions](../infrastructure/media-permissions.md)
## Changelog
### 2026-06-29 — provisioned; Booklore migrated
LXC 130 created (Debian 13, privileged, `192.168.8.213`). Docker installed. Grimmory compose deployed at `/opt/grimmory/`. MariaDB dump from Booklore (LXC 105) restored — schema-compatible since Grimmory is a direct fork. Caddy `books.hubris.network` backend updated from `192.168.8.205:6060` to `192.168.8.213:6060`. Authentik provider updated: Booklore → Grimmory, Confidential → Public (PKCE). Booklore stack removed from Portainer on LXC 105.

View File

@@ -0,0 +1,112 @@
# 131 — `teddycloud`
Open-source replacement server for Toniebox smart audio devices (Tonieboxes). Serves device content and API on port 443 and exposes a management web UI at `teddy.hubris.network`.
## At a glance
- **Hostname:** `teddycloud`
- **IP:** `192.168.8.243` (DHCP reservation; MAC `bc:24:11:11:7a:df`)
- **Privilege:** privileged
- **Resources:** 1 core / 1 GiB RAM / 16 GiB rootfs (Debian 12)
- **Mounts:** `/mnt/library` (`mp0`) — TeddyCloud content at `/mnt/library/cloud/leon`
- **Public hostname:** none (LAN-only)
## Role
Replaces the Boxine cloud (`prod.de.bb-online.com`) as the backend for Leon's Toniebox. Tonieboxes connect on port 443 using a custom CA cert issued by TeddyCloud. Content (Tonies) is stored on the NAS at `/mnt/library/cloud/leon` and is accessible from the management UI.
## Service / port map
| Service | Listen | Notes |
|---------|--------|-------|
| TeddyCloud device API | `0.0.0.0:443` | HTTPS, TeddyCloud self-signed CA, Toniebox connects here |
| TeddyCloud HTTP | `0.0.0.0:80` | Redirects to 443 |
| TeddyCloud web UI | `0.0.0.0:8443` | HTTPS management UI — fronted by Caddy at `teddy.hubris.network` (backend uses `tls_insecure_skip_verify` for self-signed cert on LAN hop) |
## Docker Compose
`/opt/teddycloud/docker-compose.yml`:
```yaml
services:
teddycloud:
image: ghcr.io/toniebox-reverse-engineering/teddycloud:latest
ports:
- "80:80"
- "443:443"
- "8443:8443"
volumes:
- certs:/teddycloud/certs
- config:/teddycloud/config
- /mnt/library/cloud/leon:/teddycloud/content
- /mnt/library/cloud/leon:/teddycloud/library
restart: unless-stopped
volumes:
certs:
config:
```
`certs` and `config` are Docker named volumes (runtime state). `content` and `library` are bind-mounted from `/mnt/library/cloud/leon` so audio content persists across container rebuilds and is browsable from the host.
## Storage / config paths
- `/opt/teddycloud/docker-compose.yml` — compose file
- Docker volume `teddycloud_certs` — TeddyCloud CA + server certs (generated on first boot)
- Docker volume `teddycloud_config` — TeddyCloud config
- `/mnt/library/cloud/leon/` — Tonie content + library (NAS bind mount)
## Networking
Two separate traffic paths — different IPs, no port 443 conflict:
**Management UI (browser):**
```
teddy.hubris.network → Technitium → 192.168.8.175 (Caddy) → 192.168.8.243:8443
```
**Toniebox device traffic:**
```
prod.de.bb-online.com → Technitium override → 192.168.8.243:443 (TeddyCloud direct)
```
Caddy terminates TLS for the management UI (IONOS DNS-01 wildcard cert). TeddyCloud terminates TLS for device traffic with its own self-signed CA — the Toniebox must have this CA installed.
### DNS overrides in Technitium
| Record | Type | Value | Purpose |
|--------|------|-------|---------|
| `teddy.hubris.network` | A | `192.168.8.175` | Management UI → Caddy (standard pattern) |
| `prod.de.bb-online.com` | A | `192.168.8.243` | Toniebox device traffic → TeddyCloud direct |
The `prod.de.bb-online.com` override is Technitium-only — it intercepts Toniebox DNS locally without touching public DNS. The `dns-sync.py` cron on LXC 107 skips non-`hubris.network` records, so it stays local.
## Config notes
- `core.boxCertAuth=false` — client cert validation disabled. The box connects without presenting its unique client cert. Set in `/var/lib/docker/volumes/teddycloud_config/_data/config.ini` (TeddyCloud hot-reloads on change).
- If you ever want per-box auth, flip to `true` and supply `certs/client/ca.der`, `client.der`, `private.der` extracted from the box flash.
## Toniebox onboarding — ESP32 SD card method
Leon's box is ESP32 generation. No hardware mod required.
1. Download the TeddyCloud CA cert from the web UI: **Security → CA Certificate → Download CA** (`ca.der`).
2. Power off the Toniebox, remove the SD card.
3. On the SD card, create folder `cert/` at the root.
4. Copy the downloaded `ca.der` into `cert/ca.der` on the SD card.
5. Reinsert SD card, power on the box.
6. The box patches itself to trust TeddyCloud's CA, then resolves `prod.de.bb-online.com` via Technitium's override (`192.168.8.243`) and connects on port 443.
Reference: [upstream wiki — ESP32 SD card method](https://github.com/toniebox-reverse-engineering/teddycloud/wiki).
## Related
- [Caddy (121)](121-caddy.md) — LAN reverse proxy (`teddy.hubris.network → 192.168.8.243:8443`)
- [DNS (107)](../infrastructure/dns.md) — Technitium A records for `teddy.hubris.network` and `prod.de.bb-online.com`
- [Media permissions](../infrastructure/media-permissions.md) — NAS `/mnt/library` mount pattern
## Changelog
### 2026-06-29 — provisioned
LXC 131 created (Debian 12, privileged, nesting=1). Docker installed. TeddyCloud running via Docker Compose at `/opt/teddycloud/`. Content bind-mounted from `/mnt/library/cloud/leon`. Caddy block added at `teddy.hubris.network → :8443`. Technitium A records: `teddy.hubris.network → 192.168.8.175` (Caddy), `prod.de.bb-online.com → 192.168.8.243` (device traffic direct).

View File

@@ -7,7 +7,7 @@ All containers live on [`hubris`](../hosts/hubris.md). Each row links to the per
| 101 | [jellyfin](101-jellyfin.md) | 192.168.8.206 | unpriv (idmap) | 2 | 4 GiB | 16 GiB | `/mnt/library` | `media.hubris.network` | running | | 101 | [jellyfin](101-jellyfin.md) | 192.168.8.206 | unpriv (idmap) | 2 | 4 GiB | 16 GiB | `/mnt/library` | `media.hubris.network` | running |
| 103 | [paperless](103-paperless.md) | 192.168.8.130 | priv | 2 | 3 GiB | 8 GiB | `/mnt/library` | `paperless.hubris.network` | running | | 103 | [paperless](103-paperless.md) | 192.168.8.130 | priv | 2 | 3 GiB | 8 GiB | `/mnt/library` | `paperless.hubris.network` | running |
| 104 | [gitea](104-gitea.md) | 192.168.8.121 | priv | 1 | 1 GiB | 8 GiB | `/mnt/library` | `git.hubris.network` | running | | 104 | [gitea](104-gitea.md) | 192.168.8.121 | priv | 1 | 1 GiB | 8 GiB | `/mnt/library` | `git.hubris.network` | running |
| 105 | [apps](105-apps.md) | 192.168.8.205 | priv | 2 | 4 GiB | 30 GiB | `/mnt/library` | `docker` / `books` / `artifacto` / `blog` | running | | 105 | [apps](105-apps.md) | 192.168.8.205 | priv | 2 | 4 GiB | 30 GiB | `/mnt/library` | `docker` / `artifacto` / `blog` | running |
| 114 | [nextcloud](114-nextcloud.md) | 192.168.8.224 | priv | 4 | 6 GiB | 25 GiB | `/mnt/library` | `cloud.hubris.network` | running | | 114 | [nextcloud](114-nextcloud.md) | 192.168.8.224 | priv | 4 | 6 GiB | 25 GiB | `/mnt/library` | `cloud.hubris.network` | running |
| 118 | [elementsynapse](118-elementsynapse.md) | 192.168.8.239 | unpriv | 1 | 2 GiB | 8 GiB | — | `matrix.hubris.network` | running | | 118 | [elementsynapse](118-elementsynapse.md) | 192.168.8.239 | unpriv | 1 | 2 GiB | 8 GiB | — | `matrix.hubris.network` | running |
| 119 | [sophia](119-sophia.md) | 192.168.8.157 | priv | 2 | 1 GiB | 10 GiB | `/mnt/library` | — | running | | 119 | [sophia](119-sophia.md) | 192.168.8.157 | priv | 2 | 1 GiB | 10 GiB | `/mnt/library` | — | running |
@@ -15,7 +15,10 @@ All containers live on [`hubris`](../hosts/hubris.md). Each row links to the per
| 121 | [caddy](121-caddy.md) | 192.168.8.175 | unpriv | 1 | 512 MiB | 6 GiB | — | (terminates all `*.hubris.network`) | running | | 121 | [caddy](121-caddy.md) | 192.168.8.175 | unpriv | 1 | 512 MiB | 6 GiB | — | (terminates all `*.hubris.network`) | running |
| 122 | [arriman](122-arriman.md) | 192.168.8.132 | priv | 4 | 8 GiB | 24 GiB | `/mnt/library` | `jellyseerr` / `qbit` / `sab` | running | | 122 | [arriman](122-arriman.md) | 192.168.8.132 | priv | 4 | 8 GiB | 24 GiB | `/mnt/library` | `jellyseerr` / `qbit` / `sab` | running |
| 124 | [authentik](124-authentik.md) | 192.168.8.180 | priv | 2 | 4 GiB | 20 GiB | — | `auth.hubris.network` | running | | 124 | [authentik](124-authentik.md) | 192.168.8.180 | priv | 2 | 4 GiB | 20 GiB | — | `auth.hubris.network` | running |
| 126 | [plato](126-plato.md) | 192.168.8.190 | priv | 2 | 2 GiB | 8 GiB | `/mnt/library/documents/plato` | `plato.hubris.network` | running | | 128 | [trmnl](128-trmnl.md) | 192.168.8.211 | unpriv | 1 | 768 MiB | 8 GiB | — | `trmnl.hubris.network` | running |
| 129 | [house](129-house.md) | 192.168.8.212 | unpriv | 1 | 1344 MiB | 8 GiB | — | `house.hubris.network` | running |
| 130 | [grimmory](130-grimmory.md) | 192.168.8.213 | priv | 1 | 2 GiB | 16 GiB | `/mnt/library` | `books.hubris.network` | running |
| 131 | [teddycloud](131-teddycloud.md) | 192.168.8.243 | priv | 1 | 1 GiB | 16 GiB | `/mnt/library` | `teddy.hubris.network` (LAN only) | running |
## Recently destroyed (kept for archaeology) ## Recently destroyed (kept for archaeology)
@@ -25,6 +28,7 @@ All containers live on [`hubris`](../hosts/hubris.md). Each row links to the per
| 100 | arr (yunohost) | ~2026-04-28 | Migrated to docker stack on [arriman](122-arriman.md); planned retention window expired | | 100 | arr (yunohost) | ~2026-04-28 | Migrated to docker stack on [arriman](122-arriman.md); planned retention window expired |
| 106 | flaresolverr | ~2026-04-28 | Folded into the arriman docker compose | | 106 | flaresolverr | ~2026-04-28 | Folded into the arriman docker compose |
| 116 | heaper | 2026-05-14 | Decommissioned by user; data subtree at `/mnt/library/heaper` (224 MiB) retained | | 116 | heaper | 2026-05-14 | Decommissioned by user; data subtree at `/mnt/library/heaper` (224 MiB) retained |
| 126 | plato | 2026-06-28 | Notes/discovery workspace decommissioned; data at `/mnt/library/documents/plato` retained for archaeology |
| 123 | claudio-bot | 2026-06-04 | Replaced by Hermes Agent on mac-mini; monitoring migrated to `homelab-health-watchdog` cron. See [deprecation plan](../plans/2026-06-04_130000-deprecate-claudio-bot.md) | | 123 | claudio-bot | 2026-06-04 | Replaced by Hermes Agent on mac-mini; monitoring migrated to `homelab-health-watchdog` cron. See [deprecation plan](../plans/2026-06-04_130000-deprecate-claudio-bot.md) |
| 109 | syncthing | 2026-05-14 | Decommissioned by user; `/mnt/library/syncthing` was already empty | | 109 | syncthing | 2026-05-14 | Decommissioned by user; `/mnt/library/syncthing` was already empty |
| 125 | seafile | 2026-05-13 | Seafile Pro evaluation, user disliked the product; teardown also removed `files.hubris.network` from caddy + dnsmasq | | 125 | seafile | 2026-05-13 | Seafile Pro evaluation, user disliked the product; teardown also removed `files.hubris.network` from caddy + dnsmasq |

View File

@@ -8,10 +8,15 @@ role: docker-apps
host: hubris host: hubris
pve_id: 105 pve_id: 105
lan_ip: 192.168.8.205 lan_ip: 192.168.8.205
mesh:
tailscale:
ip: 100.121.171.122
fqdn: apps
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_hosts: public_hosts:

View File

@@ -8,10 +8,14 @@ role: arr-stack
host: hubris host: hubris
pve_id: 122 pve_id: 122
lan_ip: 192.168.8.132 lan_ip: 192.168.8.132
mesh:
tailscale:
fqdn: arr
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_hosts: public_hosts:

View File

@@ -1,27 +1,21 @@
# Generated by mcp/build_host_files.py from inventory.yaml. # Generated by mcp/build_host_files.py from inventory.yaml.
# Do NOT edit by hand — your changes will be overwritten. # Do NOT edit by hand — your changes will be overwritten.
# Source of truth: ../inventory.yaml # Source of truth: ../inventory.yaml
name: plato name: auth-outpost
kind: lxc kind: lxc
os: linux os: linux
role: app role: authentik-gateway
host: hubris host: hubris
pve_id: 126 pve_id: 106
lan_ip: 192.168.8.190 lan_ip: 192.168.8.6
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
mounts: - tailscale
- /mnt/library/documents/plato notes:
public_host: plato.hubris.network - Runs Authentik outpost (reverse-proxy/SSO enforcement) for protected services
runs:
- plato
services_hosted:
- name: plato
url: https://plato.hubris.network
backend: plato
see_also: see_also:
- containers/126-plato.md - containers/106-auth-outpost.md
mcp_endpoint: https://mcp.hubris.network/mcp mcp_endpoint: https://mcp.hubris.network/mcp
secrets_issuance_endpoint: https://secrets.hubris.network/issue secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

@@ -12,6 +12,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
peers: peers:
- authentik - authentik
- gitea - gitea

30
hosts/dns.yaml Normal file
View File

@@ -0,0 +1,30 @@
# Generated by mcp/build_host_files.py from inventory.yaml.
# Do NOT edit by hand — your changes will be overwritten.
# Source of truth: ../inventory.yaml
name: dns
kind: lxc
os: linux
role: dns-server
host: hubris
pve_id: 107
lan_ip: 192.168.8.2
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
runs:
- authentik
services_hosted:
- name: authentik
url: https://auth.hubris.network
backend: dns
dns: null
note: Technitium DNS, split-horizon zone
notes:
- Technitium DNS, split-horizon zone for *.hubris.network
- Primary DNS for 192.168.8.0/24 LAN (inventory.services.dns references this)
see_also:
- containers/107-dns.md
mcp_endpoint: https://mcp.hubris.network/mcp
secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

@@ -8,10 +8,14 @@ role: matrix-server
host: hubris host: hubris
pve_id: 118 pve_id: 118
lan_ip: 192.168.8.239 lan_ip: 192.168.8.239
mesh:
tailscale:
fqdn: elementsynapse
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
public_host: matrix.hubris.network public_host: matrix.hubris.network
runs: runs:
- matrix - matrix

View File

@@ -8,10 +8,14 @@ role: git-server
host: hubris host: hubris
pve_id: 104 pve_id: 104
lan_ip: 192.168.8.121 lan_ip: 192.168.8.121
mesh:
tailscale:
fqdn: gitea
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_host: git.hubris.network public_host: git.hubris.network

View File

@@ -8,10 +8,14 @@ role: home-automation
host: hubris host: hubris
pve_id: 108 pve_id: 108
lan_ip: 192.168.8.101 lan_ip: 192.168.8.101
mesh:
tailscale:
fqdn: homeassistant
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
runs: runs:
- haos - haos
services_hosted: services_hosted:

24
hosts/house.yaml Normal file
View File

@@ -0,0 +1,24 @@
# Generated by mcp/build_host_files.py from inventory.yaml.
# Do NOT edit by hand — your changes will be overwritten.
# Source of truth: ../inventory.yaml
name: house
kind: lxc
os: linux
role: family-planner
host: hubris
pve_id: 129
lan_ip: 192.168.8.212
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
public_host: house.hubris.network
notes:
- Docker host for Yuvomi (family planner). Created 2026-06-26.
- Runs Yuvomi container + WebDAV doc bridge to paperless
age_pubkey: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
see_also:
- containers/129-house.md
mcp_endpoint: https://mcp.hubris.network/mcp
secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

@@ -14,6 +14,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
ssh: ssh:

View File

@@ -8,10 +8,14 @@ role: media-server
host: hubris host: hubris
pve_id: 101 pve_id: 101
lan_ip: 192.168.8.206 lan_ip: 192.168.8.206
mesh:
tailscale:
fqdn: jellyfin
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_host: media.hubris.network public_host: media.hubris.network

View File

@@ -13,6 +13,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
ssh: ssh:
user: dtoro user: dtoro
mcp_endpoint: https://mcp.hubris.network/mcp mcp_endpoint: https://mcp.hubris.network/mcp

View File

@@ -13,6 +13,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
ssh: ssh:
user: dtoro user: dtoro
notes: notes:

View File

@@ -8,10 +8,14 @@ role: photo-management
host: hubris host: hubris
pve_id: 120 pve_id: 120
lan_ip: 192.168.8.136 lan_ip: 192.168.8.136
mesh:
tailscale:
fqdn: muleimage
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_host: photos.hubris.network public_host: photos.hubris.network

View File

@@ -13,6 +13,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
ssh: ssh:
user: root user: root
notes: notes:

View File

@@ -8,10 +8,14 @@ role: file-sync
host: hubris host: hubris
pve_id: 114 pve_id: 114
lan_ip: 192.168.8.224 lan_ip: 192.168.8.224
mesh:
tailscale:
fqdn: nextcloud
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_host: cloud.hubris.network public_host: cloud.hubris.network

View File

@@ -12,6 +12,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
see_also: see_also:
- containers/102-nfs-export.md - containers/102-nfs-export.md
mcp_endpoint: https://mcp.hubris.network/mcp mcp_endpoint: https://mcp.hubris.network/mcp

View File

@@ -8,10 +8,14 @@ role: document-archive
host: hubris host: hubris
pve_id: 103 pve_id: 103
lan_ip: 192.168.8.130 lan_ip: 192.168.8.130
mesh:
tailscale:
fqdn: paperless
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
public_host: paperless.hubris.network public_host: paperless.hubris.network

View File

@@ -12,6 +12,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
ssh: ssh:
user: dtoro user: dtoro
mcp_endpoint: https://mcp.hubris.network/mcp mcp_endpoint: https://mcp.hubris.network/mcp

View File

@@ -8,10 +8,14 @@ role: workshop
host: hubris host: hubris
pve_id: 119 pve_id: 119
lan_ip: 192.168.8.109 lan_ip: 192.168.8.109
mesh:
tailscale:
fqdn: sophia
mesh_globals: mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
mounts: mounts:
- /mnt/library - /mnt/library
see_also: see_also:

27
hosts/trmnl.yaml Normal file
View File

@@ -0,0 +1,27 @@
# Generated by mcp/build_host_files.py from inventory.yaml.
# Do NOT edit by hand — your changes will be overwritten.
# Source of truth: ../inventory.yaml
name: trmnl
kind: lxc
os: linux
role: trmnl-middleware
host: hubris
pve_id: 128
lan_ip: 192.168.8.211
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
public_host: trmnl.hubris.network
runs:
- trmnl
services_hosted:
- name: trmnl
backend: trmnl
url: https://trmnl.hubris.network
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
see_also:
- containers/128-trmnl.md
mcp_endpoint: https://mcp.hubris.network/mcp
secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

@@ -12,6 +12,7 @@ mesh_globals:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
public_host: zimaos.hubris.network public_host: zimaos.hubris.network
runs: runs:
- zimaos - zimaos

View File

@@ -23,7 +23,8 @@ The app repo at `/opt/<thing>` is the working tree, but the deploy tooling (`web
- ~~`192.168.8.230` (claudio-bot — destroyed 2026-06-04)~~ - ~~`192.168.8.230` (claudio-bot — destroyed 2026-06-04)~~
- `192.168.8.136` ([mule-images (120)](../containers/120-mule-images.md)) - `192.168.8.136` ([mule-images (120)](../containers/120-mule-images.md))
- `192.168.8.77` ([hubris host](../hosts/hubris.md) — backup-library) - `192.168.8.77` ([hubris host](../hosts/hubris.md) — backup-library)
- `192.168.8.190` ([plato (126)](../containers/126-plato.md)) - ~~`192.168.8.190` ([plato (126)](../containers/126-plato.md))~~ (destroyed 2026-06-28)
- `192.168.8.211` ([trmnl (128)](../containers/128-trmnl.md) — terminalito)
**Don't strip these when editing app.ini.** **Don't strip these when editing app.ini.**
@@ -37,11 +38,12 @@ The app repo at `/opt/<thing>` is the working tree, but the deploy tooling (`web
| `dtoro/gitea-customizations` | [gitea (104)](../containers/104-gitea.md) `/var/lib/gitea/custom/` | A | `http://127.0.0.1:9797/deploy` (loopback) | (orig) | `systemctl restart gitea` if templates changed | | `dtoro/gitea-customizations` | [gitea (104)](../containers/104-gitea.md) `/var/lib/gitea/custom/` | A | `http://127.0.0.1:9797/deploy` (loopback) | (orig) | `systemctl restart gitea` if templates changed |
| `dtoro/mule-image` | [mule-images (120)](../containers/120-mule-images.md) `/opt/mule-image/` | B | `http://192.168.8.136:9797/deploy` | 6 | `docker compose up -d --build` | | `dtoro/mule-image` | [mule-images (120)](../containers/120-mule-images.md) `/opt/mule-image/` | B | `http://192.168.8.136:9797/deploy` | 6 | `docker compose up -d --build` |
| `dtoro/Artifacto` | [apps (105)](../containers/105-apps.md) `/opt/artifacto/` | B | `http://192.168.8.205:9798/deploy` | 7 | `docker compose up -d --build` | | `dtoro/Artifacto` | [apps (105)](../containers/105-apps.md) `/opt/artifacto/` | B | `http://192.168.8.205:9798/deploy` | 7 | `docker compose up -d --build` |
| `dtoro/Plato` | [plato (126)](../containers/126-plato.md) `/opt/plato/app/` | B | `http://192.168.8.190:9799/deploy` | 8 | `docker compose up -d --build` | | ~~`dtoro/Plato`~~ | ~~[plato (126)](../containers/126-plato.md) `/opt/plato/app/`~~ (destroyed 2026-06-28) | | `http://192.168.8.190:9799/deploy` (dead) | 8 (removed) | Repo archived — LXC destroyed |
| `dtoro/claudio-bot` | ~~[claudio-bot (123)](../containers/123-claudio-bot.md)~~ (destroyed 2026-06-04) | ⊘ | `http://192.168.8.230:9797/deploy` (dead) | (archived) | Repo archived — LXC destroyed | | `dtoro/claudio-bot` | ~~[claudio-bot (123)](../containers/123-claudio-bot.md)~~ (destroyed 2026-06-04) | ⊘ | `http://192.168.8.230:9797/deploy` (dead) | (archived) | Repo archived — LXC destroyed |
| `dtoro/backup-library` | [hubris host](../hosts/hubris.md) `/opt/backup-library/` | A | `http://192.168.8.77:9798/deploy` | (orig) | runs `deploy.sh` (preserves admin-edited `/etc/restic/include-*.list`) | | `dtoro/backup-library` | [hubris host](../hosts/hubris.md) `/opt/backup-library/` | A | `http://192.168.8.77:9798/deploy` | (orig) | runs `deploy.sh` (preserves admin-edited `/etc/restic/include-*.list`) |
| `dtoro/Homelab-Docs` → homelab-mcp | [apps (105)](../containers/105-apps.md) `/opt/homelab-mcp/` | B | `http://192.168.8.205:9811/deploy` | 10 | reinstalls `homelab-mcp.service` + restart | | `dtoro/Homelab-Docs` → homelab-mcp | [apps (105)](../containers/105-apps.md) `/opt/homelab-mcp/` | B | `http://192.168.8.205:9811/deploy` | 10 | reinstalls `homelab-mcp.service` + restart |
| `dtoro/Homelab-Docs` → secrets-issuance | [apps (105)](../containers/105-apps.md) `/opt/secrets-issuance/` | B | `http://192.168.8.205:9821/deploy` | 11 | reinstalls `secrets-issuance.service` + restart | | `dtoro/Homelab-Docs` → secrets-issuance | [apps (105)](../containers/105-apps.md) `/opt/secrets-issuance/` | B | `http://192.168.8.205:9821/deploy` | 11 | reinstalls `secrets-issuance.service` + restart |
| `dtoro/terminalito` | [trmnl (128)](../containers/128-trmnl.md) `/opt/terminalito/` | B | `http://192.168.8.211:9797/deploy` | 12 | reinstalls units + `systemctl restart trmnl-plugins` |
> Note: `dtoro/Homelab-Docs` has **two webhooks** firing on the same push. > Note: `dtoro/Homelab-Docs` has **two webhooks** firing on the same push.
> Each owns its own clone on LXC 105. They don't conflict because each > Each owns its own clone on LXC 105. They don't conflict because each
@@ -57,7 +59,7 @@ Always commit + push. Local-only edits drift. Common ones:
- `/var/lib/gitea/custom/``dtoro/gitea-customizations` (auto-deploys) - `/var/lib/gitea/custom/``dtoro/gitea-customizations` (auto-deploys)
- `/opt/artifacto/``dtoro/Artifacto` (auto-deploys) - `/opt/artifacto/``dtoro/Artifacto` (auto-deploys)
- `/opt/mule-image/``dtoro/mule-image` (auto-deploys) - `/opt/mule-image/``dtoro/mule-image` (auto-deploys)
- `/opt/plato/app/``dtoro/Plato` (auto-deploys) - ~~`/opt/plato/app/``dtoro/Plato`~~ (destroyed 2026-06-28)
- ~~`/opt/claudio-bot/``dtoro/claudio-bot`~~ (destroyed 2026-06-04) - ~~`/opt/claudio-bot/``dtoro/claudio-bot`~~ (destroyed 2026-06-04)
- `/opt/backup-library/``dtoro/backup-library` (auto-deploys) - `/opt/backup-library/``dtoro/backup-library` (auto-deploys)
- `/opt/homelab-mcp/` + `/opt/secrets-issuance/``dtoro/Homelab-Docs` (auto-deploys both, see [homelab-context](homelab-context.md)) - `/opt/homelab-mcp/` + `/opt/secrets-issuance/``dtoro/Homelab-Docs` (auto-deploys both, see [homelab-context](homelab-context.md))
@@ -72,11 +74,6 @@ Always commit + push. Local-only edits drift. Common ones:
- Receiver is on **loopback** (`127.0.0.1:9797`), not the LXC IP. - Receiver is on **loopback** (`127.0.0.1:9797`), not the LXC IP.
- Online3DViewer binary assets are NOT tracked; `deploy.sh` fetches them on first run. - Online3DViewer binary assets are NOT tracked; `deploy.sh` fetches them on first run.
### Plato
- Shape B (`/opt/plato-deploy/{webhook.py,deploy.sh}`, port `9799`).
- The in-LXC checkout's `origin` is `http://192.168.8.121:3000/dtoro/Plato.git` (internal gitea), and git creds are at `/root/.git-credentials` rather than the `/etc/plato-deploy/git-credentials` pattern — the unit doesn't set `ProtectHome` so root's home is reachable.
- `/data` is a host bind (`/mnt/library/documents/plato`), so `docker compose up -d --build` rebuilds the image + restarts the container without touching the SQLite db. The [fresh-DB bootstrap workaround](../containers/126-plato.md#fresh-db-bootstrap-workaround) only matters if you blow `plato.db` away.
### mule-image / Artifacto ### mule-image / Artifacto
- Async deploy (returns 202) — gitea would otherwise time out the request. Logs: `pct exec <id> -- journalctl -u <thing>-deploy-webhook -f`. - Async deploy (returns 202) — gitea would otherwise time out the request. Logs: `pct exec <id> -- journalctl -u <thing>-deploy-webhook -f`.
- **Cloning from inside the LXC must use the internal gitea IP** (`http://192.168.8.121:3000/...`). `https://git.hubris.network` hits a connection reset from inside [apps (105)](../containers/105-apps.md) (Caddy routing / TLS hairpin not configured for this LXC). Configured `origin` on the in-LXC checkout is the internal URL. - **Cloning from inside the LXC must use the internal gitea IP** (`http://192.168.8.121:3000/...`). `https://git.hubris.network` hits a connection reset from inside [apps (105)](../containers/105-apps.md) (Caddy routing / TLS hairpin not configured for this LXC). Configured `origin` on the in-LXC checkout is the internal URL.
@@ -123,6 +120,12 @@ If you're not sure what's already lurking, run `homelab apt-audit --fleet` and l
## Changelog ## Changelog
### 2026-06-28 — Plato pipeline decommissioned
LXC 126 destroyed, webhook id 8 on `dtoro/Plato` removed. `192.168.8.190` removed from gitea `app.ini` `ALLOWED_HOST_LIST`.
### 2026-06-24 — terminalito pipeline added
Webhook id 12 on `dtoro/terminalito``http://192.168.8.211:9797/deploy` on [trmnl (128)](../containers/128-trmnl.md). Shape B (`server/deploy/webhook.py` receiver, in-repo `server/deploy/deploy.sh`; secret `/etc/terminalito-deploy/secret`). `app.ini` `ALLOWED_HOST_LIST` extended with `192.168.8.211`. Verified end-to-end with a push. Repo-local `credential.helper` in `/opt/terminalito/.git/config` (the unit can't read root's global git config).
### 2026-05-20 — homelab-mcp + secrets-issuance pipelines added ### 2026-05-20 — homelab-mcp + secrets-issuance pipelines added
Webhook ids 10 + 11 on `dtoro/Homelab-Docs` (ports `9811` + `9821` on [apps (105)](../containers/105-apps.md)). Two webhooks on one repo — each owns its own clone (`/opt/homelab-mcp`, `/opt/secrets-issuance`) and only restarts its own service. See [homelab-context](homelab-context.md) for why both services live in one repo. Webhook ids 10 + 11 on `dtoro/Homelab-Docs` (ports `9811` + `9821` on [apps (105)](../containers/105-apps.md)). Two webhooks on one repo — each owns its own clone (`/opt/homelab-mcp`, `/opt/secrets-issuance`) and only restarts its own service. See [homelab-context](homelab-context.md) for why both services live in one repo.

View File

@@ -7,9 +7,9 @@ There is **no wildcard on the LAN side**. Every subdomain needs an explicit entr
## Components ## Components
- **Authoritative public DNS:** IONOS. `*.hubris.network → 82.165.190.79` (was `74.118.126.4` until 2026-04-22). - **Authoritative public DNS:** IONOS. `*.hubris.network → 82.165.190.79` (was `74.118.126.4` until 2026-04-22).
- **LAN authoritative for `hubris.network` records:** [Technitium DNS](https://technitium.com) on [dns (107)](../containers/107-dns.md) at `192.168.8.2:53` — the **single DNS source** for LAN, household, and mesh. (Through 2026-06-21 it also synced A-records into a NetBird managed zone; that replica was retired in Phase 4, see changelog.) Formerly dnsmasq on [authentik (124)](../containers/124-authentik.md) (decommissioned 2026-06-04). - **LAN authoritative for `hubris.network` records:** [Technitium DNS](https://technitium.com) on [dns (107)](../containers/107-dns.md) at `192.168.8.2:53`. Syncs A records to the NetBird managed DNS zone via cron (see [dns-sync.py](../scripts/dns-sync.py)). Formerly dnsmasq on [authentik (124)](../containers/124-authentik.md) (decommissioned 2026-06-04).
- **PVE host** (`192.168.8.77`): resolver is the local Netbird daemon at `100.122.38.109:53`, which forwards to the LAN/upstream and learns hubris.network answers via that path. `netbird status` says "Nameservers: 0/0 Available" — confirming netbird does NOT manage a hubris.network zone; it just caches whatever the system resolver returns. - **PVE host** (`192.168.8.77`): resolver is the local Netbird daemon at `100.122.38.109:53`, which forwards to the LAN/upstream and learns hubris.network answers via that path. `netbird status` says "Nameservers: 0/0 Available" — confirming netbird does NOT manage a hubris.network zone; it just caches whatever the system resolver returns.
- **All LXCs** now point at Technitium (`192.168.8.2`) directly (since 2026-06-21 — see changelog). The earlier mix of router DNS (`192.168.8.1`) / Tailscale MagicDNS (`100.100.100.100`) which returned the public IONOS A record and forced `/etc/hosts` overrides — has been removed. - **Some LXCs** keep router DNS (`192.168.8.1`) or Tailscale MagicDNS (`100.100.100.100`), both of which return the public IONOS A record. Those LXCs need either a `/etc/hosts` override or local dnsmasq — see [mesh migration](mesh.md) for which technique applies where.
## Live entries (as of 2026-06-04) ## Live entries (as of 2026-06-04)
@@ -32,11 +32,19 @@ address=/blog.hubris.network/192.168.8.175
address=/photos.hubris.network/192.168.8.175 address=/photos.hubris.network/192.168.8.175
address=/photos-new.hubris.network/192.168.8.175 address=/photos-new.hubris.network/192.168.8.175
address=/artifacto.hubris.network/192.168.8.175 address=/artifacto.hubris.network/192.168.8.175
address=/plato.hubris.network/192.168.8.175
address=/zimaos.hubris.network/192.168.8.175 address=/zimaos.hubris.network/192.168.8.175
address=/teddy.hubris.network/192.168.8.175
address=/nfs-export.hubris.network/192.168.8.200 address=/nfs-export.hubris.network/192.168.8.200
``` ```
**Non-`hubris.network` override (Toniebox device traffic):**
```
address=/prod.de.bb-online.com/192.168.8.243 # → TeddyCloud (131) direct on :443
```
This intercepts Toniebox DNS locally without touching public DNS. The `dns-sync.py` cron on LXC 107 skips non-`hubris.network` records — this entry is Technitium-only.
Note: `nfs-export.hubris.network` is the only `.hubris.network` entry that points to a non-HTTP service (NFSv4 on port 2049). It bypasses [caddy (121)](../containers/121-caddy.md) because NFS is L4, not HTTP — Caddy has nothing to do. Note: `nfs-export.hubris.network` is the only `.hubris.network` entry that points to a non-HTTP service (NFSv4 on port 2049). It bypasses [caddy (121)](../containers/121-caddy.md) because NFS is L4, not HTTP — Caddy has nothing to do.
## Why split-horizon ## Why split-horizon
@@ -50,11 +58,11 @@ Creating a new Caddyfile site block is necessary but **not sufficient**. Without
## Recipe — adding a new subdomain ## Recipe — adding a new subdomain
1. Edit `/etc/caddy/Caddyfile` on [caddy (121)](../containers/121-caddy.md), commit + push to `dtoro/caddy-conf`. Webhook reloads caddy. See [auto-deploy](auto-deploy.md). 1. Edit `/etc/caddy/Caddyfile` on [caddy (121)](../containers/121-caddy.md), commit + push to `dtoro/caddy-conf`. Webhook reloads caddy. See [auto-deploy](auto-deploy.md).
2. Add the A record in the [Technitium UI](http://192.168.8.2) at `dns (107)`. That's it — Technitium is the single DNS source; mesh peers forward to it live and LAN/household query it directly. (No managed-zone sync to wait for — retired 2026-06-21, Phase 4.) 2. Add the A record in the [Technitium UI](http://192.168.8.2) at `dns (107)` — the NetBird managed DNS zone sync picks it up within ~10 minutes via cron. Or add directly to the NetBird managed zone via API if you need it faster.
3. Verify: `dig @192.168.8.2 +short <new>.hubris.network``192.168.8.175`. 3. Verify: `dig @192.168.8.2 +short <new>.hubris.network``192.168.8.175`.
4. On macOS clients, flush: `sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder`. On a NetBird peer that caches a stale answer, `netbird service restart` clears its resolver cache. 4. On macOS clients, flush: `sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder`.
> Technitium on LXC 107 is the single source of truth. (Through 2026-06-21 a `dns-sync.py` cron mirrored it into a NetBird managed zone; that replica + cron were removed in Phase 4 once mesh peers were forwarding directly — see [changelog](#2026-06-21--dns-single-source-phase-4-complete--managed-zone-removed-technitium-is-the-single-source).) > The Technitium config on LXC 107 is the single source of truth. Never hand-edit the NetBird managed zone directly — the [`scripts/dns-sync.py`](../scripts/dns-sync.py) cron on 107 reconciles them and reaps stale records. See [dns.md changelog 2026-06-03](#2026-06-03--single-authoring-source-technitium--netbird-managed-zone-sync).
## Public path — what does and doesn't follow the LAN map ## Public path — what does and doesn't follow the LAN map
@@ -76,53 +84,11 @@ Either:
## Changelog ## Changelog
### 2026-06-21DNS single-source, Phase 4: COMPLETE — managed zone removed, Technitium is the single source ### 2026-06-29`teddy.hubris.network` added; `prod.de.bb-online.com` override added
After upgrading the Mac client `0.68.3 → 0.71.3` (matching mgmt) and clearing its NetBird resolver cache (`netbird service restart`), the managed-zone deletion was retried and **works**: with 0 managed-zone records, the Mac resolves `git`/`cloud`/`nfs-export`/`auth`.hubris.network entirely by forwarding to Technitium (`192.168.8.2`). The iPhone confirmed the same on **cellular** (no LAN/Fritz path — proof it's the mesh-forward path). TeddyCloud (LXC 131) provisioned. `teddy.hubris.network → 192.168.8.175` (Caddy → TeddyCloud web UI at :8443). Non-hubris override `prod.de.bb-online.com → 192.168.8.243` routes Toniebox device HTTPS traffic directly to TeddyCloud port 443 — this bypasses Caddy and is Technitium-only (dns-sync cron does not replicate non-hubris.network records to the NetBird managed zone).
So the first deletion attempt (the REVERTED entry below) was a **misdiagnosis**: forwarding wasn't broken — the old 0.68.3 client's resolver cache (`100.122.255.254`) held stale/empty answers and wouldn't clear on `down/up` (only a full daemon restart clears it), and the Mac's dual LAN+mesh resolver paths muddied the test. A direct query (`dig @100.122.255.254 <unsynced-name>`) had in fact shown forwarding working all along. ### 2026-06-28 — `plato.hubris.network` removed
Plato (LXC 126) decommissioned. Technitium entry deleted; dns-sync cron reaped the NetBird managed zone record.
**Done:**
- Deleted all 23 NetBird managed-zone A-records via API.
- Removed the `*/10` sync cron (`/etc/cron.d/dns-sync`). Kept `/opt/dns-sync/sync.py` + token + a pre-deletion backup as an emergency-restore tool only (run it once to rebuild the managed zone if ever reverting).
**End state — one zone:** Technitium (`192.168.8.2`) is the single authoring + serving source. Mesh peers forward to it (route via `Core``192.168.8.0/24`); LAN/household query it directly (Fritz!Box DNSv4 → Technitium). No managed-zone replica, no sync.
**Prereqs for it to keep working:** mesh clients on NetBird **0.71.x+**, and roaming peers in the `Core` group (route) — both satisfied. Other peers (laptops, proxmox-server) pick up forwarding as their resolver caches expire, or after a `netbird service restart`.
**Optional, not done:** flip the Technitium wildcard `*.hubris.network` from `→ 82.165.190.79` (VPS, mirrors public) to `→ 192.168.8.175` (Caddy) so new Caddy services need zero DNS entries. Deferred — decide separately.
### 2026-06-21 — DNS single-source, Phase 4: ATTEMPTED + REVERTED — managed zone is load-bearing
Tried to collapse to a single zone by deleting the NetBird managed-zone replica (and pausing `dns-sync`). Result: **mesh-peer DNS broke.** With the managed zone gone, the Mac (NetBird 0.68.3) failed to resolve `git`/`cloud`/`nfs-export`.hubris.network via the NetBird resolver (`100.122.255.254`) — the `home-lab-dns` nameserver group (`→ 192.168.8.2`) reports `Available` but does **not** actually serve forwarded queries. Restored the managed zone via `dns-sync.py` (23 records) and re-enabled the cron; resolution recovered immediately.
**Correction to the Phase 2 entry below:** the missing route was *a* real problem (it blocked roaming-peer *connectivity* to services — the actual user-facing win), but it was **not** the whole story. DNS forwarding to the routed-LAN IP `192.168.8.2` still does not work for mesh peers, so the original "NetBird won't forward to Technitium" finding stands and the managed zone stays.
**To actually finish single-source later**, forwarding must first be made to work. Two candidates, untried:
- Upgrade NetBird clients to 0.71.x (the Mac is 0.68.3 — version skew with mgmt 0.71.3 is a known source of resolver bugs), then retest.
- Point `home-lab-dns` at a **mesh-native** DNS IP — join CT 107 to the mesh and use its `100.122.x` address instead of the routed-LAN `192.168.8.2` (the original Phase 2 hypothesis; needs a brief 107 restart for `/dev/net/tun`).
Net state after today: Phase 1 (on-prem single resolver) and Phase 2 (roaming route → iPhone reaches the homelab) stand and deliver the practical goals. The managed-zone replica + sync are **retained** as load-bearing.
### 2026-06-21 — DNS single-source, Phase 2: roaming-peer route fixed (see Phase 4 correction above — forwarding still doesn't serve queries; this fixed *connectivity*, not DNS)
The long-standing belief that "NetBird won't forward to Technitium for mesh peers" (which is *why* the managed-zone sync was built) turned out to be **wrong**. The NetBird API showed the real cause:
- The `home-lab-dns` nameserver group (→ `192.168.8.2`, match-domain `hubris.network`) was already applied to **all** peers (via the `All` group). So every peer *had* the forwarding rule.
- But the `192.168.8.0/24` route (network resource `home-lab-network`) was distributed to the **`Services`** group only = `{netbird-ionos, proxmox-server}`. Roaming peers (`dtoro-iphone`, `mac-mini`, `republic-laptop`, `muli-laptop`, `ludo-mini`) are in **`Core`**, which had **no route to the homelab subnet** → they couldn't reach `192.168.8.2` → forwarding silently failed (`Networks: -`).
**Fix:** added the `Core` group to the `home-lab-network` resource distribution (now `[Services, Core]`) via `PUT /api/networks/.../resources/...`. Roaming peers immediately picked up `Networks: 192.168.8.0/24` and `[192.168.8.2:53] for [hubris.network] is Available`. No CT 107 mesh-join / TUN / restart was needed (the original Phase 2 hypothesis is obsolete). This also gives roaming devices full homelab **service** access, not just DNS.
The managed zone is still in place as a fallback pending the Phase 3 roaming test (iPhone on cellular); Phase 4 then deletes the managed zone + `dns-sync` cron. Rollback: PUT the resource back to `[Services]` only.
### 2026-06-21 — DNS single-source, Phase 1: on-prem LXCs decoupled from NetBird
Goal: collapse the three overlapping DNS sources (Technitium + NetBird managed zone + per-LXC band-aids) toward **one zone**, keeping NetBird. Phase 1 (the safe, mesh-independent half) is done:
- **Every homelab LXC now resolves via Technitium (`192.168.8.2`).** Fixed 8 boxes that were on a dead resolver, the router, or Tailscale MagicDNS:
- `192.168.8.180` (dead ex-Authentik): 102, 106, 126
- `192.168.8.1` (router → public answer): 101
- `100.100.100.100` (Tailscale MagicDNS): 104, 105, 114, 119 — also ran `tailscale set --accept-dns=false` so `tailscaled` stops rewriting `/etc/resolv.conf`.
- Already correct (`.2`): 103, 118, 120, 121, 122. CT 107 stays on `1.1.1.1` by design (no self-dependency).
- **Removed the redundant `/etc/hosts` band-aids** (`auth`/`mcp`/`secrets``192.168.8.175`) on 101, 103, 104, 105, 114, 118, 120, 121, 122, 126; disabled `hubris-hosts-override.service` where enabled. Backups at `/etc/hosts.bak-dnsplan`. PVE-managed lines and self-hostname maps preserved. Technitium already returns identical-or-better answers (verified: `auth → 82.165.190.79`, `mcp`/`secrets`/`cloud`/`git``192.168.8.175`).
- **Nextcloud (114):** its documented Guzzle-workaround dnsmasq is not running; resolves correctly straight from Technitium, so no special-casing remains.
- Net effect: **NetBird's DNS now only matters for off-LAN roaming peers** (Tier 2). On-prem (LXCs + household via Fritz!Box→Technitium) is fully NetBird-independent — so dropping the managed zone later can no longer break on-LAN resolution. Phases 24 (mesh-IP forwarding, roaming test, managed-zone + sync deletion) still pending.
### 2026-06-17 — Fritz!Box DNSv4 server set to Technitium; old limitation resolved ### 2026-06-17 — Fritz!Box DNSv4 server set to Technitium; old limitation resolved
Household LAN clients (192.168.178.x) now resolve `*.hubris.network` to LAN IPs — the limitation noted below is resolved. Configured at Fritz!Box Internet → Filter → DNS Server → DNSv4 Server = `192.168.8.2` (User-defined). Household LAN clients (192.168.178.x) now resolve `*.hubris.network` to LAN IPs — the limitation noted below is resolved. Configured at Fritz!Box Internet → Filter → DNS Server → DNSv4 Server = `192.168.8.2` (User-defined).

View File

@@ -64,11 +64,9 @@ The MCP server and secrets-issuance each have their own clone
- Both services bind `0.0.0.0:<port>`. The trust boundary is - Both services bind `0.0.0.0:<port>`. The trust boundary is
`MESH_SUBNETS` in the service's environment + nftables (planned). Today `MESH_SUBNETS` in the service's environment + nftables (planned). Today
`MESH_SUBNETS=100.122.0.0/16,192.168.8.0/24` — Netbird + the homelab LAN. `MESH_SUBNETS=100.122.0.0/16,100.64.0.0/10,192.168.8.0/24` — Netbird +
(The legacy Tailscale CGNAT range `100.64.0.0/10` was dropped 2026-06-21 Tailscale + the homelab LAN. Adjust if the LAN ever has untrusted
when Tailscale was decommissioned; secrets-issuance restarted. Only devices.
secrets-issuance reads `MESH_SUBNETS` — homelab-mcp does not.) Adjust if
the LAN ever has untrusted devices.
- Caddy fronts both with Let's Encrypt certs via the IONOS DNS challenge: - Caddy fronts both with Let's Encrypt certs via the IONOS DNS challenge:
`mcp.hubris.network``192.168.8.205:9810`, `mcp.hubris.network``192.168.8.205:9810`,
`secrets.hubris.network``192.168.8.205:9820`. Off-LAN clients on `secrets.hubris.network``192.168.8.205:9820`. Off-LAN clients on
@@ -127,7 +125,7 @@ The MCP server and secrets-issuance each have their own clone
step-by-step for adding a new client step-by-step for adding a new client
- [Auto-deploy](auto-deploy.md) — the `homelab-mcp` + `secrets-issuance` - [Auto-deploy](auto-deploy.md) — the `homelab-mcp` + `secrets-issuance`
pipelines (and the rest of the lab's webhook pipelines) pipelines (and the rest of the lab's webhook pipelines)
- [Mesh](mesh.md) — Netbird paths and the `192.168.8.0/24` - [Mesh](mesh.md) — Netbird / Tailscale paths and the `192.168.8.0/24`
network resource network resource
- [Apps (105)](../containers/105-apps.md) — where both services run - [Apps (105)](../containers/105-apps.md) — where both services run
- [Gitea (104)](../containers/104-gitea.md) — the source of truth - [Gitea (104)](../containers/104-gitea.md) — the source of truth

View File

@@ -42,6 +42,8 @@ LAN clients resolve via the [Technitium DNS on dns (107)](dns.md) → `192.168.8
| ------------------------------ | -------------------------------- | -------------------------------- | -------------------------------------------- | ------------------------------------------ | | ------------------------------ | -------------------------------- | -------------------------------- | -------------------------------------------- | ------------------------------------------ |
| `artifacto.hubris.network` | `/p/*`, `/static/*`, `/healthz` | `192.168.8.205:3100` | `artifacto-strip-sso` + `artifacto-ratelimit` (50 rps / 100 burst) | `fullchain.crt` / `privkey.key` | | `artifacto.hubris.network` | `/p/*`, `/static/*`, `/healthz` | `192.168.8.205:3100` | `artifacto-strip-sso` + `artifacto-ratelimit` (50 rps / 100 burst) | `fullchain.crt` / `privkey.key` |
| `blog.hubris.network` | whole host | `192.168.8.205:8080` | `blog-ratelimit` (100 rps / 200 burst) | `blog.fullchain.crt` / `blog.privkey.key` | | `blog.hubris.network` | whole host | `192.168.8.205:8080` | `blog-ratelimit` (100 rps / 200 burst) | `blog.fullchain.crt` / `blog.privkey.key` |
| `trmnl.hubris.network` | whole host | `192.168.8.211:9851` ([trmnl 128](../containers/128-trmnl.md)) | `trmnl-ratelimit` (20 rps / 40 burst) | `trmnl.fullchain.crt` / `trmnl.privkey.key` |
| `house.hubris.network` | whole host | `192.168.8.212:3000` ([house 129](../containers/129-house.md)) | `house-ratelimit` (30 rps / 60 burst) | `house.fullchain.crt` / `house.privkey.key` |
`artifacto-strip-sso` blanks inbound `X-Authentik-*` and `X-Artifacto-Gateway` so external clients can't spoof the SSO auto-login header contract. Path split is enforced at the VPS router rule, not by home Caddy. See [Artifacto on apps (105)](../containers/105-apps.md). `artifacto-strip-sso` blanks inbound `X-Authentik-*` and `X-Artifacto-Gateway` so external clients can't spoof the SSO auto-login header contract. Path split is enforced at the VPS router rule, not by home Caddy. See [Artifacto on apps (105)](../containers/105-apps.md).
@@ -85,6 +87,9 @@ No cert-mirror entry and no `hubris-public-cert-sync.sh` mapping is needed for `
## Changelog ## Changelog
### 2026-06-24 — `trmnl.hubris.network` exposed
TRMNL plugins middleware on [trmnl (128)](../containers/128-trmnl.md). File-provider router `trmnl-public` → `192.168.8.211:9851`, `trmnl-ratelimit` (20 rps / 40 burst), cert mirrored as `trmnl.fullchain.crt`/`trmnl.privkey.key`. Verified live from the internet (200 with token / 401 without). It was provisioned during a mesh outage — the `home-lab-network` (192.168.8.0/24) route had no active routing peer because the **mac-mini routing peer's netbird was down** (all home-backed public services 504'd). Bringing netbird up on mac-mini restored the route; no traefik change was needed.
### 2026-05-31 — `auth.hubris.network` now served locally on the VPS ### 2026-05-31 — `auth.hubris.network` now served locally on the VPS
Authentik migrated onto the VPS ([investigation](../investigations/2026-05-31-authentik-vps-migration.md)). Unlike the home-backed services above, `auth` is a local container routed via traefik Docker-provider labels with traefik-managed Let's Encrypt — no cert-mirror, no `traefik-dynamic.yaml` router. Admin UI gated by an ipAllowList middleware. Traefik gained a second Docker network (`auth`, `172.30.1.0/24`) to reach it while keeping its DB/Redis isolated from the netbird stack. Authentik migrated onto the VPS ([investigation](../investigations/2026-05-31-authentik-vps-migration.md)). Unlike the home-backed services above, `auth` is a local container routed via traefik Docker-provider labels with traefik-managed Let's Encrypt — no cert-mirror, no `traefik-dynamic.yaml` router. Admin UI gated by an ipAllowList middleware. Traefik gained a second Docker network (`auth`, `172.30.1.0/24`) to reach it while keeping its DB/Redis isolated from the netbird stack.

View File

@@ -37,7 +37,7 @@ Every LXC that mounts `/mnt/library` participates in a shared `media` group with
- `/etc/subgid` has `root:100000:65536` AND `root:10000:1` (second line required for unprivileged LXCs to receive GID 10000). - `/etc/subgid` has `root:100000:65536` AND `root:10000:1` (second line required for unprivileged LXCs to receive GID 10000).
- Shared subtrees owned `:media` mode `2775` (drwxrwsr-x, setgid): - Shared subtrees owned `:media` mode `2775` (drwxrwsr-x, setgid):
- `movies`, `tv`, `music`, `anime`, `podcasts` — jellyfin libraries - `movies`, `tv`, `music`, `anime`, `podcasts` — jellyfin libraries
- `audiobooks`, `audiobookshelf-metadata`, `books`, `comics` — audiobookshelf / booklore - `audiobooks`, `audiobookshelf-metadata`, `books`, `comics` — audiobookshelf / grimmory
- `downloads` — \*arr stack output - `downloads` — \*arr stack output
- `images` — photoprism / immich / mulita - `images` — photoprism / immich / mulita
- `roms` — emu frontends - `roms` — emu frontends
@@ -61,6 +61,7 @@ Every LXC that mounts `/mnt/library` participates in a shared `media` group with
| 119 | [sophia](../containers/119-sophia.md) | priv | www-data | | 119 | [sophia](../containers/119-sophia.md) | priv | www-data |
| 120 | [mule-images](../containers/120-mule-images.md) | priv | www-data | | 120 | [mule-images](../containers/120-mule-images.md) | priv | www-data |
| 122 | [arriman](../containers/122-arriman.md) | priv | www-data, audiobookshelf, radarr, sonarr, lidarr, prowlarr, qbittorrent, bazarr, jellyseerr, mylar, jackett, overseerr, plex, arr | | 122 | [arriman](../containers/122-arriman.md) | priv | www-data, audiobookshelf, radarr, sonarr, lidarr, prowlarr, qbittorrent, bazarr, jellyseerr, mylar, jackett, overseerr, plex, arr |
| 130 | [grimmory](../containers/130-grimmory.md) | priv | Docker container uses `GROUP_ID=10000` env var (linuxserver pattern) — no in-LXC group needed |
> Some entries from earlier snapshots — 100 (arr-yunohost), 107 (marimo), 109 (syncthing), 110 (photoprism), 112 (immich), 116 (heaper) — referenced LXCs that have since been destroyed. See [containers/index](../containers/index.md#recently-destroyed-kept-for-archaeology). > Some entries from earlier snapshots — 100 (arr-yunohost), 107 (marimo), 109 (syncthing), 110 (photoprism), 112 (immich), 116 (heaper) — referenced LXCs that have since been destroyed. See [containers/index](../containers/index.md#recently-destroyed-kept-for-archaeology).
@@ -68,7 +69,7 @@ Config backups: `/root/101.conf.bak.*`, `/root/109.conf.bak.*` (109 destroyed 20
## Gotchas ## Gotchas
- **[apps (105)](../containers/105-apps.md) is a Docker host.** Adding `media` to the LXC alone is *not* enough for Docker containers inside. Each Docker container needs its GID passed in explicitly: `--group-add 10000` or `user: "<uid>:10000"` in compose. Booklore, audiobookshelf-in-docker, etc. need this per-container. - **[apps (105)](../containers/105-apps.md) and [grimmory (130)](../containers/130-grimmory.md) are Docker hosts.** Adding `media` to the LXC alone is *not* enough for Docker containers inside. Each Docker container needs its GID passed in explicitly: `--group-add 10000`, `user: "<uid>:10000"`, or `GROUP_ID=10000` (linuxserver images) in compose. Grimmory, audiobookshelf-in-docker, etc. need this per-container.
- **`pct exec` does NOT run initgroups.** So `pct exec <id> -- id` shows only the primary group. For interactive verification, use `pct exec <id> -- sudo -i -u root id` or `su - <user> -c id`. Real systemd services work fine. - **`pct exec` does NOT run initgroups.** So `pct exec <id> -- id` shows only the primary group. For interactive verification, use `pct exec <id> -- sudo -i -u root id` or `su - <user> -c id`. Real systemd services work fine.
- **systemd `User=root`** skips initgroups — explicit `SupplementaryGroups=media` drop-in needed. - **systemd `User=root`** skips initgroups — explicit `SupplementaryGroups=media` drop-in needed.
- **`pct restore`** or template rebuilds wipe in-container group membership and unprivileged-LXC idmap blocks. Re-apply from this page. - **`pct restore`** or template rebuilds wipe in-container group membership and unprivileged-LXC idmap blocks. Re-apply from this page.

View File

@@ -1,12 +1,12 @@
# Mesh — Netbird # Mesh — Tailscale → Netbird migration
The hubris fleet runs on Netbird. Tailscale — the previous overlay — was **fully decommissioned on 2026-06-21**: removed from the 6 LXCs that still ran it (101, 103, 104, 105, 114, 119), apt package + state purged, `tailscaled` disabled. The fleet is now Netbird-only. (Historical migration notes below are kept for context.) The hubris fleet is migrating from Tailscale to Netbird. Netbird is the target end-state. In-progress as of 2026-04-21.
## Current state ## Current state
- **PVE host** uses Netbird (`wt0`, `100.122.38.109/16`). Its resolver is the local netbird daemon, which forwards to LAN/upstream — so the PVE host gets `*.hubris.network → 192.168.8.175` via the system resolver chain. - **PVE host** uses Netbird (`wt0`, `100.122.38.109/16`). Its resolver is the local netbird daemon, which forwards to LAN/upstream — so the PVE host gets `*.hubris.network → 192.168.8.175` via the system resolver chain.
- **Netbird mgmt host** (`82.165.190.79`, FQDN `inspiring-ramanujan.netbird.selfhosted`, NB IP `100.122.165.149`) is now itself a peer on the mesh (joined 2026-04-22 via setup key, netbird 0.69.0). Routes the homelab network (`192.168.8.0/24`) via the PVE peer. This gives the mgmt host LAN access *and* split-horizon DNS for `*.hubris.network`. Useful independently of any Authentik integration. - **Netbird mgmt host** (`82.165.190.79`, FQDN `inspiring-ramanujan.netbird.selfhosted`, NB IP `100.122.165.149`) is now itself a peer on the mesh (joined 2026-04-22 via setup key, netbird 0.69.0). Routes the homelab network (`192.168.8.0/24`) via the PVE peer. This gives the mgmt host LAN access *and* split-horizon DNS for `*.hubris.network`. Useful independently of any Authentik integration.
- **All LXCs** now resolve via Technitium (`192.168.8.2`) directly — as of the 2026-06-21 DNS single-source work (Phase 1). The previous mix of router DNS (`192.168.8.1`) / Tailscale MagicDNS (`100.100.100.100`) returned the *public* IONOS A record and is gone. See [dns.md changelog 2026-06-21](dns.md). - **Most LXCs** still run Tailscale or use router DNS (`192.168.8.1`) / Tailscale MagicDNS (`100.100.100.100`), both of which return the *public* IONOS A record `*.hubris.network → 82.165.190.79`. The VPS only routes hostnames it actually publishes (today, `artifacto` + `blog`), so this path is a dead end for any LAN-only service.
## ICE / STUN / TURN ## ICE / STUN / TURN
@@ -45,8 +45,6 @@ Pre-migration, the bundled `netbirdio/netbird-server` combined image silently ig
## Consequence — every LXC wired to Authentik needs an internal override ## Consequence — every LXC wired to Authentik needs an internal override
> **RESOLVED 2026-06-21 (DNS single-source, Phase 1).** Every homelab LXC now points its resolver directly at **Technitium (`192.168.8.2`)**, which answers the full split-horizon zone (`auth → 82.165.190.79`, everything else → Caddy `192.168.8.175`). The per-LXC `/etc/hosts` overrides and Tailscale-MagicDNS/dead-`.180`/router resolvers below were removed; `hubris-hosts-override.service` disabled where present. The section is kept for history. See [dns.md changelog 2026-06-21](dns.md).
Until each LXC is migrated to Netbird, anything that needs to reach `auth.hubris.network` (Authentik), `cloud.hubris.network` (Nextcloud), etc., must override the public answer with `192.168.8.175`. Until each LXC is migrated to Netbird, anything that needs to reach `auth.hubris.network` (Authentik), `cloud.hubris.network` (Nextcloud), etc., must override the public answer with `192.168.8.175`.
Two techniques. Pick by HTTP-client behavior. Two techniques. Pick by HTTP-client behavior.
@@ -92,7 +90,7 @@ pct set <id> --nameserver "127.0.0.1 192.168.8.1 1.1.1.1"
## Adding new LXCs ## Adding new LXCs
- Don't add new LXCs to Tailscale; add them to Netbird. Tailscale is being decommissioned on hubris. - Don't add new LXCs to Tailscale; add them to Netbird. Tailscale is being decommissioned on hubris.
- When wiring a new app into Authentik: `cat /etc/resolv.conf` on the target LXC. It should be `192.168.8.2` (Technitium), which returns correct split-horizon answers — no `/etc/hosts` override needed. (Historically, boxes on `192.168.8.1`/`100.100.100.100` needed an override; those resolvers were removed 2026-06-21.) - When wiring a new app into Authentik: `cat /etc/resolv.conf` on the target LXC. If nameserver is `192.168.8.1` or `100.100.100.100`, add the hosts override. If it's the netbird daemon IP, skip.
## Long-term fix ## Long-term fix

View File

@@ -64,14 +64,14 @@ No NAT on Proxmox — traffic flows without double-NAT.
## Remote access ## Remote access
- **NetBird mesh** — the remote-administration path. Authenticated via [Authentik on the VPS](../vps/). - **NetBird mesh** — primary path for remote administration. Authenticated via [Authentik on the VPS](../vps/).
- Tailscale (the previous overlay) was **decommissioned 2026-06-21**. See [mesh.md](mesh.md). - **Tailscale** — legacy, being phased out. See [mesh.md](mesh.md).
## Related ## Related
- [DNS](dns.md) — split-horizon config and entry list - [DNS](dns.md) — split-horizon config and entry list
- [Ingress](ingress.md) — public entry points via VPS traefik - [Ingress](ingress.md) — public entry points via VPS traefik
- [Mesh](mesh.md) — NetBird VPN overlay - [Mesh](mesh.md) — NetBird / Tailscale VPN overlay
- [hosts/hubris.md](../hosts/hubris.md) — Proxmox host (vmbr0/vmbr1 config) - [hosts/hubris.md](../hosts/hubris.md) — Proxmox host (vmbr0/vmbr1 config)
- [CT 107 — dns](../containers/107-dns.md) — Technitium DNS + DHCP server - [CT 107 — dns](../containers/107-dns.md) — Technitium DNS + DHCP server

View File

@@ -131,7 +131,6 @@ are managed by `ssh/deploy-keys.sh`. SSH user is `root`.
| 120 | mule-images | `192.168.8.136` | photo-management | | 120 | mule-images | `192.168.8.136` | photo-management |
| 121 | caddy | `192.168.8.175` | reverse-proxy | | 121 | caddy | `192.168.8.175` | reverse-proxy |
| 122 | arriman | `192.168.8.132` | arr-stack | | 122 | arriman | `192.168.8.132` | arr-stack |
| 126 | plato | `192.168.8.190` | app |
### Workstations ### Workstations

View File

@@ -11,8 +11,8 @@
# ("external" is reserved for hosts the homelab CLI manages via ssh but # ("external" is reserved for hosts the homelab CLI manages via ssh but
# that aren't homelab clients themselves — e.g. the IONOS netbird VPS # that aren't homelab clients themselves — e.g. the IONOS netbird VPS
# with no /etc/age/key.txt and no /opt/homelab-context clone.) # with no /etc/age/key.txt and no /opt/homelab-context clone.)
# - `mesh:` lists addresses the host is reachable at via `netbird`. # - `mesh:` lists addresses the host is reachable at. Both `netbird` and
# (Tailscale was decommissioned 2026-06-21 — see infrastructure/mesh.md.) # `tailscale` are accepted during the migration (see infrastructure/mesh.md).
# Prefer netbird FQDNs over raw IPs. # Prefer netbird FQDNs over raw IPs.
# - `age_pubkey:` provisioned by secrets-issuance on first bootstrap and # - `age_pubkey:` provisioned by secrets-issuance on first bootstrap and
# committed back via `homelab client add --finalize-pubkey <key>`. # committed back via `homelab client add --finalize-pubkey <key>`.
@@ -32,6 +32,7 @@ mesh:
primary: netbird primary: netbird
accepted: accepted:
- netbird - netbird
- tailscale
netbird_subnet: 100.122.0.0/16 netbird_subnet: 100.122.0.0/16
netbird_domain: netbird.selfhosted netbird_domain: netbird.selfhosted
services: services:
@@ -68,15 +69,16 @@ services:
photos: photos:
url: https://photos.hubris.network url: https://photos.hubris.network
backend: mule-images backend: mule-images
plato:
url: https://plato.hubris.network
backend: plato
arr_stack: arr_stack:
backend: arriman backend: arriman
note: jellyseerr / qbit / sab on docker compose note: jellyseerr / qbit / sab on docker compose
artifacto: artifacto:
backend: apps backend: apps
url: https://artifacto.hubris.network url: https://artifacto.hubris.network
trmnl:
backend: trmnl
url: https://trmnl.hubris.network
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
zimaos: zimaos:
url: https://zimaos.hubris.network url: https://zimaos.hubris.network
backend: zimaos backend: zimaos
@@ -116,6 +118,27 @@ hosts:
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
trmnl:
kind: lxc
pve_id: 128
host: hubris
os: linux
role: trmnl-middleware
lan_ip: 192.168.8.211
public_host: trmnl.hubris.network
# not yet mesh/SOPS-enrolled — see containers/128-trmnl.md
house:
kind: lxc
pve_id: 129
host: hubris
os: linux
role: family-planner
lan_ip: 192.168.8.212
public_host: house.hubris.network
notes:
- Docker host for Yuvomi (family planner). Created 2026-06-26.
- Runs Yuvomi container + WebDAV doc bridge to paperless
age_pubkey: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
jellyfin: jellyfin:
kind: lxc kind: lxc
pve_id: 101 pve_id: 101
@@ -124,6 +147,9 @@ hosts:
role: media-server role: media-server
lan_ip: 192.168.8.206 lan_ip: 192.168.8.206
public_host: media.hubris.network public_host: media.hubris.network
mesh:
tailscale:
fqdn: jellyfin
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
@@ -142,6 +168,9 @@ hosts:
role: document-archive role: document-archive
lan_ip: 192.168.8.130 lan_ip: 192.168.8.130
public_host: paperless.hubris.network public_host: paperless.hubris.network
mesh:
tailscale:
fqdn: paperless
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
@@ -154,6 +183,9 @@ hosts:
lan_ip: 192.168.8.121 lan_ip: 192.168.8.121
public_host: git.hubris.network public_host: git.hubris.network
backend_port: 3000 backend_port: 3000
mesh:
tailscale:
fqdn: gitea
mounts: mounts:
- /mnt/library - /mnt/library
notes: notes:
@@ -168,6 +200,10 @@ hosts:
lan_ip: 192.168.8.205 lan_ip: 192.168.8.205
public_hosts: public_hosts:
- artifacto.hubris.network - artifacto.hubris.network
mesh:
tailscale:
ip: 100.121.171.122
fqdn: apps
mounts: mounts:
- /mnt/library - /mnt/library
runs: runs:
@@ -175,7 +211,27 @@ hosts:
- plantuml - plantuml
- homelab-mcp - homelab-mcp
- secrets-issuance - secrets-issuance
# booklore removed 2026-06-29 → migrated to grimmory (LXC 130)
age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
auth-outpost:
kind: lxc
pve_id: 106
host: hubris
os: linux
role: authentik-gateway
lan_ip: 192.168.8.6
notes:
- Runs Authentik outpost (reverse-proxy/SSO enforcement) for protected services
dns:
kind: lxc
pve_id: 107
host: hubris
os: linux
role: dns-server
lan_ip: 192.168.8.2
notes:
- Technitium DNS, split-horizon zone for *.hubris.network
- Primary DNS for 192.168.8.0/24 LAN (inventory.services.dns references this)
nextcloud: nextcloud:
kind: lxc kind: lxc
pve_id: 114 pve_id: 114
@@ -184,6 +240,9 @@ hosts:
role: file-sync role: file-sync
lan_ip: 192.168.8.224 lan_ip: 192.168.8.224
public_host: cloud.hubris.network public_host: cloud.hubris.network
mesh:
tailscale:
fqdn: nextcloud
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
@@ -195,6 +254,9 @@ hosts:
role: matrix-server role: matrix-server
lan_ip: 192.168.8.239 lan_ip: 192.168.8.239
public_host: matrix.hubris.network public_host: matrix.hubris.network
mesh:
tailscale:
fqdn: elementsynapse
sophia: sophia:
kind: lxc kind: lxc
pve_id: 119 pve_id: 119
@@ -202,6 +264,9 @@ hosts:
os: linux os: linux
role: workshop role: workshop
lan_ip: 192.168.8.109 lan_ip: 192.168.8.109
mesh:
tailscale:
fqdn: sophia
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
@@ -213,6 +278,9 @@ hosts:
role: photo-management role: photo-management
lan_ip: 192.168.8.136 lan_ip: 192.168.8.136
public_host: photos.hubris.network public_host: photos.hubris.network
mesh:
tailscale:
fqdn: muleimage
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
@@ -240,21 +308,28 @@ hosts:
- jellyseerr.hubris.network - jellyseerr.hubris.network
- qbit.hubris.network - qbit.hubris.network
- sab.hubris.network - sab.hubris.network
mesh:
tailscale:
fqdn: arr
mounts: mounts:
- /mnt/library - /mnt/library
age_pubkey: '' age_pubkey: ''
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent grimmory:
plato:
kind: lxc kind: lxc
pve_id: 126 pve_id: 130
host: hubris host: hubris
os: linux os: linux
role: app role: book-library
lan_ip: 192.168.8.190 lan_ip: 192.168.8.213
public_host: plato.hubris.network public_host: books.hubris.network
mounts: mounts:
- /mnt/library/documents/plato - /mnt/library
age_pubkey: '' notes:
- Docker host for Grimmory (community fork of Booklore). Created 2026-06-29.
- Migrated from apps LXC 105; MariaDB data carried over (schema-compatible fork).
age_pubkey: age1uellsemnjrzgfg9fxw4jefpy05laxzggwnwhh6ny3wl7alyp6v8q0muxet
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent
# 126 (plato) — destroyed 2026-06-28, notes workspace decommissioned
zimaos: zimaos:
kind: vm kind: vm
pve_id: 100 pve_id: 100
@@ -270,6 +345,9 @@ hosts:
os: linux os: linux
role: home-automation role: home-automation
lan_ip: 192.168.8.101 lan_ip: 192.168.8.101
mesh:
tailscale:
fqdn: homeassistant
republic-laptop: republic-laptop:
kind: workstation kind: workstation
os: linux os: linux

View File

@@ -18,7 +18,7 @@ operational reference is here.
| --- | --- | --- | | --- | --- | --- |
| Hostname matches an entry in `inventory.yaml` | The bootstrap looks up `hosts/$(hostname).yaml`. | `hostname` (Linux) / `scutil --get LocalHostName` (macOS) | | Hostname matches an entry in `inventory.yaml` | The bootstrap looks up `hosts/$(hostname).yaml`. | `hostname` (Linux) / `scutil --get LocalHostName` (macOS) |
| OS is Linux or macOS | bootstrap detects via `uname -s` | `uname -s` | | OS is Linux or macOS | bootstrap detects via `uname -s` | `uname -s` |
| On the Netbird mesh **or** on the LAN | issuance is gated to mesh + LAN subnets. **For Netbird: use a setup-key, not interactive auth** — see "Getting onto Netbird" below. | `netbird status` | | On the mesh (Netbird or Tailscale) **or** on the LAN | issuance is gated to mesh + LAN subnets. **For Netbird: use a setup-key, not interactive auth** — see "Getting onto Netbird" below. | `netbird status` / `tailscale status` |
| `git`, `python3`, `python3-yaml`, `age`, `sops` | bootstrap preflight; `homelab` CLI imports yaml | See per-OS commands below | | `git`, `python3`, `python3-yaml`, `age`, `sops` | bootstrap preflight; `homelab` CLI imports yaml | See per-OS commands below |
| Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network/mcp` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) | | Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network/mcp` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) |

View File

@@ -0,0 +1,202 @@
# Runbook: Budget import from N26 CSV → Yuvomi
Distil a bank-export CSV into Yuvomi's Budget and Subscriptions modules using
the `yuvomi-mcp` tools. Run this whenever a new CSV period needs to be
summarised into targets and fixed costs.
---
## Prerequisites
- `yuvomi-mcp` is running on LXC 129 and connected as an MCP server in Claude.
- The CSV is an N26 export (columns: Booking Date, Value Date, Partner Name,
Partner Iban, Type, Payment Reference, Account Name, Amount (EUR), …).
- API token: `homelab secret yuvomi-api-token` (decrypts on any enrolled client).
- Direct API base: `https://house.hubris.network/api/v1`
---
## API quirks (Yuvomi ≤ 0.77.x)
- **Subscriptions live under `/budget/subscriptions`**, NOT `/subscriptions/`.
A top-level `/subscriptions` route returns 404.
- `GET /budget/subscriptions``{ data: { subscriptions: [...], summary: {...} } }`
- `GET /budget/subscriptions/meta``{ data: { categories: [...], payment_methods: [...] } }`
- `POST /budget/subscriptions` → create a subscription (name, amount, billing_cycle,
cycle_interval, next_payment_date, currency, category_id, payment_method_id required)
- `GET /budget/` (no month) → returns only **non-recurring** base entries.
Use `GET /budget/?month=YYYY-MM` to get all entries (recurring + one-time) for a month.
- `GET /budget/categories` → expense category keys + income category names (German keys
like `"Erwerbseinkommen"`, `"Sozialleistungen"`, `"Geschenke & Transfers"`).
- Budget entries: `amount` positive = income, negative = expense.
- Recurring entries: set `is_recurring: 1` + `recurrence_interval: "monthly"`.
The `date` field sets the start month.
- `recurrence_virtual: 1` smooths non-monthly amounts across all months in the summary
(e.g. 55.08 € quarterly → shows as ~18.36 €/month).
- Custom RRULE strings (`recurrence_rule`) are **not accepted** by the API — use
`cycle_interval` on the subscription instead, or `recurrence_interval` on budget entries.
---
## Subscription category IDs (as of 2026-06-26)
| id | name | budget_subcategory_key |
|---|---|---|
| 1 | Entertainment | subscription_entertainment |
| 2 | Productivity | subscription_productivity |
| 3 | Utilities | subscription_utilities |
| 4 | Health | subscription_health |
| 5 | Education | subscription_education |
| 6 | Other | subscription_other |
## Payment method IDs
| id | name |
|---|---|
| 1 | Credit Card |
| 2 | Debit Card |
| 3 | PayPal |
| 6 | Bank Transfer / SEPA |
| 7 | Other |
---
## Step 1 — Categorise the transactions
Skip these as internal/already-covered:
- Fixed costs you'll enter as **subscriptions** (Miete, SWM, SYNVIA, Hundefutter,
Netflix, Grover, Rundfunk ARD, KuKita, Lillydoo)
- Internal transfers (The Joy Pot ↔ Cookie, Hauptkonto, Tagesgeldkonto splits)
- Identified income (Cookie Share, Kindergeld, Pocket Money credits, Distributor)
- Fun Money pass-throughs (in and out same month → net zero)
**Variable expense taxonomy:**
| Category key | Subcategory key | Examples |
|---|---|---|
| `food` | `groceries` | E-Center, Knuspr, EDEKA, Tegut, VollCorner, Lidl, Netto, REWE, KoRo, Roast Market |
| `food` | `restaurants_bars` | Restaurants, Lieferando, Cafes, Zeit für Brot, Baobab, Höflinger |
| `personal_health` | `beauty_cosmetics` | DM, Rossmann |
| `personal_health` | `pharmacy` | Apotheke, MVZ Dermatologie |
| `transport` | `apps_taxi` | Uber, RYD GMBH, MVG, Handyparken |
| `shopping_clothing` | `gifts` | Children products: Schlummersack, Catchy Kids, SP EVERY., Dukal, Berger-Lernwelt |
| `shopping_clothing` | `clothes_shoes` | Zalando, Ernsting's, Schuhmair, Thalia, Vinted, Airbnb, Hotel at Booking.com |
| `shopping_clothing` | `electronics` | Amazon, AMZN Mktp DE |
| `housing` | `renovation_maintenance` | IKEA, Markus Festl, Granit, Sostrene Grene, Mol* tischdecken, Gaertnerei, Dehner |
| `education` | `courses_college` | Kathrin Orlob (PEKiP), Nerina Aupperle |
| `leisure` | `streaming` | WOW wowtv.de |
| `financial_other` | `bank_fees` | Unidentified PayPal, Ratepay, N26 fees |
| `Geschenke & Transfers` | *(income)* | One-off incoming transfers |
---
## Step 2 — Create subscriptions
```
get_subscriptions_meta() ← get category_id and payment_method_id
```
**Standard Cookie household subscriptions (as of 2026-07):**
| Name | Amount | billing_cycle | cycle_interval | category_id | payment_method_id |
|---|---|---|---|---|---|
| Miete | 1080.00 | monthly | 1 | 6 (Other) | 6 (Bank Transfer) |
| Strom (SWM) | 79.00 | monthly | 1 | 3 (Utilities) | 6 |
| Internet / TV / Telefon | 29.99 | monthly | 1 | 3 (Utilities) | 6 |
| Hundefutter | 75.00 | monthly | 1 | 6 (Other) | 6 |
| Netflix | 8.00 | monthly | 1 | 1 (Entertainment) | 6 |
| Grover | 16.90 | monthly | 1 | 6 (Other) | 2 (Debit Card) |
| Rundfunk ARD / ZDF | 55.08 | monthly | 3 | 1 (Entertainment) | 6 |
| KuKita Daycare (Leon) | 503.00 | monthly | 1 | 5 (Education) | 6 |
| Lillydoo diapers | 56.70 | monthly | 2 | 4 (Health) | 3 (PayPal) |
Monthly equivalent total: **1,838.60 €** (Yuvomi applies cycle_interval to prorate).
---
## Step 3 — Add recurring income entries
```
stage_add_budget_entry(
title="Kindergeld",
amount=55.00,
category="Sozialleistungen",
date="YYYY-MM-01",
is_recurring=True,
recurrence_interval="monthly",
)
commit_pending(pending_id)
```
**Standard recurring income:**
| Title | Amount | category |
|---|---|---|
| Kindergeld | +55.00 | Sozialleistungen |
| Cookie Share | +2650.00 | Erwerbseinkommen *(see recommended amount below)* |
---
## Step 4 — Post variable transactions
For each non-skipped CSV row, call `stage_add_budget_entry` with the mapped
category/subcategory and the actual transaction amount and date. Use the Partner
Name + Payment Reference as the title (truncate to 100 chars).
---
## Step 5 — Verify
```
get_budget_summary("YYYY-MM")
list_subscriptions()
```
Expected for a full month with KuKita:
- Fixed expenses ≥ 1,838 € (subscriptions)
- Variable expenses ≥ 500 € (groceries alone)
---
## Cookie Share: how much to transfer monthly
Calculated from JanJun 2026 data (Cookie account, one-offs stripped):
| | €/month |
|---|---|
| **Fixed costs (subscriptions)** | **1,839** |
| Miete | 1,080 |
| KuKita *(permanent from Jul 2026)* | 503 |
| Strom + SYNVIA + Rundfunk + Netflix + Grover + Hundefutter + Lillydoo | 256 |
| **Variable (6-month averages)** | **1,032** |
| Groceries | 595 |
| Children products | 142 |
| Dining & cafes | 100 |
| Transport | 66 |
| Drugstore | 52 |
| Clothing, Amazon, Pharmacy | 77 |
| **Total monthly spend** | **≈ 2,871** |
| Minus Kindergeld (fixed income) | 55 |
| Minus Pocket Money (conservative ~600 €) | 600 |
| **→ Recommended Cookie Share** | **≈ 2,650 €** |
| With 200 € buffer | **≈ 2,850 €** |
**Current Cookie Share (Jun 2026): 1,995 € — shortfall ~655 €.**
The gap was covered by irregular Pocket Money top-ups (avg 962 €/mo over 6 months, but
highly variable: 121 €3,000 €). KuKita starting in June is the biggest step-up; raising
Cookie Share to **2,650 €** makes the budget self-sufficient without relying on top-ups.
---
## Changelog
### 2026-06-29 — Corrections from first real import
- Subscriptions endpoint is `/budget/subscriptions`, NOT `/subscriptions/` (404).
- `recurrence_rule` RRULE strings are rejected by the API; use `cycle_interval` instead.
- `GET /budget/` (no filter) returns only non-recurring entries; use `?month=` for full view.
- Added Cookie Share recommendation (2,650 €/month) based on 6-month expense analysis.
- Added full category taxonomy table.
### 2026-06-29 — Initial runbook
Created from JanJun 2026 N26 Cookie account analysis.

View File

@@ -0,0 +1,107 @@
# 2026-06-24 — TRMNL plugins LXC (128) + middleware deploy pipeline
## Goal
Stand up a dedicated LXC to host self-hosted **middleware for TRMNL e-ink plugins**.
TRMNL cloud polls `https://trmnl.hubris.network/<plugin>/dashboard` every 15 min; the
middleware fetches/shapes live data and returns JSON that TRMNL merges into the plugin's
Liquid template. First consumer: the Munich Home Dashboard (`/munich-home/dashboard`).
One LXC + one FastAPI service hosts all current and future plugins (router per plugin).
Source repo: gitea `dtoro/terminalito` (app code). This repo only documents the fabric
wiring, same split as Artifacto/Plato.
## Current state
- No TRMNL middleware in the lab. Highest LXC id is 127 (see `containers/index.md`).
- Public hostnames terminate at the [VPS netbird traefik](../hosts/netbird-vps.md) → netbird
mesh → [caddy (121)](../containers/121-caddy.md) → backend LXC. Cert obtained by Caddy
(IONOS DNS-01) and mirrored to the VPS by the daily cert-sync timer on the host.
- Auto-deploy pipelines are gitea-webhook driven, two shapes (see [auto-deploy](../infrastructure/auto-deploy.md)).
## Target state
```
TRMNL cloud --GET 15m, Bearer token--> https://trmnl.hubris.network/munich-home/dashboard
VPS traefik (public TLS) --netbird--> caddy (121) --> trmnl (128) :9851 trmnl-plugins.service
├ Open-Meteo (weather)
├ MVG departures (transit)
└ Google Calendar (OAuth, SOPS)
```
- **LXC 128 `trmnl`**: Debian, unprivileged, ~1 core / 512 MiB1 GiB / 8 GiB rootfs. No mounts.
- **Service** `trmnl-plugins.service``uvicorn server.app:app --host 0.0.0.0 --port 9851`,
`EnvironmentFile=/etc/trmnl-plugins/env`. Auth: every path except `/health` requires
`Authorization: Bearer $TRMNL_POLL_TOKEN`.
- **Auto-deploy** (Shape B): `/opt/terminalito` working tree, sibling `/opt/terminalito-deploy/`.
- Public hostname `trmnl.hubris.network`.
## Pre-flight checklist
- [ ] Confirm next free LXC id is 128 (`homelab list`, `containers/index.md`).
- [ ] Decide IP on `192.168.8.0/16` LAN (e.g. `192.168.8.211`) — pick a free one.
- [ ] Have Google OAuth client + refresh token, MVG stop globalIds, and a generated
`trmnl_poll_token` ready for the secret (see `dtoro/terminalito` README).
## Step-by-step procedure
1. **Provision + enroll**
```bash
# create LXC 128 trmnl on hubris (Debian), then enroll it:
homelab client add trmnl # joins netbird, provisions /etc/age/key.txt, edits inventory.yaml
homelab client add --finalize-pubkey <age_pubkey> # commits the age pubkey
ssh trmnl 'apt-get install -y python3-venv git'
```
2. **Secret** (`trmnl-oauth`): create `secrets/trmnl-oauth.yaml` with
`google_client_id/secret/refresh_token`, MVG stop ids, and `trmnl_poll_token`; add a
`path_regex` rule in `.sops.yaml` granting **trmnl**'s age pubkey; `sops updatekeys`.
The service reads it at deploy time via `homelab secret trmnl-oauth` → `/etc/trmnl-plugins/env`.
3. **App + service** on LXC 128 (clone uses the **internal** gitea URL — `git.hubris.network`
resets from inside LXCs):
```bash
git clone http://192.168.8.121:3000/dtoro/terminalito.git /opt/terminalito
python3 -m venv /opt/terminalito/server/.venv
/opt/terminalito/server/.venv/bin/pip install -r /opt/terminalito/server/requirements.txt
# install /etc/systemd/system/trmnl-plugins.service, enable --now
```
4. **Deploy pipeline** (Shape B, mirrors homelab-mcp): create `/opt/terminalito-deploy/`
`{webhook.py,deploy.sh}` (HMAC vs `/etc/terminalito-deploy/secret`, filter `refs/heads/main`,
`git pull` → `pip install -r server/requirements.txt` → rebuild env from `homelab secret` →
`systemctl restart trmnl-plugins`). Receiver `:9797`. Git creds at
`/etc/terminalito-deploy/git-credentials` (mode 600). Register a gitea webhook on
`dtoro/terminalito`; add `192.168.8.<128-ip>` to gitea `app.ini` `ALLOWED_HOST_LIST`.
5. **DNS**: add `trmnl.hubris.network` A → `192.168.8.175` (caddy) on [Technitium (107)](../containers/107-dns.md).
6. **Caddy** (`dtoro/caddy-conf`, commit+push auto-deploys):
```
trmnl.hubris.network { reverse_proxy 192.168.8.<128-ip>:9851 }
```
7. **Public exposure** on the [VPS](../hosts/netbird-vps.md): add traefik router+service for
`Host(\`trmnl.hubris.network\`)` → `http://192.168.8.<128-ip>:9851`; add the host to the
cert-sync map so the LE cert mirrors over.
8. **TRMNL cloud**: create a Polling private plugin, URL `…/munich-home/dashboard`, header
`Authorization: Bearer <trmnl_poll_token>`, refresh 15 min; paste `full.liquid`; add to a playlist.
## Verification
- `ssh trmnl systemctl is-active trmnl-plugins` → `active`; `curl -s localhost:9851/health` → `ok`.
- LAN: `curl -s -H "Authorization: Bearer <tok>" https://trmnl.hubris.network/munich-home/dashboard`
→ 200 JSON; without the header → 401; `/health` → 200.
- Public: same curl from off-mesh resolves via VPS, 200.
- Push a no-op commit → `/opt/terminalito-deploy` logs show pull+restart; webhook 202.
- `homelab mcp get_host trmnl` and `search_docs trmnl` agree with `containers/128-trmnl.md`.
## Post-migration
When executed, write changelog entries (same date) on:
`containers/128-trmnl.md` (new page), `containers/index.md` (row), `inventory.yaml`
(`services.trmnl`), `infrastructure/auto-deploy.md` (pipeline row + `ALLOWED_HOST_LIST` +
changelog), `containers/104-gitea.md` (webhook + allowed host), `containers/121-caddy.md`
(new proxied host), `containers/107-dns.md` (A record), `hosts/netbird-vps.md` (public route +
cert-sync). Then set this plan's status to `Done` in `plans/index.md`.

View File

@@ -0,0 +1,443 @@
# Yuvomi deployment — `house.hubris.network`
Deploy [Yuvomi](https://yuvomi.cloud/) (previously Oikos) — a self-hosted
family planner with 14 modules (calendar, tasks, meals, groceries, budget,
documents, notes, etc). Single Docker container (Express.js + SQLCipher
SQLite), 256 MB RAM min.
**Target hostname:** `house.hubris.network` — publicly reachable via VPS
traefik, LAN reachable via Caddy.
**Integrations:**
- Authentik SSO (OIDC)
- Google Calendar (tokens exist on trmnl LXC 128)
- Paperless (Yuvomi's Documents module / clarification needed — see Phase 4)
---
## Phase 0 — Clarifications needed
### 0.1 Paperless connection
Yuvomi's "Documents" module stores documents inside its encrypted SQLite DB or
optionally on WebDAV. There is **no direct Paperless-ngx API connector** in
Yuvomi. Options:
a) **Keep as-is** — Yuvomi's docs are separate from Paperless, no integration
b) **WebDAV bridge** — Mount Paperless's consumption dir as WebDAV, point
Yuvomi doc storage there (Yuvomi stores newly uploaded docs directly in the
Paperless consume folder)
c) **Custom module** — Write a Yuvomi module that fetches from Paperless API
Decision needed before Phase 3 config.
**Decision:** WebDAV bridge (Phase 6.2).
### 0.2 Deployment target
Two options:
| Option | Pros | Cons |
|--------|------|------|
| **apps LXC (105)** — Docker already there, 4GB RAM, 2 cores | Zero provisioning, existing compose pattern | Shared with artifacto, MCP, secrets-issuance; Portainer-managed stacks can be tricky |
| **New LXC (~129)** — dedicated, clean | Isolated, no side-effects | Need to create, install Docker, wire into everything |
**Decision:** New LXC (129).
---
## Phase 1 — Provision new LXC (129) for Yuvomi
### 1.1 Create the LXC on hubris
```
ssh root@192.168.8.77 << 'EOF'
# Check available templates
pveam list local | grep debian
# Create unprivileged Debian 13 LXC (follows trmnl's unpriv pattern)
pct create 129 local:vztmpl/debian-13-standard_13.7-1_amd64.tar.zst \
--hostname house \
--description "Yuvomi family planner — house.hubris.network" \
--cores 1 \
--memory 1024 \
--swap 512 \
--rootfs local:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp,type=veth \
--unprivileged 1 \
--features nesting=1 \
--onboot 1 \
--start 1
EOF
```
Resources: 1 core / 1 GiB RAM / 8 GiB rootfs (generous for a single Express.js
container; can downsize later).
### 1.2 Set static IP and install Docker
After the LXC boots, find its DHCP lease, then set a static IP:
```
# Find actual IP
ssh root@192.168.8.77 'lxc-attach 129 -- ip addr show eth0 | grep inet'
# Reserve 192.168.8.212 (or whatever is free) via Technitium DHCP,
# or set static IP in PVE config:
ssh root@192.168.8.77 'pct set 129 --net0 name=eth0,bridge=vmbr0,ip=192.168.8.212/24,gw=192.168.8.1,type=veth'
ssh root@192.168.8.77 'lxc-attach 129 -- reboot'
```
### 1.3 Install Docker inside the LXC
```
ssh root@192.168.8.77 << 'DOCKER'
lxc-attach 129 -- bash -c '
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
docker --version
docker compose version
'
DOCKER
```
### 1.4 Download Yuvomi and start
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
mkdir -p /opt/yuvomi /opt/yuvomi/data /opt/yuvomi/backups /opt/yuvomi/modules
cd /opt/yuvomi
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ulsklyc/yuvomi/main/.env.example
cp .env.example .env
"'
```
### 1.5 Generate keys and configure .env
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "
SESSION_SECRET=\$(openssl rand -hex 32)
DB_KEY=\$(openssl rand -hex 32)
cd /opt/yuvomi
sed -i \"s/SESSION_SECRET=.*/SESSION_SECRET=\$SESSION_SECRET/\" .env
sed -i \"s/DB_ENCRYPTION_KEY=.*/DB_ENCRYPTION_KEY=\$DB_KEY/\" .env
sed -i \"s/OIKOS_HTTP_PORT=3000/OIKOS_HTTP_PORT=3000/\" .env
sed -i \"s/# TZ=.*/TZ=Europe\\/Berlin/\" .env
echo \"SESSION_SECURE=true\" >> .env
echo \"TRUST_PROXY=1\" >> .env
echo \"BASE_URL=https://house.hubris.network\" >> .env
"'
```
### 1.6 Start Yuvomi
```
ssh root@192.168.8.77 'lxc-attach 129 -- bash -c "cd /opt/yuvomi && docker compose up -d"'
```
### 1.7 Verify
```
ssh root@192.168.8.77 'lxc-attach 129 -- curl -s http://127.0.0.1:3000/health'
# Expected: 200 OK
---
## Phase 2
### 2.1 Caddy — add `house.hubris.network`
Edit `/etc/caddy/Caddyfile` on LXC 121 (via `dtoro/caddy-conf` repo):
```
house.hubris.network {
tls {
dns ionos
}
reverse_proxy 192.168.8.212:3000
}
```
- Commit to `dtoro/caddy-conf` → auto-deploy via webhook
- If not yet deployed, push manually: `cd /etc/caddy && git add Caddyfile && git commit -m 'add house.hubris.network → yuvomi' && git push`
### 2.2 Verify LAN access
```
curl -sI https://house.hubris.network/
# Expected: 200 or 302 (redirect to /login or the setup wizard)
```
### 2.3 DNS — add Technitium record
Add A record `house.hubris.network → 192.168.8.175` (Caddy) on DNS LXC (107).
If using the DNS web UI: http://192.168.8.2/ → Zones → hubris.network → Add A record.
### 2.4 DNS mesh sync
If mesh DNS (Netbird managed zone) is in use, add the same record there or
verify dns-sync picks it up.
---
## Phase 3 — Public exposure (VPS traefik)
### 3.1 Add cert sync entry
On hubris (PVE host), edit `/usr/local/bin/hubris-public-cert-sync.sh`, add:
```bash
[house.hubris.network]="house.fullchain.crt house.privkey.key"
```
Run once:
```
systemctl start hubris-public-cert-sync.service
```
Verify certs landed on VPS:
```
ssh root@100.122.165.149 "ls -la /var/lib/docker/volumes/opt_netbird_traefik_letsencrypt/_data/house.*"
```
### 3.2 Add traefik router
On the VPS, edit `/opt/traefik-dynamic.yaml`:
```yaml
http:
routers:
house-public:
rule: 'Host(`house.hubris.network`)'
entryPoints:
- websecure
priority: 10
tls: {}
middlewares:
- house-ratelimit
service: house-public
middlewares:
house-ratelimit:
rateLimit:
average: 30
period: 1s
burst: 60
services:
house-public:
loadBalancer:
servers:
- url: 'http://192.168.8.212:3000'
tls:
certificates:
- certFile: /letsencrypt/house.fullchain.crt
keyFile: /letsencrypt/house.privkey.key
```
Restart traefik:
```
docker restart netbird-traefik
```
### 3.3 Verify public access
From outside the homelab LAN (or with `--resolve`):
```
curl -sI --resolve house.hubris.network:443:82.165.190.79 https://house.hubris.network/
# Expected: 200 or 302
echo | openssl s_client -connect 82.165.190.79:443 -servername house.hubris.network 2>&1 | openssl x509 -noout -subject
# Expected: CN=house.hubris.network (not TRAEFIK DEFAULT CERT)
```
---
## Phase 4 — Authentik SSO (OIDC)
### 4.1 Create OIDC provider in Authentik
Via VPS admin UI (`https://auth.hubris.network/if/admin/`):
- Applications → Providers → Create → OAuth2/OpenID Provider
- Name: `yuvomi`
- Client ID: auto-generated
- Client Secret: auto-generated (save this)
- Redirect URIs: `https://house.hubris.network/oauth2/callback`
- Signing Key: auto-generated
- Subject Mode: Based on User ID (or Based on Username — pick what Yuvomi expects)
### 4.2 Create application in Authentik
- Applications → Applications → Create
- Name: `Yuvomi`
- Slug: `yuvomi`
- Provider: select the one created above
- Launch URL: `https://house.hubris.network`
### 4.3 Set env vars in Yuvomi `.env`
On apps LXC (105), edit `/opt/yuvomi/.env`:
```
OIDC_ISSUER=https://auth.hubris.network/application/o/yuvomi/
OIDC_CLIENT_ID=<from Authentik>
OIDC_CLIENT_SECRET=<from Authentik>
# OIDC_TRUST_EMAIL_WITHOUT_VERIFIED_CLAIM=true # if Authentik doesn't send email_verified
```
### 4.4 Restart Yuvomi
```
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'
```
### 4.5 Verify SSO flow
Open `https://house.hubris.network/` — should redirect to Authentik login,
then back to Yuvomi.
---
## Phase 5 — Google Calendar
### 5.1 Extract tokens from trmnl LXC
On trmnl (LXC 128), the env file at `/etc/trmnl-plugins/env` contains:
```
GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REFRESH_TOKEN=1//03CS0rkuf7XVQCgYIARAAGAMSNwF-L9Irr5_b4kLhkx-dtf9EGQ1eJ1OnvxkaV_P1_4TDPwUN2lFb7nsbrZklN7qbjpkHpQyYlBY
```
### 5.2 Add Google Account redirect URI
In the Google Cloud Console (OAuth 2.0 Client IDs), add:
```
https://house.hubris.network/auth/google/callback
```
to the authorized redirect URIs for the existing client ID.
### 5.3 Set env vars in Yuvomi `.env`
```
GOOGLE_CLIENT_ID=119823214387-32f20ed3imesiv7uh5si7p3rou9fros4.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
```
Note: Yuvomi's Google Calendar integration uses the OAuth flow to get its own
refresh token — it doesn't reuse the trmnl refresh token. The first-time setup
in Yuvomi Settings → Calendar → Google Calendar will prompt for authorization.
### 5.4 Restart and verify
```
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose restart'
```
Then in Yuvomi UI: Settings → Calendar → Connect Google Calendar → authorize.
---
## Phase 6 — Paperless integration (decide approach first)
### 6.1 If using as standalone documents module (no Paperless bridge)
No action needed. Yuvomi's Documents module works out of the box — docs stored
in encrypted SQLite.
### 6.2 If using WebDAV bridge to Paperless consumption
- Paperless consumes documents from `/mnt/library/documents/consume/`
- Point Yuvomi's WebDAV document storage at a WebDAV server serving that dir
- Options: run a lightweight WebDAV container on paperless LXC (103), or use
Nextcloud's WebDAV if documents are already in `/mnt/library`
Set env vars:
```
DOCUMENT_STORAGE_WEBDAV_ENABLED=true
DOCUMENT_STORAGE_WEBDAV_URL=http://192.168.8.130:8000/... # or WebDAV server
DOCUMENT_STORAGE_WEBDAV_USERNAME=...
DOCUMENT_STORAGE_WEBDAV_PASSWORD=...
DOCUMENT_STORAGE_WEBDAV_ALLOW_PRIVATE_NETWORK=true
```
### 6.3 If building a custom module
Write a Yuvomi module (client-side JS + module.json) that reads from
Paperless API at `https://paperless.hubris.network/api/` using a Paperless
API token. See `modules/MODULES.md` in the Yuvomi repo for the module format.
---
## Phase 7 — Backup & maintenance
### 7.1 Data persistence
Yuvomi stores everything in a single SQLCipher-encrypted SQLite file at
`/opt/yuvomi/data/oikos.db`. This is the only file needed for backup.
### 7.2 Add to homelab context
- Create `/opt/homelab-context/containers/129-yuvomi.md` (or `.../house.md`)
- Update `inventory.yaml` if using a new LXC
- Add changelog entries to caddy (121) and ingress docs
- Update `plans/index.md` → mark this plan `Done`
### 7.3 Schedule backup
Add a cron (or existing backup system) for `/opt/yuvomi/data/` if not already
covered by the host-level backup scheme.
---
## Summary of steps
| Phase | What | Who/Where |
|-------|------|-----------|
| 0 | Clarify Paperless approach + deployment target | dtoro |
| 1 | Docker Compose on apps LXC, start container | Hermes |
| 2 | Caddy block + DNS record for `house.hubris.network` | Hermes |
| 3 | VPS traefik router + cert sync for public exposure | Hermes |
| 4 | Authentik OIDC provider + env vars | Hermes (needs admin UI) |
| 5 | Google Calendar tokens + redirect URI | Hermes + dtoro (Google Cloud Console) |
| 6 | Paperless integration (depends on Phase 0 decision) | Hermes |
| 7 | Documentation, backup, inventory updates | Hermes |
---
## Duration estimate
| Phase | Time | Notes |
|-------|------|-------|
| Phase 1 | ~15 min | Download, config, startup |
| Phase 2 | ~10 min | Caddy + DNS |
| Phase 3 | ~15 min | VPS traefik + cert sync |
| Phase 4 | ~20 min | Authentik provider setup + env |
| Phase 5 | ~10 min + Google UI | Redirect URI takes 1 min in console |
| Phase 6 | TBD | Depends on chosen approach |
| Phase 7 | ~10 min | Doc + inventory updates |
| **Total** | **~1.5h + Phase 6** | |
## Rollback
If anything goes wrong:
```bash
# Stop and remove container
pct exec 105 -- bash -c 'cd /opt/yuvomi && docker compose down'
# Remove Caddy block, commit, push — auto-deploys
# Remove VPS traefik router, restart netbird-traefik
# Remove cert sync entry
# Remove DNS record
```

View File

@@ -0,0 +1,246 @@
# Plan: Migrate Booklore → Grimmory (LXC 130)
**Status:** in-progress
**Date:** 2026-06-29
**Goal:** Replace Booklore on shared apps LXC 105 with Grimmory on a dedicated LXC 130. Grimmory is the community fork/successor of Booklore with the same database schema and port, so the migration is a near-drop-in swap.
---
## Pre-flight checklist
- [ ] Note Booklore MariaDB credentials from Portainer compose on LXC 105 (`DATABASE_PASSWORD`, `MYSQL_ROOT_PASSWORD`)
- [ ] Confirm `/mnt/library/books` is readable on LXC 105 (`ls /mnt/library/books | head`)
---
## Step 1 — Dump Booklore MariaDB
```bash
# On hubris — find the MariaDB container name
pct exec 105 -- docker ps --format '{{.Names}}' | grep -i maria
# Dump (replace <CONTAINER> and <PASSWORD> from Portainer compose)
pct exec 105 -- docker exec <CONTAINER> \
mysqldump -u grimmory -p<PASSWORD> grimmory \
> /tmp/booklore-$(date +%Y%m%d).sql
# Pull to hubris root for safekeeping
pct pull 105 /tmp/booklore-$(date +%Y%m%d).sql /root/
```
---
## Step 2 — Create LXC 130
```bash
# On hubris — list available Debian 13 templates
pveam list local | grep debian-13
# Create LXC
pct create 130 local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst \
--hostname grimmory \
--ostype debian \
--unprivileged 0 \
--cores 1 --memory 2048 --rootfs local-lvm:16 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--onboot 1 \
--mp0 /mnt/library,mp=/mnt/library \
--features nesting=1
pct start 130
pct exec 130 -- apt-get update -qq
```
Set the static IP directly in PVE (same pattern as all other LXCs — no Fritz!Box reservation needed):
```bash
pct set 130 --net0 name=eth0,bridge=vmbr0,ip=192.168.8.213/24,gw=192.168.8.1
pct reboot 130
```
---
## Step 3 — Bootstrap LXC 130
```bash
pct exec 130 -- bash -c '
# Media group
groupadd -g 10000 media
# Docker
apt-get install -y ca-certificates curl
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
'
```
---
## Step 4 — Deploy Grimmory compose
```bash
pct exec 130 -- mkdir -p /opt/grimmory/mariadb/config /opt/grimmory/data /opt/grimmory/bookdrop
```
Write `/opt/grimmory/docker-compose.yml` on LXC 130:
```yaml
services:
grimmory:
image: ghcr.io/grimmory-tools/grimmory:latest
container_name: grimmory
restart: unless-stopped
ports:
- "192.168.8.213:6060:6060"
volumes:
- ./data:/app/data
- /mnt/library/books:/books
- ./bookdrop:/bookdrop
environment:
- DATABASE_URL=jdbc:mariadb://mariadb:3306/grimmory
- DATABASE_USERNAME=grimmory
- DATABASE_PASSWORD=${GRIMMORY_DB_PASSWORD}
- USER_ID=0
- GROUP_ID=10000
- TZ=Europe/Berlin
- FORCE_DISABLE_OIDC=false
extra_hosts:
- "auth.hubris.network:192.168.8.175"
depends_on:
mariadb:
condition: service_healthy
mariadb:
image: lscr.io/linuxserver/mariadb:11.4.8
container_name: grimmory-mariadb
restart: unless-stopped
volumes:
- ./mariadb/config:/config
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=grimmory
- MYSQL_USER=grimmory
- MYSQL_PASSWORD=${GRIMMORY_DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
```
Write `/opt/grimmory/.env` on LXC 130 (fill real passwords):
```
GRIMMORY_DB_PASSWORD=<same_password_as_booklore>
MYSQL_ROOT_PASSWORD=<root_password>
```
Start:
```bash
pct exec 130 -- bash -c 'cd /opt/grimmory && docker compose up -d mariadb'
# wait ~15s for MariaDB to init, then start grimmory
pct exec 130 -- bash -c 'cd /opt/grimmory && docker compose up -d'
```
Verify Grimmory responds (before DB restore — will show setup wizard):
```bash
curl -s -o /dev/null -w '%{http_code}' http://192.168.8.213:6060
# expect 200 or 302
```
---
## Step 5 — Restore Booklore DB
```bash
# Stop Grimmory (keep MariaDB running)
pct exec 130 -- docker stop grimmory
# Copy dump to LXC 130
pct push 130 /root/booklore-$(date +%Y%m%d).sql /tmp/booklore.sql
# Restore (replace <PASSWORD>)
pct exec 130 -- docker exec -i grimmory-mariadb \
mysql -u grimmory -p<GRIMMORY_DB_PASSWORD> grimmory \
< /tmp/booklore.sql
# Restart Grimmory
pct exec 130 -- docker start grimmory
```
Verify books appear:
```bash
curl -s http://192.168.8.213:6060 | grep -i grimmory
```
---
## Step 6 — Authentik OIDC update
In Authentik Admin UI (`https://auth.hubris.network`):
1. Providers → find `Booklore` provider
2. Edit:
- Name: `Grimmory`
- Client Type: **Public** (Grimmory uses PKCE — no secret needed)
- Redirect URIs: `https://books.hubris.network/oauth2-callback`
- Scopes: openid, profile, email, offline_access
- Back-channel logout URL: `http://192.168.8.213:6060/api/v1/auth/oidc/backchannel-logout`
3. Note the **Client ID** and **Application slug** for Grimmory's OIDC settings
In Grimmory Admin UI (`http://192.168.8.213:6060` → Settings → Authentication → OIDC):
- Issuer URI: `https://auth.hubris.network/application/o/<slug>/` (trailing slash required!)
- Client ID: (from Authentik)
- Client Secret: leave blank (PKCE)
- Click **Test Connection** — all checks should pass (container reaches Authentik via extra_hosts)
---
## Step 7 — Caddy cutover
In the `dtoro/caddy-conf` repo, update `books.hubris.network`:
```caddy
books.hubris.network {
reverse_proxy 192.168.8.213:6060
}
```
Git push → Caddy webhook auto-reloads (see [caddy (121)](../containers/121-caddy.md)).
Test:
```bash
curl -s -o /dev/null -w '%{http_code}\n' https://books.hubris.network
```
---
## Step 8 — Enroll as homelab client
```bash
homelab client add grimmory --lan-ip 192.168.8.213
```
Commits the `age_pubkey` back to `inventory.yaml`.
---
## Step 9 — Verify end-to-end
- [ ] `https://books.hubris.network` loads Grimmory
- [ ] OIDC login via Authentik works
- [ ] Library books from `/mnt/library/books` are visible
- [ ] Reading progress / metadata from Booklore is present
---
## Step 10 — Decommission Booklore on LXC 105
1. Portainer → navigate to the Booklore stack → Stop → Remove
2. Keep the dump at `/root/booklore-<date>.sql` on hubris (or archive to `/mnt/library/documents/`)
---
## Rollback
If something goes wrong before Caddy cutover: no user-visible impact, just shut down LXC 130.
If Caddy already cut over: revert the `books.hubris.network` block to `192.168.8.205:6060` and push. Booklore still running on LXC 105 until Portainer stack is removed.

View File

@@ -6,6 +6,9 @@ Pre-flight runbooks for planned changes that haven't happened yet. Once executed
| Date | Title | Status | | Date | Title | Status |
| ---- | ----- | ------ | | ---- | ----- | ------ |
| 2026-06-24 | [TRMNL plugins LXC (128) + middleware deploy pipeline](2026-06-24-trmnl-plugins-lxc.md) | In Progress |
| 2026-06-25 | [Yuvomi deployment — house.hubris.network](2026-06-25-yuvomi-deployment.md) | Done |
| 2026-06-24 | [TRMNL plugins LXC (128) + middleware deploy pipeline](2026-06-24-trmnl-plugins-lxc.md) | In Progress |
| 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Done | | 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Done |
## Conventions ## Conventions

View File

@@ -13,38 +13,47 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXaWJhdFAxdThGSVZRN2I1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaRWN5ZzIrNCtYeVRVNWdB
VjE1djFtdjdnSFVJU3lMZC9YSzNSeGVLaVJBCjlId3pvcFRadUdwamxaa00zNUVl b0M3SHF1WVRTZzhhZ3NqK0llaVQ4SWxvSndNClprREJjWm80aGViTktSbGV0ZjFJ
VUJFaDRRLzdSNDNVekR3eWdmREh3eUEKLS0tIEVMMmh2eUlSL0FhRzNnZXJyWk9I R2thaXNXLzM5UmY2UHhuYXNITys2eGcKLS0tIGJkZ0RJS1lFTUhCZFlQTmd6M0NH
MEY3dlVDQktRR1VUajM4WmVwYS80TVUKA0bkns4IE093PvF5Ka3HNWmi+Htqs66H d0N1RWRRNTkzcGR2Zit3cnN2TXd0dm8KpvHCBO1gejHD0okrivBzC0qmfcFDQgHY
BBEAETnQZOdM/Ca+oySDnhLYYT4rD46m4d3H0YQfXQhlVk/h9CTshQ== 8ZLi83Mv7PflCZpcv67d7mai1F89DGDCsoo7TMGHh6rNU+Mpy/g5+A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bVZma09OVWkrVmtVNEda YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2YVQ0WnVDUEl3eU1kdGw1
MnRxdDZuRTV3RmhieTB2ZXVTcjY2VWtTOW5rCkw2STNWTHluRGZhT3B4TmtFWHUv aE81eU9iUExJZkg2ZElNR3Z6b3VCWkZSNHk4CkVGZFVZUnpxZEZRd2t3eDBhcmd3
L2hyd3QwMW1iaHRCRWMycmxHVW5qMlEKLS0tIHNrTjdYb3NCdU05WjR3RFdvdFBR VWIwSGhWS2JKSXQ2K0xva1NTYVB5bDAKLS0tIEFEc0Q4TXBXeWV6eW93eWxSMkdM
Z3NRRVRRcnBhdXVmRmprbk1iL0lWS28KTvN7Z3VyXKCnbJBD9N+FtC5UDCxofPIU TllXK2xIL0R6T3dHOGdDNVdDTzdycHMKntYg1r5tOHWxpkce89ixirQBOBpIdAuN
WERW/eV7+2F6SeClSu7iE+pDMbjiKrFBVKBAU8OA8yd8VmcnD8IWdw== PBAdd7vY9zL4tq+AB5Goz7gj2I56Fw3tM970YX/JaThCCiyquk2OPQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBublRwajRDeUtmdmtpN3pj YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDTnlKU2J0RFU0YU43TzJt
VVE0VWJhVWRXeE1UM0xHdEJKUVhHVWpNTG00CnZjNUg3ZkUvcFZTRHc4UDMrOVhV MGpQdy9Gb1J5cnd2K2Nvc0FnTUVCSzZudHpvCndmUThtaStCTWFqUzhMVlZ3dmZZ
QTNPMmphTkhxZjgzNXN6REtTTTZUU00KLS0tIEk0MXFBNS8wVmpMZFhoYUpsOGlG ZHpsTUFmWEVCYWRpT29RNHRVSk5iQlkKLS0tIERpY1RUalA4UkQvSFR4SXNlZ3Z0
ZE1OaXdhZVVGQXUrdUhJT09UU21DNkUK4DcLYPqf3ojRotaa95KIUhKYQwWorsum VG1xSDJCV0ZZR3gwczcxYlNsLzArOVkKvnPBHgk24SBvAsQWw+2FxCVLUdKMNyb3
IletEBZtyJdOJgpwN/eZLe12a4E5thI245jSjQlw2B8RKFqOCDyASw== D/Zk0EnwdT6JitDTHcQ2PlDoKBSK6BpW2Bk1gnaC9doiXSz6ykziWw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPVEdGR0VwUjk3MUROOHRJ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Y2c3Z1NUT0NxQlpabTlP
aXdFMTFDRFZBek5FSWJSbWJDU2F2ODFyTVFzCndTbStrQmFSSWJwbFc0M1Nia2FS SGtwK3JwcHorUDRXKzlFTjBhUno0SUsrK0VzCkdVQzAzSHJuRG9BRmJ1QW9aWFk0
eGZEMGsrYkNnT3M2aEhOS0IzOVNOVncKLS0tIENtOEx4SVJQTFcxR3NIM0NHVDI5 MHcxL044b2VxYnpOTlJtNFN3NjNsek0KLS0tIGovbGtSOWdmclVKMytHZU5tUmhl
Z1RIMWRBNGhCOFF4MkdHRlpya25ybGMKsyLYsmqxuXvJ4ZF97Jh5D8BoepehSdKi WFJLbFVlQjQ5U2pQenhWN1dUd1dWcUUKxb2yYZFys0AqY7+M0/gTIDK+1Bl0FIEj
yGzLaaQo0gW/wu7n0fq7S7HhbiTcPZ8lQboVvhYWU2lTx1p8npUsAQ== DSqbAb2UYfNXKVEScJl4QeRLEYuTUKejD0WT4cYJzyjRAZzc7PnykA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRU0RhSWZpTVdFelVTeXF1
aXhPSGFLdWxMQ2J6UkVEQlN5aUJLYm0xN21ZCnhTYWpvQ1B1Z0dnamQ5SWk3NjVR
bGs5TGdZb2NkVkROS0hYQ2ZDVXpIWVEKLS0tIENyblo2L203OUNISUVKMkhXWmg1
NllKUW1mUVRldU03RUM1dThUeDJ5WjAKiy7aGBpQImoMdmFkNwR33Aksx8YDrjOg
DfOpNOiGHWWNfmLHngvGKzCwjqV0nsl1rM2khXcZ21w/9L8USNCceQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-20T16:24:18Z" lastmodified: "2026-05-20T16:24:18Z"
mac: ENC[AES256_GCM,data:d3qcjEqpPIFeut634ImvISJLdit0MQWcdFYomrsqriqJ0NUKdyq3XCk85+vvPvDWikB4WEApk6HMXssSY8mhDci95q5Ssmr+JRAhLebMZjs9yXUf9A7vXpNFswmdldu3CKBiDrhm2GE08qUfsCkcf3jjihEqsfHhxMuWSd5fZpU=,iv:VZgvZ3bBArRChgRX38U6/43XoX5aTnts7Kd4S9spBPk=,tag:/5oOw5yvulvFT6KXv2zj0g==,type:str] mac: ENC[AES256_GCM,data:d3qcjEqpPIFeut634ImvISJLdit0MQWcdFYomrsqriqJ0NUKdyq3XCk85+vvPvDWikB4WEApk6HMXssSY8mhDci95q5Ssmr+JRAhLebMZjs9yXUf9A7vXpNFswmdldu3CKBiDrhm2GE08qUfsCkcf3jjihEqsfHhxMuWSd5fZpU=,iv:VZgvZ3bBArRChgRX38U6/43XoX5aTnts7Kd4S9spBPk=,tag:/5oOw5yvulvFT6KXv2zj0g==,type:str]

View File

@@ -9,38 +9,47 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUanM5MzFZYnkxVHVHSlYy YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFMWU0K3V5NmZBNUk2TWNO
U1Q1ejNJb3ZORklKM2xjK0p2cytRU0NNRUZJClBTUDJ5dFM2eUFjV0t1YzBCcXcv NmdRWjlBV3p5dWNhUWhTOFlEdCtkMVh5STJBCkwzbUNkenJjei9TVWVUMi81d0ZX
cmE3czJST1RuMHJVN0tmVTZNdGNRS1kKLS0tIFUydktvK0tBU2U3czRhWHVySGZR b3dTTnJudVZiV0YzajNPcXlUWUtxdWMKLS0tIEo1eUR3dmJQRWM0bHRtMFc5Q290
ck9KTVR6RS9pOHQ4WkZneG1Tbk5OSWMKwOQ+CWkuLSqfhle9fgDw4XXIp0ojZssw QmwyMk44SVBXQ0k5QjN3ZWtNS1FSWTgKcc/F1HfMfnwun+mIUq1Ds+DZPk7F1ohe
9YWK/suEAb6u9nzbw7zuEmZxhhDV0Y61UAeSSbSmygy7MD7dvxrvgQ== OIVt5aJliApDYesRJ14K1ZEdi4YqCqO+1cLi69iWKHNT3PcOYrepUA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCbm5MOG1Bd0FZbVZTbUU1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFNVUyajV1NFo5VFRyMDlG
VGpGVWNnUk03RURPNURNNjdQejY2WkxkNzBRClpuMGFaQzZhaENJckZvbUI3VmUy dWFlRlVJZDlJTHdYOFJLRi81OU9BdkI2RG13ClMwLzFHQ0VxSXJzQ3V2S2pjVFRS
ekV0eFMza1p4S2w5L0hvNndjN3RpdTQKLS0tIDRTdExxTlZQK0FKV294dnh4dmd4 dEQvK1VMZ3FDZDkxSHdYb3NHeHE4QTgKLS0tIDNyZnV5OFVQNXVEUUlzK3RYMytt
a21yK0VabzhOcDhRajlxbVE1dTdtcWcKQorqkwUs7lppaDJuCDn+KtH+76xPIvsr Nnl5bTZuMWFLY28vZnBPWlFEYmFyK00KtwcrF0W91FIi+9nn8rF2G8xP2/ca2h3R
9axkrDLumeY9LtDhunlaEpax7zdvvAiC4DzJdbJX0LgFCaurtgubOA== DagqhSmpaEC7QHtYGP9SAChhBrDuWdVtK0Gdbc4m/31gW7Fh815zfA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaS1JrL3lpa1c4QUwxWlZT YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTlFZYVFHZHNCUkllZC94
VWQ1ZDVPVjN6QTdFNGQzNkV5WllOK3dYWHhzCllTbVdzRVFTbDZ4TU9PZWpKNnJu ZUFaS3JZTmdPclA4dlR0QklxMmVxNWNyZUhvCnVoY0YvWGlsbm9CaURkQzZUMTRs
cWJ6YWs5amVsR2xZZFpaQlErOXBKZVEKLS0tIFFzVG9QWmdNWGl5UEkxaXZvMUxx ZVNmcDJxZWN2WG9nUUNtT0I4S1FIem8KLS0tIHBRWFhLN3p2Z0ZaWDEyeEw4WUhs
SExXL1gxNzVud2JrZ04zTnI4cUlNSVEKPF2zoSnYEt/zeG8QW1454Mcr8u8JTCl7 Z0RwQ290OWloQzh1eXVLenlrOTZyRzQKQEnY4KC8ReKGFDzklK/A6uIGMRIhMfLW
jDyghcRw94enbPbA43zsKGn6QALQ40PfXmZ/MlEbdnf6U3zI7zvygQ== IzogPQiWYcDerMDd14kktBOcbjKRkI9gPSDZdmieS9z/wA5J37DNNA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwMlVSajFiT2loclFzeEQ5 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCSUlxdTFhR1dxaDU2MGdL
ekxqUjBkSEh5M2FJMXA2VGkycllYa1R1Qm40Clh0L0RwSzZqeXlQVXMvRVlsZ05L dVJoYVQwZm9DbEVOYXd4Ymd0dk56R283WjJFCi9MS05CVkxYY2NPc0pjeGNacm8w
RHkyMlJlc1BmV0tjQk1uaWJLemR2cmMKLS0tIDlXT1dhRkpUdDRpRkJhL2NHSjN5 ZDZGYUMrdDdKS05BckhoRDl4OTVxYjgKLS0tIHlPaUg4Yk96UVJacDFhSm1FaGZo
QU5Pc25lSUV4OWROZENXQzB4Y0o2Y1kKzhOY5jN4gi+u5tl9rAHGRb8Bh9DESl1K RVVSWVRteGcwN2dHOVhuWFpmU0p5aEkKXetFyc9PKB6dCljl0c/+JJrEyDvYJP0Y
qNiD+wauApal8iRrIkGdGsrWrWThSwy7vjnUsoB8JDrBaqLj2usuFA== 1fbPf7WBJsLTaQgrjsGHU4wqlg0Se5GHMeriOPYaRjiL9locKW2SiQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwcWxaenJybWpqQ0RpM3ov
cEszZDI4YkswY1hpUmZDcmZ1Ky84N0xUUlh3CjJLcm5nVE0rSHhyeEhNSmFIaE00
RkpqVHFEdVR5VmdETnhnQ0JiSlFyNTQKLS0tIGNXdkMzaHBtTWU3azdDVm15NkpZ
TWlYZXdBYTROcHBqMCtFcEtJRUJtOEEKdBbB5a5teUnvIYaLJaLp3KZkEXhnkFor
pbnL/WrHQViLdu8pJ5nscIO9ryQt7dS6aAu6gP3Tbs7XdREOTFgKmg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-20T14:39:14Z" lastmodified: "2026-05-20T14:39:14Z"
mac: ENC[AES256_GCM,data:CAoIW1sJkIbNoXMaVlXwfHdREzATNqDKpSN5ecaSiRARThB0/tcLNbkazXKyV1Rx5b+b3s+yCpBzwmNnH6ytaCzG5pKPcnfd6YJq7C5rm/EQHyaVGrPdBsSfqiz4yBXKClej//v3+qkD5Ls2PMppv5KmqigOKNQC/ot3ht2c7lM=,iv:eOmDDclksr9f9CDu6dJprSFwlWuoggzVFjyfqWf5Uhg=,tag:IU3sMlIi92ohM86zOIJdAg==,type:str] mac: ENC[AES256_GCM,data:CAoIW1sJkIbNoXMaVlXwfHdREzATNqDKpSN5ecaSiRARThB0/tcLNbkazXKyV1Rx5b+b3s+yCpBzwmNnH6ytaCzG5pKPcnfd6YJq7C5rm/EQHyaVGrPdBsSfqiz4yBXKClej//v3+qkD5Ls2PMppv5KmqigOKNQC/ot3ht2c7lM=,iv:eOmDDclksr9f9CDu6dJprSFwlWuoggzVFjyfqWf5Uhg=,tag:IU3sMlIi92ohM86zOIJdAg==,type:str]

View File

@@ -0,0 +1,50 @@
members:
"+491726924525": ENC[AES256_GCM,data:RA==,iv:FLOi3HmBPFtS8BAqPR9Epihs868OwAWs1t2LGjg9kGU=,tag:bf8PGNipOeepfnzHUE5KMA==,type:str]
"+4917622791635": ENC[AES256_GCM,data:Mw==,iv:onlvwM9nlAlXnL6f+BTRZlOi4+C6pryyiay421GY9WA=,tag:KiYrTcRlBNbB5mYYlb7GHg==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmYW9VS2FLb1BTVTk2OFZZ
Wnd1U2tnanR5N2d6SmNqdFYrekpzZTRMRGpZCjZhREcrY29qc2pFZUhnQnc3U1dE
aU5Kd1Robkk4OVd2M0ZkalBWMlQzR0EKLS0tIDJOTjkwR2htaDhDR0hqN0R4cCtB
dldLSHBOVjZxNVFZVUtkZm1INkdvM1kK2iaqr75MZ+4QlWaNOjH1X11zHbi4Ahy0
ely7Fakx6wSoQtl803q6UyNJyIqCOavJKoLQw48FhAl2yGmv9KsvrQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4eXFvMjZXVWkvZnNLSGhS
eWJ3Q0M0QzlnYnJ2dTVhZGRtZ3Y4Ylh5NHc0CkVTRnNLa0NzK2czcjFSSmgxQ081
ZVo2MzR4S2ZlY1AxZC8rWTJ4d25vekUKLS0tIEFnK2gvTVBUWk5jOW1NaUtXTmRL
U1FCaHZjaTUwZ1RGVlZkZ0JzT1NOek0KceYGFDlpcpAQte0yqebyuQBxr00/Rurh
McQ607wCQWbGKkwkHCDi1VGqqokvbU7MRT9iOBLLBlxEF+KS9UiYuA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6aXRkd3d4ZTV0RmltR1JN
bFg4VU0vMDVuR0cxc2I0b0NRRUQyQlE0RHk4CnpPaUpGMEh0Unk4d3BDeE84Q0px
RnZnUHRQa3BJQUpGWUFZd2dLczhmc3MKLS0tIE9NOUNacDM5dktuWWZ2WWRxajZu
Uk5Gb216dmdvUkJzYkoyWE5yQ044eWMKSGR5dDya0gyBWDMB+NSudXK38VQYP88y
M6lyeZUAJ3cFydjyE1PjyllBehX9tYh/rh9RS67y/zRSmkXH5e7eAg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkM2I2MkV6NlhydURwYk8w
ajB1cEtiQjlLYkRGbDZXTHkzTWxuNW53NTJVCmJuMzJZSlNtU1BxVFNpOWpSVWp3
TmxOeTRhRmZlanp4QmVmRjRSOElHL3cKLS0tIHp1MzZQUlUxNXFlS1B6TWhQdzhE
ekFibGF3ZE95K2FjZmZmVGkzek54R1UK7Au0Op4P+JZjHIiJJ5effjxjeAJItXw1
Wrq8Z6D9NZTAZ2TBUGpdqIZcZhBiOuqxdNV88ZHOu8sjT1SLwm17GQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-28T14:50:45Z"
mac: ENC[AES256_GCM,data:7gWwrXY9q7zpiDZgFidEMa4VbxJ0uM7HdG6Ea8P4fbISgRnOx+aoAwznEQ+K/fIBsa1qtiR82lIxsNr6b44W4jmFF3+rPAm9DMs6ImX1KIVCW3GVptM9fMqOxnCI7RGMKKwWYxkvfE2lwl5EsBljqqlsuxDfX26etfqukIhbOIY=,iv:6iI7Q8wFKguICPtFvZWshok7BkEUUITp5XcSYt9hwFM=,tag:jIOzsy4jiSvuHh5GNQsjpQ==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4

View File

@@ -8,38 +8,47 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvdUxUdzd2VmN1MWU2dXdM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxZjBPQ3M3NkJwZ2dDZlA3
N0FWcXkveVdHYk5aYzFrOVZaRzVYZWs1RFZBCkFwRy9xVFBrVjU0NmhkMlpyN0pp MGR2cTM1b0NBN2ZETFJZUW95M2lITXo4dlNJCjY5NEMrc2xNaEpCVUtkVHJYa1Rr
ZlZKenhoNFVRdGg4Q0ZTMS9YUDFKMHcKLS0tIG1GYkZaYkhJNk5zdWZBMEtNL041 citGL3ZQQnlMR3BpVFpmU3d0eEhiZG8KLS0tIFZGejF0UDJZTUVUMU9KL2hySm9L
MWhEQ1RmQ1k0d2Y2dzh2YnRvRm1IOEUKJ64/tNyLe2qBIL4CetRlpLaxhfOL93D5 aEx2ZWRuTldDdFJrNUlmMHZVay9vRnMK0XMd/S3VSdD71eh2MUJGs8c7Jdxmfpua
BjeHLwtfXB2fNCJSdCY8H4KsQP+epwriUQPtZVId32n9xDEt71obSw== p4ZN4Y4MARz2XMA8xqDpw1SrMevaceD5+p+R+zhunb99aFfhtV7WxQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRNi9kMkt5Vk4rS0pqY2ZP YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3aXdya1RpSGdsQVNMYXlQ
ODErMlRxNmlWaHBvYjNaQ3lRT1RSQitMVlFBCk5HQ2o0QUg4dWFodTNEVGJFTlF0 cFhCZDUvTnFUMWdpQzAreGprTm9ZOE8vZUI4CmF4K2ZGQ29QOU1kMlhUV3N6RUNk
Y2JIVEFPZGVUdXpNS2lGOVQzeFRvMFEKLS0tIGtQY0dicE91R0J3eEM3bEdMZHJ4 bzJwZDNmR2NITUlzY2hSekozdDBsQzAKLS0tIHI4cy9IR1gvWVo1emcxbzFURWVM
TndiYVFQWGZyRkF1bWYrRlZ6N0JBRm8KEUKY3chev13KjnGKdTR8tvyYV3s0W1rI YzJvRjlNeE5KancrUUtSZndQNWxhYkEKAedJLVpc47R8rgm4YRyT7Z9G4tfbBnqQ
8LeJSIocSX58PexqgcKsT8pGpuIiOetEzjzv2WPa0MzEwtFCVgwKYQ== c1UObcIa551wR79n7vJvVb2cSLz3VEURe6sNi4OqJmg017qrMOS5Wg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFcktzeGx5VWZaeFVNc2Mv YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrazAvYnovQ3ZFQ3YvcTh3
aWt1dzNUVko5YytOTTdxMWVEVm85N29TMDBrClc3ODdVVDRVRFI3K3BiUGd1TFp1 eGhYdFFTZ2ZkTElzVmFta3ROMGpCMlE3VEVvCjNXd2x2QTREQ2NVMEk5OU1JY2Z6
SE5nalY0RkwzNmZQOWU1THhGQ3Q0UkUKLS0tIHNZdHhxdDY0a1AyY0g3WUx3bUJh NHQzQVVaMVR0bWM4R0lxVVpwTEY4YXMKLS0tIHFTT1dRejNyQlVoUEREUnhlM09V
U0RqRWRFRXNLTWFwemNOYk8vTmxZeTAKaZI6WSNM022xtZQx5yOYXj5mesAjWsu/ ZEF5WFlnYmgyVDRUYXBTVTgyV2laWkkKdE7hAUxtDZJt8P4LrfOomK8rMTlXeXd0
ZIDG6MayoyrhwAkuZsWOKtKp6zbRCcnm7s9OHdlNsx7PWG1ODEFpOQ== fyY29wRnXqn8s0IkpibWkByWLgyXHe1nNHHgsiPsxYMFbCRW1iwZoQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVbFFxSkhscUV2bm93Z01O YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNdzIxYUlkY1YvTVdrREgz
WDl3MU11K2lrNzAwUVN5MDhodDlveFZ6QlYwCjNaNjd3bG9QMTQxcVA5TTNQU3Za bEhrTlpXRDFST2MxTUFpSWwrMzF6ZTVsUFRNCnB5WVJKaHNFL0hQak9ZbTEwNjF6
bWdZTDdGZzJObnJ6NlBxKzVYcmpVNk0KLS0tIEJTcFVTRmkxQlJWSzVIM0ErOVNN YmFEdVJ2YkEyS3BDQkUwZjBJNXh4UUUKLS0tIEpIcGV4N1R6aUU5S0l5dGZOdnNV
c290WkI1ZzBlYkg3QzJJUlQwOFZPYmMKQFp4vqFwh0GpYOur4kGMCeENnuNZnN0x Njh1WlAwR2tsTXZoUS9JMmlYZm1ZUzQKcqXYn1KgSzAXS+m13/wLmeriNf+fTzY6
sbNmM9rRm/10T4tHF9aR2/WIjEqm6M6+yXd3phhB8rIggDPlVk2bCg== TSBcB3OxgqvYAWwZutXr6UScVauZPuatSjwE1va4HJQuhSVoCGwqcQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4blY0TEdsU3VoSmR3M0FK
dlg4S1JCRVpZRnc4SURNaWdSUjVQdnpCazJnCjhobHN2dmpGZzhrMWs4UUFFdVF6
RmtJb0VaQlFIeGhOMFA3Ui9iVmI1K2cKLS0tIHk3M3RhaUtSSm5vajFJUUprRzNa
c3JiWC9WNVRYOXJIQmMzUHZxbUZtS0EKsE5cG85lsDFABMbR/A80TQMlurf8qHDR
tVxZuMQPIXBdg6Ov72xnQeMGC//apWO40gnfjGn+P3oZGy7m8XJBuw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-21T20:13:20Z" lastmodified: "2026-05-21T20:13:20Z"
mac: ENC[AES256_GCM,data:bi4o17skIOtZoxJGzLFJ7IAv+X265qIhgnQz6wr4bch2xofwPZZfzECck0I0xDhcwxcDtx3VZ5E2VBdvHWk+iSan+clRq7735k9+DIpjdWaGxIHuylmelZl6aE5kE36UO7fQdnwmiqCPHxiVq/sKuqknFXkl1+FvO1ryrtSHQsU=,iv:xxm+5rK7o2c43iGS1j2Q274QCRsbDn+k+wskpp5tzeU=,tag:sZHk9eth/TwqM8tIWSrv3w==,type:str] mac: ENC[AES256_GCM,data:bi4o17skIOtZoxJGzLFJ7IAv+X265qIhgnQz6wr4bch2xofwPZZfzECck0I0xDhcwxcDtx3VZ5E2VBdvHWk+iSan+clRq7735k9+DIpjdWaGxIHuylmelZl6aE5kE36UO7fQdnwmiqCPHxiVq/sKuqknFXkl1+FvO1ryrtSHQsU=,iv:xxm+5rK7o2c43iGS1j2Q274QCRsbDn+k+wskpp5tzeU=,tag:sZHk9eth/TwqM8tIWSrv3w==,type:str]

View File

@@ -1,34 +1,48 @@
api_key: ENC[AES256_GCM,data:TbPkuLCidS7cg52DYb0MOCui3TsaiwkOWyYLhp6gcvXI8TqvXej1tNsCF0TNAIr9vvnFaYno/UTEPiOKe/EQqR8uQMyWuvsmMg==,iv:bahy9ae4Qxvkv1OAjx6LwzK5ggqDh2GVHDj95PVP1Mc=,tag:J7PajLaeqFOdz3FESYZmSQ==,type:str] api_key: ENC[AES256_GCM,data:TbPkuLCidS7cg52DYb0MOCui3TsaiwkOWyYLhp6gcvXI8TqvXej1tNsCF0TNAIr9vvnFaYno/UTEPiOKe/EQqR8uQMyWuvsmMg==,iv:bahy9ae4Qxvkv1OAjx6LwzK5ggqDh2GVHDj95PVP1Mc=,tag:J7PajLaeqFOdz3FESYZmSQ==,type:str]
sops: sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: age:
- enc: | - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3djYvT3JJUWh3cE1ZbFc0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4TFVsb3VYWnVFcnhRL2Vz
SiswOGlKNEdFNlJhTVlVSkhqZGlJdGl4U2pvCnV0aEhud0VaOHVGbkVVVkJ3b2Js SVp6cG9CYzdqSFNkTGhHSmtlRDJjVWwyK0RNCkdEcEJtNjB0aHBZQVZBbTlHMTla
K1liaEFscGJxMXVVNjNJNW9NeGR6Um8KLS0tIEt2TlFCV09CWDJKbmFySm1EUXc3 cHduT1lyTWlESmQrVHB6NU9LTmU2MTQKLS0tIHkwQXM5TzZCRXFsOHdjYThZQ0Z2
Zm9IT21QN0lkSlh0UVdhYkVrTDdiUWcK8IO+gylIYKDlSADOtj2gSpM7Af/JG/7Q bS9wOTBDenRMb2hGcmgrcTl5R3dnTG8KZfSI1pnfpcqtD0Z5N6hONSeuAaggCkZI
6y1dT2d82cEu7KuXXuog6gP3sADe/6SDHGgC6Ot6EWM+5dwMN6WwTA== VJgh6TF7soOHbvddcNc5RINHQI6XLOZAeSOzvKm3zT8y5xe48Ff+3g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
- enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKY2xVSUVBUTd0dVhxekhY YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkb3ZmZmpPZDAzZzB5SmNx
d2t1U00xNnRjTC9PT3puVGo1U2ViekhmQng4CnJlZWYxK0tyQ2U0Y3lXRUZSTEti SHFKT3JENnVwZUttcFNYd3RoTHI2MCttQUNFCllZc2Z5Y1dSSnV0bW1LeGJvaURh
cU9QdWhjalJhb3dJSk4vK1hWZGFGNEkKLS0tIG51aFRGcW9xdmdmSkt0TTZXSll4 TlZGWjhseUdjU1FRWE41aTlMczI0U28KLS0tIHpzajRWWmlUU3FCaEFWMlBsVjBT
dkJMYjdPbUgyL1Rmc0tQbHgyR3RLZTAKllFoX3m/zntAtfGkSGFmRzXuk4pHALkR bGVRMGF2YjVwcVNCYkNEZVM1TmsyUncKYLscCS6BD3fwVmK3JnkISsEhXvhuRBnr
XeQaAl33n5dMiZHtynoDNN3eBXtDDWiKZAhZeWI5SMoLpWBEXapNag== tJ+v7mcEXj4pz8FjIyY6MG3T+EXx6+HwuWtPyxbmEfqnxu2Ocfg5Dg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
- enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsdUpxd0RYRld3L1BYdllp YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUSCtGaUdaR2pVekZCUUkz
R3RwVStLUysyR1lXQ0RlcW4vWGRINzB3WUd3CktTSXJzUWsxcEdVajVjS3N1d0NE am1pRTJSdzgvUjJsTlpXVE0rVlczOFZ3aGpzClJNVXRJV2JqbHB5ZEUvUFJXR2cy
WHU4QkVvU3dyYWZ0VVpmYnB2blVWaGcKLS0tIG5sa1I2YW1tS0hLeGc1SWtjV0NM MUpad0ZMYUI2MmtoUm8zcG1RS01WcmMKLS0tIGsvVGhITGRsQkJXcUdmQllOVEVr
TkFoL0ZZbmhXdElFNytLclByK0s1LzgKVrBvlaryZXYn43v1ukjGZnhSPuwfslf7 QnN5QVZXcFR0cDVab09hcEFiWjRoMmsKYo5ThhNgdp9GUXZkyF6JGGdWnmHmZK5c
Ruy7z7EzVaCXRn+F9gABD4mhIQaUIRswiut7aF9lrPOR72/y8s/1JA== QIqk5L5fYq+Wif8bKUJBEv2fGyJJZD74AOCFjQUIDgBnLx2tK3Kf4Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiVHRnMTZXK1FNWmFJZlF5
MWk5cjRHQ2NuRWJrMlhQVXhPL094elF1MW5VCkx5TklkR1VQSXc1OTljaXBFR2FQ
WEtudU5DNUxTWGNpbUFESjBoSkh6bVUKLS0tIFFiV2NkcktOdnF1djY2UC93OVdF
Unhpa0h5RU9xUHNic0hXVGhqSUthN3MKMAOF3gxkJVFU9aKqeUaViDs+Ka8NI4YQ
Bb8wPf9BcQe0NPjVMrxxP7L9c5oACiiNuA/46YxMYu7K1dOj0KapGw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
lastmodified: "2026-06-01T21:23:34Z" lastmodified: "2026-06-01T21:23:34Z"
mac: ENC[AES256_GCM,data:DXvUZUOMKQA0aDpN1foKy+aHhBh6daGSRJM1FagTh76qutk4XF9Een9iyy9sQ7olabCjf3oUezvU7XWB83bSDD3e23CFuweOl0RreRFqIsbXBi55Dbcxw8+9fvMyOK4PXu6mkT/PRao/XR5sSMqiHt3FoJbjazSaJlI5teeVXbM=,iv:IICO5ay+KqkTikfxW4kqB2qcSRpbsOP1NjGWtCB9uqI=,tag:MX2TqUxre+ff3RszCfXeFg==,type:str] mac: ENC[AES256_GCM,data:DXvUZUOMKQA0aDpN1foKy+aHhBh6daGSRJM1FagTh76qutk4XF9Een9iyy9sQ7olabCjf3oUezvU7XWB83bSDD3e23CFuweOl0RreRFqIsbXBi55Dbcxw8+9fvMyOK4PXu6mkT/PRao/XR5sSMqiHt3FoJbjazSaJlI5teeVXbM=,iv:IICO5ay+KqkTikfxW4kqB2qcSRpbsOP1NjGWtCB9uqI=,tag:MX2TqUxre+ff3RszCfXeFg==,type:str]
pgp: []
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.4 version: 3.9.4

View File

@@ -8,38 +8,47 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGbTFyeCtJVWNOUXlSNXQ0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVZnc3SXVxWlJHV2FlZU91
MEF6aG84NEVNUzlrMnoxcEF0ZFpEVC9LK1ZvCk04TkNkVWtVMWhhL2QwVEVEMi9X Q2pVRXEvbFNOODVaQ2F3dzVva3o5RTI0TTFnCkI4MWN2TDR1dnR4OEhmZzhZMW1Z
M3Jkd1NoVFBFaDczMGhtN0svR284UzgKLS0tIE5OdTlXWXptTS9TZWdUNzFKU1VW a2wxaGR5MlVNWmh5YkZwb2dTbGJJZncKLS0tIEUyVkhYZWJiQUhNNTNtS095ellm
QXl2N2I3dFFORlFEbUVSK2ppSmNHSk0KzeS7uJCvQLd97XI/MfKuhJi4pQ/jKx2d UnJBZGVPNC9lY0xKTGNldWs1aFJGSk0KvKBYSxJ0jwddf9OycUfrfYYmH8MoxIbC
5veiGfnmI/j/WCBdH35PMTEotbBBQ26uFt+uvcShK2gO62MVbht8PQ== oO8nk2sfY9enGziu+A0GKEmCIzhBkD+Cj/8jyqTH56NKFykKNpFQyg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWG9PME1XQ1dSNkVEME9N YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZMXpiYlVadUgwei8xU0tv
ZlBNL0FnclpoeEVLTVk5emlnTmpFVWtLcVF3CmQrY1ExOG5wMmRoMkV4Y3hXSjU5 bkpoK1RzS0tnYlJFN3hqRmJiK0Q2VVhyN1NjClZLRVZMcldNQ01yOVhQdnV2QkRE
OTZidFVWcklmZ0dHU0pJeTdiUTM5RzQKLS0tIHBGbncxRkpnUTVlNitRZkhyRjBE bUl3YTQwSHJPSmlpeEpqUG5tSnZRZ0UKLS0tIFVodEZtaVFoYzFkdGd6VkdIeW5P
bjdVSkZRZ241OE9Xd240OExWVkFrczAK+loPUAs5aBMC6XRY+yu0r3bqnWozldP6 bHQvbnhYNnFuR0JGbkljdVRjVEZ6a3cKXccTdlgR94QmaeLGYnXXKOYFuJYkJ08Q
m6mnRhUl2+JGm5RHglDWibUwPZ9I1EVlufDwDCABBnoXnY/HWuOlAw== JYkpwRlYzYMfqlYNuANi2LYTZePQITIYZ+A9sHajCXQlUGjs9PCWsA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxOG96YzNTQk1sQitQNnV3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0bkppcWFWZkVxWGQzRzli
V3FsUEN5b00wcys5TmJSY0lTTm1qaEV1MGtZCnlYa3ZMNTFTdkliM0NjVW9BLzhj bkMvemZ4SGJodEUycGVxNlZFYzJqQlpnZml3Cm9UNFBjd29MTm1NTXI2OWljNFRZ
ZytCMDQrcjVmdEx0cjhNRllZY2xuWDAKLS0tIHhoMVR0KzhZb1dVMldIUjUyc2JS bGErNC8ydVFjTHNRcWpWTFFvNFQ1V2MKLS0tIHpXUzR1WGpPRnJNa3JEWFhFQ0N0
OHBSbVliajNSTDR5VW9odkdpN01iT0UKzifxPVQc98lEB4sXoDcDw7t3R396iwa7 ZWhnMzNEaTJ5RHRaRi9lMDZyVkxFd00K/zh7XlSjvO/we8GNDhKvhmPBAPZTi1o9
RENR54u5GYUHHySq14v4k+7zhV42+xaXW45ZPhcA4BKuaAw8ygSDGQ== kBruJ1OAFIJU4h/6dOmgGsj1Jxx/KZwMMAcHNzUBzm8x282MlFzM4g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5R1EvVlZOOVMxSSsya2Fn YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqUjZXMmpXTitCam1HbkIz
Zm5iWWN6d2RFNzU5aXBNbHNkcnYwM1VLY1Z3CjlEazNpcnhQdTRFMTVxdWliV0Ju Y1UxY0xLR25GbnJucTMxV2x5Wk9SZFVVUVFrCkJzQmE4cUkxSGV3VjAyVlZEQkda
eEZMMnliS3BnU0RMSy9lNDZhVUw4K28KLS0tIG4vN01xZjRNSmVMVWx0VEJhd29Q aW9SRmdGbWwxd0o3OTYvV2pmUHJxN2sKLS0tIG44cXZRTC9kSmJLcE1JSVVqWjYw
QTR3cEhvTVowU2ZXNDY3ZjhSVktSdEEKTxL2M/OgOMcptliInoED6aRDEQaqGnXF QkQzTFZZWno5dHhmN2pEWXpycDdvUjQKmcbWAZh85vSozzsX4CndtrxneA3em3JG
N7dg2UnQsuFplJIfsW8KRoDQkcuXbdjn/W5aQh76+OC8wuRPVNWcEA== nhESflBPc/E2KckyTHOEJkPBq48UXiAvM3pY0s6FGgkhXHriTVJ7xA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNdWNGZi9JWkZnSjF5eUZN
OUhtZ3B5aDVaV2dpU0liOHdtZitQSUcwbGpZCmttMEI3Mm94SnZwNEd4bUYrRHlD
aGJBa3pKZ0VRVXlkL1JHVG41U24rU1kKLS0tIFEvYlliOE9iSHhIbHR3RVZNc0dx
VDVkN3dYVEJXb2dPYjAzOFU5Z1VaU2MKzVcJ9/z4+9phsdwyowwK//Tpb0Ga8rwo
nTcLYw50wOKKG4ju1ISB5RuuUckU34dmUV7k4se9/oxnnectFwYVgQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-21T20:13:20Z" lastmodified: "2026-05-21T20:13:20Z"
mac: ENC[AES256_GCM,data:PXXEpDbJIoIwWuNwMetpALtxrcB7yJhDYqp6LSKV8WFQI2TJSDgDHiNQ2cngfB9PXKmUNNhlvhmB52Hi6WQSWQdXjiy79T1fOjMhbUAKmykTaZDrwvlrDGkPgiZ1cFSb8+hx/5aZw7YhdQO9+7LAmymeIldZIFEpEO2pvuPZ6MM=,iv:mJb3K8TnsnY183OX7o9pF7oVMDcwVU/rOgX74KTRpr4=,tag:f+M+FT0Vjozm7a8LFDI8lA==,type:str] mac: ENC[AES256_GCM,data:PXXEpDbJIoIwWuNwMetpALtxrcB7yJhDYqp6LSKV8WFQI2TJSDgDHiNQ2cngfB9PXKmUNNhlvhmB52Hi6WQSWQdXjiy79T1fOjMhbUAKmykTaZDrwvlrDGkPgiZ1cFSb8+hx/5aZw7YhdQO9+7LAmymeIldZIFEpEO2pvuPZ6MM=,iv:mJb3K8TnsnY183OX7o9pF7oVMDcwVU/rOgX74KTRpr4=,tag:f+M+FT0Vjozm7a8LFDI8lA==,type:str]

View File

@@ -0,0 +1,48 @@
token: ENC[AES256_GCM,data:LgiLFEsli412rVxSo9YU69wx9Lh7eDGBw6nmROh6Qbw7bYN8lylNjSDWkjNJ/UJvDg==,iv:vNmiV/D+SQaMtgwTK/mFDQIwSQ/L0JRatmoj5AWfBiw=,tag:W5P98TlmevQ0j051044LPQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQYVBwS250T0ZFa2VGbGow
R0MwMzNsY0t2N1ZBVnlnbkF6UEIyUStaUEhJCnBuNWNZT1VxRFlKNFBjOTZHNEd0
cCtxN1p3TjkzWUZNZkdZeXRJVEhaQzgKLS0tIFVSWUpVQ2k3VE83eHU4VmR4SnVH
RVgxdDNnKzMwM2c2bm1YS1ZSRDdsMVkK8PVncSpesaxIEz7a39rVBzB0V3RphmAA
ANV8fY4W4tLuxo+EhB90ciy2dnAHaKEbxHBWMMjN4u22yZRT4lPWQw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Sk1hdENFQWIvZXhIK0U0
TmhCWkw5TWhSVXdIbDN5SmVNdHkwYm1aOGxvCmlnQmZYWitxUmdURzNYRDBFUUVM
TjdIN2FEd2ptWG5UVlFCb1IyVzFjMmsKLS0tIDJMZHlXVHFNUlY2bnhlS3hFT3FY
U2szdVB4MGxxUnpJN1BBaUhKVE5WQkEKz7BkFIfs2RSyAww73dH5+PyDNjo4Ocor
mX5pLhSRGGf13bfA5lMQiGvqtd3jw1c4zKVZD9BcthIk5H/oqallNQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmVWI2L05GbjdLR1k3a280
N2RRWmZsZWZPeWdmd3I3aThrV1ppOEw4VTJRCit6aXgwTVIwQldkSmpRNkM4UTh4
UEcyQjkrMFk2UGVBL1JZdGg2WDI3c0EKLS0tIGV5VkRWYWpXZXZGN1FXNmhTSmR5
cjQ0TWg0TkFmMnIyMDNBNVZXa1l0REUKVHdvbXFwLgMECi2JLg8aoYIkOWHwYyZh
v3mVVaaki5KceuyUhcGsdthNadrM0RDi89uUw8O2cfADdDgLlha/iQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnemkvTUxHZDNxYmJ4eGRK
Y09laWNnN0pXN1VHQzUzSWlJZGFqcmkwTWdJCjkxZG9pVVVzWXBTU0lpL0JFR2w3
TWhPTDNFSXhPTkVMVUpOY0xHMlN6RkkKLS0tIDB3d0ZyRVpGT3YzSXIrM1pPQ1Uz
QkpQYXRWaGtabnU5c1RpeE9vb0MwMWcKVRnpepDJ8a2ECD5uaK6O3fMKwWcLJP8a
uFWds5rvwDfrM1aNLXYbPIfXtqLx4fbxkoRZvHcuZ2/lYLDBFBrFlQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-28T14:02:13Z"
mac: ENC[AES256_GCM,data:NDjBNtSzGdm0+SqAvaUlltljXcyXzLQx3b9RUWZIlS/D3ox1BTO6t3t4WT8szbSnKcT51mJGJ8EIqppNWkmOpEaMNGdMw70DmGnf/IJAzScTpRyvhfsGlohCVaekMBjF+wg1uoB91vSKcTFq/wfUW79KICmxMF6woavRQ94yfQs=,iv:1f9sGKyTITSslqDXB3Khk/OGiPaJM2BreSEG6aTbZDs=,tag:UulDl/JQamg/p/q4esvtIA==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4