4 Commits

Author SHA1 Message Date
f5e23bea0a DNS Phase 4 COMPLETE: single zone — managed zone removed
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 works: with 0 managed-zone records, the Mac resolves all
hubris.network names by forwarding to Technitium (192.168.8.2). iPhone
confirmed on cellular (no LAN path -> proves mesh-forward).

The first deletion "failure" was a misdiagnosis: the old 0.68.3 resolver
cache held stale answers and wouldn't clear on down/up (needs daemon
restart); the Mac's dual LAN+mesh paths muddied it. A direct
dig @100.122.255.254 of an unsynced name had shown forwarding working.

Done:
- Deleted all 23 NetBird managed-zone A-records.
- Removed the */10 dns-sync cron. Kept /opt/dns-sync/sync.py + token +
  pre-deletion backup as an emergency-restore tool only.

End state: Technitium is the single DNS source. Mesh peers forward to it
(Core route -> 192.168.8.0/24); LAN/household query it directly. No replica,
no sync. Requires mesh clients on 0.71.x+.

Docs: dns.md + 107-dns.md updated to single-source; subdomain recipe no
longer references the sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:49:36 +02:00
0ff592d237 DNS Phase 4: attempted single-zone, reverted (managed zone load-bearing)
Deleting the NetBird managed-zone replica broke mesh-peer DNS: the Mac
(NetBird 0.68.3) could not resolve hubris.network via the home-lab-dns
nameserver group (-> 192.168.8.2) even though it shows "Available" and the
192.168.8.0/24 route is present. Forwarding to the routed-LAN IP does not
actually serve queries on the current client. Restored the managed zone via
dns-sync.py and re-enabled the cron; resolution recovered.

Correction to Phase 2: the route fix delivered roaming-peer *service
connectivity* (the real iPhone win) but did NOT enable DNS forwarding. The
original "NetBird won't forward to Technitium for mesh peers" finding
stands; managed zone + sync are retained as load-bearing.

To finish single-source later: upgrade clients to 0.71.x, or point the
nameserver group at a mesh-native DNS IP (join CT 107 to the mesh).

Docs: dns.md + 107-dns.md corrected to reflect retained managed zone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:33:33 +02:00
f9f2e8ab5e DNS Phase 2: fix roaming-peer resolution via route distribution
Root cause of "NetBird won't forward to Technitium" was NOT a nameserver
bug — it was a missing route. The home-lab-dns nameserver group
(-> 192.168.8.2, domain hubris.network) was applied to all peers, but the
192.168.8.0/24 route (home-lab-network resource) was distributed only to
the Services group. Roaming peers (Core: iphone + laptops) had no route to
192.168.8.2, so forwarding silently failed (Networks: -).

Fix: added Core to the home-lab-network resource distribution via the
NetBird API. Roaming peers now get the subnet route + the already-applied
nameserver forwarding -> *.hubris.network resolves off-LAN. Also grants
roaming devices full homelab service access. No CT 107 mesh-join needed
(original Phase 2 hypothesis obsolete).

Docs: corrected dns.md + 107-dns.md root-cause claims. Managed zone kept
as fallback pending Phase 4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:28:08 +02:00
85457095e4 DNS single-source Phase 1 + decommission Tailscale fleet-wide
Phase 1 of the DNS-redundancy cleanup (keep NetBird, collapse toward one
zone) — the safe, mesh-independent half:

- Every homelab LXC now resolves via Technitium (192.168.8.2). Fixed 8
  boxes on a dead resolver (.180), the router (.1), or Tailscale MagicDNS
  (100.100.100.100): 101,102,104,105,106,114,119,126.
- Removed the redundant /etc/hosts auth/mcp/secrets overrides (Technitium
  returns identical-or-better answers); disabled hubris-hosts-override.
- Net effect: on-prem DNS (LXCs + household via Fritz!Box->Technitium) is
  now NetBird-independent, so dropping the managed zone later can't break
  on-LAN resolution. Phases 2-4 still pending.

Tailscale decommissioned (was legacy/being-phased-out):
- Removed from the 6 LXCs still running it (101,103,104,105,114,119):
  logout, disable tailscaled, apt purge, state cleared.
- inventory.yaml: dropped tailscale from accepted + all mesh blocks;
  regenerated hosts/*.yaml (also pruned orphan authentik/claudio-bot).
- Tightened secrets-issuance MESH_SUBNETS: removed the now-vestigial
  Tailscale CGNAT range 100.64.0.0/10.
- Updated narrative docs (mesh, dns, network, README, AGENTS,
  agent-enrollment, homelab-context, 105-apps, 107-dns).

Live infra changed on the fleet + Mac; this commit records the docs/inventory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:14:07 +02:00
57 changed files with 349 additions and 1575 deletions

View File

@@ -24,8 +24,7 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
- path_regex: ^secrets/gitea-pat\.yaml$
# Write-scoped Gitea PAT (dtoro user). Same recipient list as hello.yaml
@@ -35,8 +34,7 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
- path_regex: ^secrets/gitea-tokens\.yaml$
# Workstations only.
@@ -57,8 +55,7 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
- path_regex: ^secrets/netbird-authentik-oidc\.yaml$
# Authentik OIDC client secret for the netbird-dashboard provider.
@@ -68,8 +65,7 @@ creation_rules:
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs,
age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
- path_regex: ^secrets/netbird-pat\.yaml$
# NetBird API Personal Access Token. Consumed by the dns-sync job on the
@@ -90,23 +86,5 @@ creation_rules:
age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6,
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
age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
# 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
with their mesh addresses, roles, and service mappings. Treat this file as
authoritative; anything you read in narrative pages should agree with it.
- `/opt/homelab-context/infrastructure/mesh.md` Tailscale → Netbird state.
Both meshes are accepted today; Netbird is preferred for new traffic.
- `/opt/homelab-context/infrastructure/mesh.md` — Netbird mesh state.
Tailscale was decommissioned 2026-06-21; the fleet is Netbird-only.
- `/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
and to mesh addresses off-LAN.
@@ -83,12 +83,7 @@ Grep is fine for browsing or when MCP is unreachable.
- **Wiki updates**: same-session rule applies to any meaningful state change
this client makes.
## 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
## 6. Auto-setup mechanism
The homelab-context repo ships tooling that gets automatically installed
on every client after `git pull`. This is handled by `tools/post-pull.sh`
@@ -109,7 +104,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.
## 8. When in doubt
## 7. When in doubt
Run `homelab mcp search_docs <query>` or `homelab mcp get_host <name>`.
The clone is the fallback; MCP is the index.

View File

@@ -1,33 +0,0 @@
# 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 |
| 103 | [paperless](containers/103-paperless.md) | 192.168.8.130 | Document mgmt |
| 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 / PlantUML / Portainer / WriteFreely) |
| 105 | [apps](containers/105-apps.md) | 192.168.8.205 | Docker host (Artifacto / Booklore / PlantUML / Portainer / WriteFreely) |
| 114 | [nextcloud](containers/114-nextcloud.md) | 192.168.8.224 | Personal cloud |
| 118 | [elementsynapse](containers/118-elementsynapse.md) | 192.168.8.239 | Matrix Synapse |
| 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 |
| 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 |
| 130 | [grimmory](containers/130-grimmory.md) | 192.168.8.213 | Digital library (Grimmory — fork of Booklore) |
| 126 | [plato](containers/126-plato.md) | 192.168.8.190 | Plato (notes/discovery workspace) |
### Cross-cutting infrastructure
- [DNS — split-horizon](infrastructure/dns.md)
- [Ingress — Caddy + VPS traefik](infrastructure/ingress.md)
- [Mesh — Tailscale → Netbird migration](infrastructure/mesh.md)
- [Mesh — Netbird overlay](infrastructure/mesh.md)
- [Monitoring — Hermes health watchdog](infrastructure/monitoring.md)
- [Media permissions — `media` GID 10000](infrastructure/media-permissions.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)
- **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
- **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)
- **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)
## What it does

View File

@@ -45,9 +45,6 @@ LXC has `/etc/hosts` override mapping `auth.hubris.network → 192.168.8.175` (r
## 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
Initial documentation.

View File

@@ -1,6 +1,6 @@
# 105 — `apps`
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.
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.
## At a glance
- **Hostname:** `apps`
@@ -15,6 +15,7 @@ Docker host for everything that doesn't justify its own LXC. Currently runs Arti
| Hostname | Container | Backend port | Notes |
| --------------------------------- | ---------------- | ------------ | ----- |
| `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). |
| `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). |
@@ -45,6 +46,11 @@ Receiver at `/opt/artifacto-deploy/` (outside the app repo): `deploy.sh` + `webh
### 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 `://`).
### 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/`)
FastMCP server (Python venv at `/opt/homelab-mcp/.venv`). Reads from
`/opt/homelab-context/` (this LXC is itself an enrolled
@@ -81,9 +87,10 @@ same key. Mesh+LAN source-IP gated via the `MESH_SUBNETS` env in
- `/revoke` is admin-token-gated by `/etc/secrets-issuance/admin-token`;
shreds the local key file and adds the hostname to the denylist.
Called by `homelab client remove`.
- Trust subnets today: `100.122.0.0/16` (Netbird), `100.64.0.0/10`
(Tailscale), `192.168.8.0/24` (LAN). Tighten if the LAN gets
untrusted devices.
- Trust subnets today: `100.122.0.0/16` (Netbird), `192.168.8.0/24` (LAN).
(The legacy Tailscale CGNAT range `100.64.0.0/10` was removed from
`secrets-issuance`'s `MESH_SUBNETS` on 2026-06-21 when Tailscale was
decommissioned; service restarted.) Tighten if the LAN gets untrusted devices.
#### Auto-deploy pipeline (secrets-issuance)
Receiver at `/opt/secrets-issuance/secrets-issuance/deploy/webhook/`,
@@ -107,9 +114,6 @@ Native OIDC via `[oauth.generic]` in `config/config.ini`. `host = https://auth.h
## 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
Two new services from the [homelab-context distribution plan](../infrastructure/homelab-context.md):
`homelab-mcp.service` on `:9810` (MCP read+management surface) and

View File

@@ -24,12 +24,14 @@ 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.
## Who points here
- **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.
- **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).
- **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 — 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.
- **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.)
## dns-sync (Technitium = authoring source)
`/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).
## dns-sync (RETIRED 2026-06-21 — Phase 4 complete)
**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.
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
@@ -48,9 +50,6 @@ Replaces the DHCP that was previously served by the Slate AX router. Static-IP L
## 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)
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`.

108
containers/126-plato.md Normal file
View File

@@ -0,0 +1,108 @@
# 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.

View File

@@ -1,48 +0,0 @@
# 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.

View File

@@ -1,51 +0,0 @@
# 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

@@ -1,65 +0,0 @@
# 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

@@ -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 |
| 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 |
| 105 | [apps](105-apps.md) | 192.168.8.205 | priv | 2 | 4 GiB | 30 GiB | `/mnt/library` | `docker` / `artifacto` / `blog` | running |
| 105 | [apps](105-apps.md) | 192.168.8.205 | priv | 2 | 4 GiB | 30 GiB | `/mnt/library` | `docker` / `books` / `artifacto` / `blog` | 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 |
| 119 | [sophia](119-sophia.md) | 192.168.8.157 | priv | 2 | 1 GiB | 10 GiB | `/mnt/library` | — | running |
@@ -15,9 +15,7 @@ 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 |
| 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 |
| 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 |
| 126 | [plato](126-plato.md) | 192.168.8.190 | priv | 2 | 2 GiB | 8 GiB | `/mnt/library/documents/plato` | `plato.hubris.network` | running |
## Recently destroyed (kept for archaeology)
@@ -27,7 +25,6 @@ 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 |
| 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 |
| 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) |
| 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 |

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
# 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,14 +8,10 @@ role: matrix-server
host: hubris
pve_id: 118
lan_ip: 192.168.8.239
mesh:
tailscale:
fqdn: elementsynapse
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
public_host: matrix.hubris.network
runs:
- matrix

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
# 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,7 +14,6 @@ mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
mounts:
- /mnt/library
ssh:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +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: auth-outpost
name: plato
kind: lxc
os: linux
role: authentik-gateway
role: app
host: hubris
pve_id: 106
lan_ip: 192.168.8.6
pve_id: 126
lan_ip: 192.168.8.190
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
notes:
- Runs Authentik outpost (reverse-proxy/SSO enforcement) for protected services
mounts:
- /mnt/library/documents/plato
public_host: plato.hubris.network
runs:
- plato
services_hosted:
- name: plato
url: https://plato.hubris.network
backend: plato
see_also:
- containers/106-auth-outpost.md
- containers/126-plato.md
mcp_endpoint: https://mcp.hubris.network/mcp
secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
# 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,7 +12,6 @@ mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
public_host: zimaos.hubris.network
runs:
- zimaos

View File

@@ -23,8 +23,7 @@ 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.136` ([mule-images (120)](../containers/120-mule-images.md))
- `192.168.8.77` ([hubris host](../hosts/hubris.md) — backup-library)
- ~~`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)
- `192.168.8.190` ([plato (126)](../containers/126-plato.md))
**Don't strip these when editing app.ini.**
@@ -38,12 +37,11 @@ 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/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/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/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/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/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/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.
> Each owns its own clone on LXC 105. They don't conflict because each
@@ -59,7 +57,7 @@ Always commit + push. Local-only edits drift. Common ones:
- `/var/lib/gitea/custom/``dtoro/gitea-customizations` (auto-deploys)
- `/opt/artifacto/``dtoro/Artifacto` (auto-deploys)
- `/opt/mule-image/``dtoro/mule-image` (auto-deploys)
- ~~`/opt/plato/app/``dtoro/Plato`~~ (destroyed 2026-06-28)
- `/opt/plato/app/``dtoro/Plato` (auto-deploys)
- ~~`/opt/claudio-bot/``dtoro/claudio-bot`~~ (destroyed 2026-06-04)
- `/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))
@@ -74,6 +72,11 @@ Always commit + push. Local-only edits drift. Common ones:
- 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.
### 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
- 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.
@@ -120,12 +123,6 @@ If you're not sure what's already lurking, run `homelab apt-audit --fleet` and l
## 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
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
- **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`. 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).
- **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).
- **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.
- **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.
- **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.
## Live entries (as of 2026-06-04)
@@ -32,6 +32,7 @@ address=/blog.hubris.network/192.168.8.175
address=/photos.hubris.network/192.168.8.175
address=/photos-new.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=/nfs-export.hubris.network/192.168.8.200
```
@@ -49,11 +50,11 @@ Creating a new Caddyfile site block is necessary but **not sufficient**. Without
## 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).
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.
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.)
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`.
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.
> 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).
> 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).)
## Public path — what does and doesn't follow the LAN map
@@ -75,8 +76,53 @@ Either:
## Changelog
### 2026-06-28`plato.hubris.network` removed
Plato (LXC 126) decommissioned. Technitium entry deleted; dns-sync cron reaped the NetBird managed zone record.
### 2026-06-21DNS single-source, Phase 4: COMPLETE — managed zone removed, Technitium is the single source
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).
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.
**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
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,9 +64,11 @@ The MCP server and secrets-issuance each have their own clone
- Both services bind `0.0.0.0:<port>`. The trust boundary is
`MESH_SUBNETS` in the service's environment + nftables (planned). Today
`MESH_SUBNETS=100.122.0.0/16,100.64.0.0/10,192.168.8.0/24` — Netbird +
Tailscale + the homelab LAN. Adjust if the LAN ever has untrusted
devices.
`MESH_SUBNETS=100.122.0.0/16,192.168.8.0/24` — Netbird + the homelab LAN.
(The legacy Tailscale CGNAT range `100.64.0.0/10` was dropped 2026-06-21
when Tailscale was decommissioned; secrets-issuance restarted. Only
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:
`mcp.hubris.network``192.168.8.205:9810`,
`secrets.hubris.network``192.168.8.205:9820`. Off-LAN clients on
@@ -125,7 +127,7 @@ The MCP server and secrets-issuance each have their own clone
step-by-step for adding a new client
- [Auto-deploy](auto-deploy.md) — the `homelab-mcp` + `secrets-issuance`
pipelines (and the rest of the lab's webhook pipelines)
- [Mesh](mesh.md) — Netbird / Tailscale paths and the `192.168.8.0/24`
- [Mesh](mesh.md) — Netbird paths and the `192.168.8.0/24`
network resource
- [Apps (105)](../containers/105-apps.md) — where both services run
- [Gitea (104)](../containers/104-gitea.md) — the source of truth

View File

@@ -42,8 +42,6 @@ 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` |
| `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).
@@ -87,9 +85,6 @@ No cert-mirror entry and no `hubris-public-cert-sync.sh` mapping is needed for `
## 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
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).
- Shared subtrees owned `:media` mode `2775` (drwxrwsr-x, setgid):
- `movies`, `tv`, `music`, `anime`, `podcasts` — jellyfin libraries
- `audiobooks`, `audiobookshelf-metadata`, `books`, `comics` — audiobookshelf / grimmory
- `audiobooks`, `audiobookshelf-metadata`, `books`, `comics` — audiobookshelf / booklore
- `downloads` — \*arr stack output
- `images` — photoprism / immich / mulita
- `roms` — emu frontends
@@ -61,7 +61,6 @@ Every LXC that mounts `/mnt/library` participates in a shared `media` group with
| 119 | [sophia](../containers/119-sophia.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 |
| 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).
@@ -69,7 +68,7 @@ Config backups: `/root/101.conf.bak.*`, `/root/109.conf.bak.*` (109 destroyed 20
## Gotchas
- **[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.
- **[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.
- **`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.
- **`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 — Tailscale → Netbird migration
# Mesh — Netbird
The hubris fleet is migrating from Tailscale to Netbird. Netbird is the target end-state. In-progress as of 2026-04-21.
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.)
## 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.
- **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.
- **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.
- **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).
## ICE / STUN / TURN
@@ -45,6 +45,8 @@ Pre-migration, the bundled `netbirdio/netbird-server` combined image silently ig
## 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`.
Two techniques. Pick by HTTP-client behavior.
@@ -90,7 +92,7 @@ pct set <id> --nameserver "127.0.0.1 192.168.8.1 1.1.1.1"
## Adding new LXCs
- 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. If nameserver is `192.168.8.1` or `100.100.100.100`, add the hosts override. If it's the netbird daemon IP, skip.
- 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.)
## Long-term fix

View File

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

View File

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

View File

@@ -11,8 +11,8 @@
# ("external" is reserved for hosts the homelab CLI manages via ssh but
# that aren't homelab clients themselves — e.g. the IONOS netbird VPS
# with no /etc/age/key.txt and no /opt/homelab-context clone.)
# - `mesh:` lists addresses the host is reachable at. Both `netbird` and
# `tailscale` are accepted during the migration (see infrastructure/mesh.md).
# - `mesh:` lists addresses the host is reachable at via `netbird`.
# (Tailscale was decommissioned 2026-06-21 — see infrastructure/mesh.md.)
# Prefer netbird FQDNs over raw IPs.
# - `age_pubkey:` provisioned by secrets-issuance on first bootstrap and
# committed back via `homelab client add --finalize-pubkey <key>`.
@@ -32,7 +32,6 @@ mesh:
primary: netbird
accepted:
- netbird
- tailscale
netbird_subnet: 100.122.0.0/16
netbird_domain: netbird.selfhosted
services:
@@ -69,16 +68,15 @@ services:
photos:
url: https://photos.hubris.network
backend: mule-images
plato:
url: https://plato.hubris.network
backend: plato
arr_stack:
backend: arriman
note: jellyseerr / qbit / sab on docker compose
artifacto:
backend: apps
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:
url: https://zimaos.hubris.network
backend: zimaos
@@ -118,27 +116,6 @@ hosts:
mounts:
- /mnt/library
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:
kind: lxc
pve_id: 101
@@ -147,9 +124,6 @@ hosts:
role: media-server
lan_ip: 192.168.8.206
public_host: media.hubris.network
mesh:
tailscale:
fqdn: jellyfin
mounts:
- /mnt/library
age_pubkey: ''
@@ -168,9 +142,6 @@ hosts:
role: document-archive
lan_ip: 192.168.8.130
public_host: paperless.hubris.network
mesh:
tailscale:
fqdn: paperless
mounts:
- /mnt/library
age_pubkey: ''
@@ -183,9 +154,6 @@ hosts:
lan_ip: 192.168.8.121
public_host: git.hubris.network
backend_port: 3000
mesh:
tailscale:
fqdn: gitea
mounts:
- /mnt/library
notes:
@@ -200,10 +168,6 @@ hosts:
lan_ip: 192.168.8.205
public_hosts:
- artifacto.hubris.network
mesh:
tailscale:
ip: 100.121.171.122
fqdn: apps
mounts:
- /mnt/library
runs:
@@ -211,27 +175,7 @@ hosts:
- plantuml
- homelab-mcp
- secrets-issuance
# booklore removed 2026-06-29 → migrated to grimmory (LXC 130)
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:
kind: lxc
pve_id: 114
@@ -240,9 +184,6 @@ hosts:
role: file-sync
lan_ip: 192.168.8.224
public_host: cloud.hubris.network
mesh:
tailscale:
fqdn: nextcloud
mounts:
- /mnt/library
age_pubkey: ''
@@ -254,9 +195,6 @@ hosts:
role: matrix-server
lan_ip: 192.168.8.239
public_host: matrix.hubris.network
mesh:
tailscale:
fqdn: elementsynapse
sophia:
kind: lxc
pve_id: 119
@@ -264,9 +202,6 @@ hosts:
os: linux
role: workshop
lan_ip: 192.168.8.109
mesh:
tailscale:
fqdn: sophia
mounts:
- /mnt/library
age_pubkey: ''
@@ -278,9 +213,6 @@ hosts:
role: photo-management
lan_ip: 192.168.8.136
public_host: photos.hubris.network
mesh:
tailscale:
fqdn: muleimage
mounts:
- /mnt/library
age_pubkey: ''
@@ -308,28 +240,21 @@ hosts:
- jellyseerr.hubris.network
- qbit.hubris.network
- sab.hubris.network
mesh:
tailscale:
fqdn: arr
mounts:
- /mnt/library
age_pubkey: ''
grimmory:
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent
plato:
kind: lxc
pve_id: 130
pve_id: 126
host: hubris
os: linux
role: book-library
lan_ip: 192.168.8.213
public_host: books.hubris.network
role: app
lan_ip: 192.168.8.190
public_host: plato.hubris.network
mounts:
- /mnt/library
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: '' # filled by homelab client add --finalize-pubkey
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent
# 126 (plato) — destroyed 2026-06-28, notes workspace decommissioned
- /mnt/library/documents/plato
age_pubkey: ''
zimaos:
kind: vm
pve_id: 100
@@ -345,9 +270,6 @@ hosts:
os: linux
role: home-automation
lan_ip: 192.168.8.101
mesh:
tailscale:
fqdn: homeassistant
republic-laptop:
kind: workstation
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) |
| OS is Linux or macOS | bootstrap detects via `uname -s` | `uname -s` |
| 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` |
| 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` |
| `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`) |

View File

@@ -1,107 +0,0 @@
# 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

@@ -1,443 +0,0 @@
# 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

@@ -1,246 +0,0 @@
# 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,9 +6,6 @@ Pre-flight runbooks for planned changes that haven't happened yet. Once executed
| 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 |
## Conventions

View File

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

View File

@@ -9,47 +9,38 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFMWU0K3V5NmZBNUk2TWNO
NmdRWjlBV3p5dWNhUWhTOFlEdCtkMVh5STJBCkwzbUNkenJjei9TVWVUMi81d0ZX
b3dTTnJudVZiV0YzajNPcXlUWUtxdWMKLS0tIEo1eUR3dmJQRWM0bHRtMFc5Q290
QmwyMk44SVBXQ0k5QjN3ZWtNS1FSWTgKcc/F1HfMfnwun+mIUq1Ds+DZPk7F1ohe
OIVt5aJliApDYesRJ14K1ZEdi4YqCqO+1cLi69iWKHNT3PcOYrepUA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUanM5MzFZYnkxVHVHSlYy
U1Q1ejNJb3ZORklKM2xjK0p2cytRU0NNRUZJClBTUDJ5dFM2eUFjV0t1YzBCcXcv
cmE3czJST1RuMHJVN0tmVTZNdGNRS1kKLS0tIFUydktvK0tBU2U3czRhWHVySGZR
ck9KTVR6RS9pOHQ4WkZneG1Tbk5OSWMKwOQ+CWkuLSqfhle9fgDw4XXIp0ojZssw
9YWK/suEAb6u9nzbw7zuEmZxhhDV0Y61UAeSSbSmygy7MD7dvxrvgQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFNVUyajV1NFo5VFRyMDlG
dWFlRlVJZDlJTHdYOFJLRi81OU9BdkI2RG13ClMwLzFHQ0VxSXJzQ3V2S2pjVFRS
dEQvK1VMZ3FDZDkxSHdYb3NHeHE4QTgKLS0tIDNyZnV5OFVQNXVEUUlzK3RYMytt
Nnl5bTZuMWFLY28vZnBPWlFEYmFyK00KtwcrF0W91FIi+9nn8rF2G8xP2/ca2h3R
DagqhSmpaEC7QHtYGP9SAChhBrDuWdVtK0Gdbc4m/31gW7Fh815zfA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCbm5MOG1Bd0FZbVZTbUU1
VGpGVWNnUk03RURPNURNNjdQejY2WkxkNzBRClpuMGFaQzZhaENJckZvbUI3VmUy
ekV0eFMza1p4S2w5L0hvNndjN3RpdTQKLS0tIDRTdExxTlZQK0FKV294dnh4dmd4
a21yK0VabzhOcDhRajlxbVE1dTdtcWcKQorqkwUs7lppaDJuCDn+KtH+76xPIvsr
9axkrDLumeY9LtDhunlaEpax7zdvvAiC4DzJdbJX0LgFCaurtgubOA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTlFZYVFHZHNCUkllZC94
ZUFaS3JZTmdPclA4dlR0QklxMmVxNWNyZUhvCnVoY0YvWGlsbm9CaURkQzZUMTRs
ZVNmcDJxZWN2WG9nUUNtT0I4S1FIem8KLS0tIHBRWFhLN3p2Z0ZaWDEyeEw4WUhs
Z0RwQ290OWloQzh1eXVLenlrOTZyRzQKQEnY4KC8ReKGFDzklK/A6uIGMRIhMfLW
IzogPQiWYcDerMDd14kktBOcbjKRkI9gPSDZdmieS9z/wA5J37DNNA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaS1JrL3lpa1c4QUwxWlZT
VWQ1ZDVPVjN6QTdFNGQzNkV5WllOK3dYWHhzCllTbVdzRVFTbDZ4TU9PZWpKNnJu
cWJ6YWs5amVsR2xZZFpaQlErOXBKZVEKLS0tIFFzVG9QWmdNWGl5UEkxaXZvMUxx
SExXL1gxNzVud2JrZ04zTnI4cUlNSVEKPF2zoSnYEt/zeG8QW1454Mcr8u8JTCl7
jDyghcRw94enbPbA43zsKGn6QALQ40PfXmZ/MlEbdnf6U3zI7zvygQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCSUlxdTFhR1dxaDU2MGdL
dVJoYVQwZm9DbEVOYXd4Ymd0dk56R283WjJFCi9MS05CVkxYY2NPc0pjeGNacm8w
ZDZGYUMrdDdKS05BckhoRDl4OTVxYjgKLS0tIHlPaUg4Yk96UVJacDFhSm1FaGZo
RVVSWVRteGcwN2dHOVhuWFpmU0p5aEkKXetFyc9PKB6dCljl0c/+JJrEyDvYJP0Y
1fbPf7WBJsLTaQgrjsGHU4wqlg0Se5GHMeriOPYaRjiL9locKW2SiQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwcWxaenJybWpqQ0RpM3ov
cEszZDI4YkswY1hpUmZDcmZ1Ky84N0xUUlh3CjJLcm5nVE0rSHhyeEhNSmFIaE00
RkpqVHFEdVR5VmdETnhnQ0JiSlFyNTQKLS0tIGNXdkMzaHBtTWU3azdDVm15NkpZ
TWlYZXdBYTROcHBqMCtFcEtJRUJtOEEKdBbB5a5teUnvIYaLJaLp3KZkEXhnkFor
pbnL/WrHQViLdu8pJ5nscIO9ryQt7dS6aAu6gP3Tbs7XdREOTFgKmg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwMlVSajFiT2loclFzeEQ5
ekxqUjBkSEh5M2FJMXA2VGkycllYa1R1Qm40Clh0L0RwSzZqeXlQVXMvRVlsZ05L
RHkyMlJlc1BmV0tjQk1uaWJLemR2cmMKLS0tIDlXT1dhRkpUdDRpRkJhL2NHSjN5
QU5Pc25lSUV4OWROZENXQzB4Y0o2Y1kKzhOY5jN4gi+u5tl9rAHGRb8Bh9DESl1K
qNiD+wauApal8iRrIkGdGsrWrWThSwy7vjnUsoB8JDrBaqLj2usuFA==
-----END AGE ENCRYPTED FILE-----
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]

View File

@@ -1,50 +0,0 @@
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,47 +8,38 @@ sops:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxZjBPQ3M3NkJwZ2dDZlA3
MGR2cTM1b0NBN2ZETFJZUW95M2lITXo4dlNJCjY5NEMrc2xNaEpCVUtkVHJYa1Rr
citGL3ZQQnlMR3BpVFpmU3d0eEhiZG8KLS0tIFZGejF0UDJZTUVUMU9KL2hySm9L
aEx2ZWRuTldDdFJrNUlmMHZVay9vRnMK0XMd/S3VSdD71eh2MUJGs8c7Jdxmfpua
p4ZN4Y4MARz2XMA8xqDpw1SrMevaceD5+p+R+zhunb99aFfhtV7WxQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvdUxUdzd2VmN1MWU2dXdM
N0FWcXkveVdHYk5aYzFrOVZaRzVYZWs1RFZBCkFwRy9xVFBrVjU0NmhkMlpyN0pp
ZlZKenhoNFVRdGg4Q0ZTMS9YUDFKMHcKLS0tIG1GYkZaYkhJNk5zdWZBMEtNL041
MWhEQ1RmQ1k0d2Y2dzh2YnRvRm1IOEUKJ64/tNyLe2qBIL4CetRlpLaxhfOL93D5
BjeHLwtfXB2fNCJSdCY8H4KsQP+epwriUQPtZVId32n9xDEt71obSw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3aXdya1RpSGdsQVNMYXlQ
cFhCZDUvTnFUMWdpQzAreGprTm9ZOE8vZUI4CmF4K2ZGQ29QOU1kMlhUV3N6RUNk
bzJwZDNmR2NITUlzY2hSekozdDBsQzAKLS0tIHI4cy9IR1gvWVo1emcxbzFURWVM
YzJvRjlNeE5KancrUUtSZndQNWxhYkEKAedJLVpc47R8rgm4YRyT7Z9G4tfbBnqQ
c1UObcIa551wR79n7vJvVb2cSLz3VEURe6sNi4OqJmg017qrMOS5Wg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRNi9kMkt5Vk4rS0pqY2ZP
ODErMlRxNmlWaHBvYjNaQ3lRT1RSQitMVlFBCk5HQ2o0QUg4dWFodTNEVGJFTlF0
Y2JIVEFPZGVUdXpNS2lGOVQzeFRvMFEKLS0tIGtQY0dicE91R0J3eEM3bEdMZHJ4
TndiYVFQWGZyRkF1bWYrRlZ6N0JBRm8KEUKY3chev13KjnGKdTR8tvyYV3s0W1rI
8LeJSIocSX58PexqgcKsT8pGpuIiOetEzjzv2WPa0MzEwtFCVgwKYQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrazAvYnovQ3ZFQ3YvcTh3
eGhYdFFTZ2ZkTElzVmFta3ROMGpCMlE3VEVvCjNXd2x2QTREQ2NVMEk5OU1JY2Z6
NHQzQVVaMVR0bWM4R0lxVVpwTEY4YXMKLS0tIHFTT1dRejNyQlVoUEREUnhlM09V
ZEF5WFlnYmgyVDRUYXBTVTgyV2laWkkKdE7hAUxtDZJt8P4LrfOomK8rMTlXeXd0
fyY29wRnXqn8s0IkpibWkByWLgyXHe1nNHHgsiPsxYMFbCRW1iwZoQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFcktzeGx5VWZaeFVNc2Mv
aWt1dzNUVko5YytOTTdxMWVEVm85N29TMDBrClc3ODdVVDRVRFI3K3BiUGd1TFp1
SE5nalY0RkwzNmZQOWU1THhGQ3Q0UkUKLS0tIHNZdHhxdDY0a1AyY0g3WUx3bUJh
U0RqRWRFRXNLTWFwemNOYk8vTmxZeTAKaZI6WSNM022xtZQx5yOYXj5mesAjWsu/
ZIDG6MayoyrhwAkuZsWOKtKp6zbRCcnm7s9OHdlNsx7PWG1ODEFpOQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNdzIxYUlkY1YvTVdrREgz
bEhrTlpXRDFST2MxTUFpSWwrMzF6ZTVsUFRNCnB5WVJKaHNFL0hQak9ZbTEwNjF6
YmFEdVJ2YkEyS3BDQkUwZjBJNXh4UUUKLS0tIEpIcGV4N1R6aUU5S0l5dGZOdnNV
Njh1WlAwR2tsTXZoUS9JMmlYZm1ZUzQKcqXYn1KgSzAXS+m13/wLmeriNf+fTzY6
TSBcB3OxgqvYAWwZutXr6UScVauZPuatSjwE1va4HJQuhSVoCGwqcQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4blY0TEdsU3VoSmR3M0FK
dlg4S1JCRVpZRnc4SURNaWdSUjVQdnpCazJnCjhobHN2dmpGZzhrMWs4UUFFdVF6
RmtJb0VaQlFIeGhOMFA3Ui9iVmI1K2cKLS0tIHk3M3RhaUtSSm5vajFJUUprRzNa
c3JiWC9WNVRYOXJIQmMzUHZxbUZtS0EKsE5cG85lsDFABMbR/A80TQMlurf8qHDR
tVxZuMQPIXBdg6Ov72xnQeMGC//apWO40gnfjGn+P3oZGy7m8XJBuw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVbFFxSkhscUV2bm93Z01O
WDl3MU11K2lrNzAwUVN5MDhodDlveFZ6QlYwCjNaNjd3bG9QMTQxcVA5TTNQU3Za
bWdZTDdGZzJObnJ6NlBxKzVYcmpVNk0KLS0tIEJTcFVTRmkxQlJWSzVIM0ErOVNN
c290WkI1ZzBlYkg3QzJJUlQwOFZPYmMKQFp4vqFwh0GpYOur4kGMCeENnuNZnN0x
sbNmM9rRm/10T4tHF9aR2/WIjEqm6M6+yXd3phhB8rIggDPlVk2bCg==
-----END AGE ENCRYPTED FILE-----
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]

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
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