diff --git a/.hermes/plans/2026-06-03_223218-dhcp-pool-exclude-static-ips.md b/.hermes/plans/2026-06-03_223218-dhcp-pool-exclude-static-ips.md new file mode 100644 index 0000000..3d0d9b6 --- /dev/null +++ b/.hermes/plans/2026-06-03_223218-dhcp-pool-exclude-static-ips.md @@ -0,0 +1,246 @@ +# Plan: Narrow Technitium DHCP Pool to Avoid Static-IP Conflicts + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Eliminate the IP conflict risk created by the Technitium DHCP pool (`.100–.240`) overlapping with all static LXC/VM IPs (`.101–.239`). + +**Architecture:** Shrink the DHCP pool range on Technitium so it only covers IPs that no static host uses. No LXC/VM IPs change. Single server-side change (Technitium API), plus documentation updates. + +**Tech Stack:** Technitium DNS API (`/api/dhcp/scopes/set`), bash/curl, homelab-context repo for docs. + +--- + +## Problem statement + +The Technitium DHCP server on [CT 107](containers/107-dns.md) serves `192.168.8.100–192.168.8.240`. **Every static homelab IP except hubris (`.77`) sits inside that range:** + +| Host | IP | Inside pool? | +|---|---|---| +| hubris (Proxmox) | .77 | No — below `.100` | +| haos (VM 108) | .101 | YES | +| gitea (104) | .121 | YES | +| paperless (103) | .130 | YES | +| arriman (122) | .132 | YES | +| mule-images (120) | .136 | YES | +| sophia (119) | .157 | YES | +| mac-mini | .174 | YES | +| caddy (121) | .175 | YES | +| authentik (124) | .180 | YES | +| plato (126) | .190 | YES | +| zimaos (VM 100) | .195 | YES | +| nfs-export (102) | .200 | YES | +| apps (105) | .205 | YES | +| jellyfin (101) | .206 | YES | +| nextcloud (114) | .224 | YES | +| claudio-bot (123) | .230 | YES | +| elementsynapse (118) | .239 | YES | + +The docs claim "Static-IP LXCs (below `.100`) are unaffected" — this is **false**. Static IPs span `.101–.239`, the DHCP pool spans `.100–.240`. They overlap almost entirely. + +If the DHCP server hands out `.121/.136/.224` (or any of the above) to a new dynamic client before the static LXC claims it on boot, the static service will fail to bind and the service goes dark. + +--- + +## Proposed approach: Shrink the pool + +**Move the DHCP pool start from `.100` to `.241`**, resulting in: +- **New pool:** `192.168.8.241 – 192.168.8.254` (14 dynamic IPs) +- **Reserved:** `.100–.240` stays for static hosts, `.2` for Technitium, `.1` for gateway +- **Zero changes to any LXC, VM, Caddy, or Proxmox config.** + +Why `.241–.254`: +- Highest static IP is `.239` (elementsynapse) — `.241` gives a 1-IP gap +- `.255` is the broadcast address (unusable) +- 14 IPs is plenty for truly dynamic clients (new transient containers, test VMs) +- If more are ever needed, the pool can easily be widened back down + +--- + +## Tasks + +### Task 1: Verify current Technitium DHCP scope from the API + +**Objective:** Confirm the active pool range matches what's documented. + +**Step 1: Log in to Technitium API and get a token** + +```bash +TOKEN=$(curl -sk -X POST http://192.168.8.2:5380/api/user/login \ + -H "Content-Type: application/json" \ + -d '{"user":"admin","pass":"'$(cat /opt/technitium/admin_password.txt)'","includeInfo":false}' \ + | jq -r '.token') +echo "Token: ${TOKEN:0:10}..." +``` + +**Step 2: Fetch current DHCP scopes** + +```bash +curl -sk "http://192.168.8.2:5380/api/dhcp/scopes/list?token=$TOKEN" | jq . +``` + +**Expected:** One scope named `homelab` with `startingAddress: "192.168.8.100"` and `endingAddress: "192.168.8.240"`. + +**Verification:** If the scope is NOT `.100–.240`, note the actual range and adjust the plan. + +--- + +### Task 2: Update the DHCP scope to `.241–.254` + +**Objective:** Shrink the pool so it no longer overlaps static IPs. + +**Step 1: Update the scope via API** + +```bash +curl -sk -X POST "http://192.168.8.2:5380/api/dhcp/scopes/set?token=$TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "homelab", + "startingAddress": "192.168.8.241", + "endingAddress": "192.168.8.254", + "subnetMask": "255.255.255.0", + "gatewayAddress": "192.168.8.1", + "dnsServerAddresses": ["192.168.8.2"], + "leaseTime": 86400 + }' +``` + +**Step 2: Verify the change took effect** + +```bash +curl -sk "http://192.168.8.2:5380/api/dhcp/scopes/list?token=$TOKEN" | jq '.response.scopes[0] | {startingAddress, endingAddress}' +``` + +**Expected:** +```json +{ + "startingAddress": "192.168.8.241", + "endingAddress": "192.168.8.254" +} +``` + +**Pitfall:** If the API returns `{"status":"error"}`, the scope name or parameter format may differ. Inspect the response body. Technitium's API might use `rangeStart`/`rangeEnd` instead of `startingAddress`/`endingAddress`. Adjust if needed (check the full scope object from Task 1 step 2 for exact key names). + +--- + +### Task 3: Check for active DHCP leases in the old pool that would be stranded + +**Objective:** Ensure no DHCP client is currently holding an IP in `.100–.240` that it will lose when its lease expires. + +**Step 1: List active DHCP leases** + +```bash +curl -sk "http://192.168.8.2:5380/api/dhcp/leases/list?token=$TOKEN" | jq '.response.leases[] | {ip: .ipAddress, client: .clientHostname, mac: .hardwareAddress, expires: .leaseExpires}' +``` + +**Step 2: Interpret results** + +- If the only leases are from static LXCs that configured themselves before the DHCP move (e.g., old leases from before the 2026-06-02 static-IP migration), these leases are stale and harmless. +- If a *dynamic* client (e.g., a test laptop, transient VM) holds `.195` or similar, note it — it will lose its IP on next renew and should be moved to a static assignment or into the `.241+` pool. +- **ZimaOS (VM 100) at `.195` is a DHCP lease, not static** — this is the one host that needs attention. Either: + - Set a static IP inside ZimaOS (preferred), or + - Add a DHCP reservation for MAC in Technitium to pin `.195` + +**Verification:** No "surprise" dynamic clients that would break on lease expiry. + +--- + +### Task 4: Fix ZimaOS IP stability (if needed) + +**Objective:** Ensure ZimaOS at `.195` won't float or break when the pool shrinks. + +**If ZimaOS already has a static IP configured inside the VM:** Nothing to do. + +**If ZimaOS is DHCP-only (likely — doc says "DHCP lease, not a reservation"):** + +Option A (preferred): Set a static IP inside ZimaOS via its web UI at `http://192.168.8.195` → Settings → Network → Static IP → `192.168.8.195/24`, gateway `192.168.8.1`, DNS `192.168.8.2`. + +Option B: Add a DHCP reservation in Technitium for ZimaOS's MAC address: +```bash +ZIMAMAC=$(ssh root@hubris "qm config 100 | grep net0 | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}'") +curl -sk -X POST "http://192.168.8.2:5380/api/dhcp/reservations/add?token=$TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"hardwareAddress\":\"$ZIMAMAC\",\"ipAddress\":\"192.168.8.195\"}" +``` + +**Pitfall:** The `/api/dhcp/reservations/add` endpoint signature is unverified — confirm the exact endpoint name from Technitium's API docs or the web UI before running it. The web console at `http://192.168.8.2:5380` → DHCP → Reservations can be used as a manual fallback. + +--- + +### Task 5: Update documentation in homelab-context + +**Objective:** Fix the now-wrong claims about static IPs being "below .100". + +**Files to edit:** + +1. **`infrastructure/network.md`** — Line 53 + - Old: `Most homelab LXCs use static IPs below \`.100\`. DHCP only covers new/transient containers.` + - New: `Static IPs span \`.101–.239\` (all LXCs + VMs + workstations). DHCP pool narrowed to \`.241–.254\` to avoid overlap.` + +2. **`containers/107-dns.md`** — Lines 37, 42, 55 + - Line 37: Update pool range: `192.168.8.241 – 192.168.8.254` + - Line 42: `Static-IP LXCs (below \`.100\`)` → `Static-IP LXCs (\`.101–.239\`) are excluded from the pool.` + - Line 55: Add changelog entry for the pool shrink + +3. **`containers/107-dns.md`** — Add changelog entry: + ```markdown + ### 2026-06-03 — DHCP pool narrowed to `.241–.254` to exclude static IPs + Previous pool `.100–.240` overlapped with all static LXCs/VMs (\`.101–.239\`), creating IP conflict risk. Shrunk pool to `.241–.254`. No services re-IP'd. See [plan](../plans/2026-06-03-dhcp-pool-exclude-static-ips.md). + ``` + +4. **`infrastructure/network.md`** — Line 51: Update pool range in the DHCP table row. + +5. **`plans/2026-06-01-slate-ax-to-sodola-migration.md`** — Line 60: Optionally update the pool range in the config table (or add a post-migration note). This is the historical migration plan, so a footnote rather than an edit may be better. + +**Commit:** +```bash +cd /opt/homelab-context +git add infrastructure/network.md containers/107-dns.md plans/ +git commit -m "docs: DHCP pool narrowed to .241-.254 to exclude static IPs" +git push +``` + +--- + +### Task 6: Verify no regressions + +**Objective:** Smoke-test that DNS and key services still work after the scope change. + +```bash +# 1. DNS resolution via Technitium +dig @192.168.8.2 +short git.hubris.network +# Expected: 192.168.8.175 + +# 2. Caddy reverse-proxy chain +curl -sI https://git.hubris.network | head -1 +# Expected: HTTP/2 200 + +# 3. All app names resolve +for name in git cloud media paperless photos matrix auth plato artifacto; do + result=$(dig @192.168.8.2 +short ${name}.hubris.network) + printf "%-20s → %s\n" "${name}.hubris.network" "$result" +done + +# 4. Technitium DHCP scope is correct +curl -sk "http://192.168.8.2:5380/api/dhcp/scopes/list?token=$TOKEN" | jq '.response.scopes[0] | {startingAddress, endingAddress}' +``` + +--- + +## Risk assessment + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| API call fails (wrong field names) | Medium | Low | Inspect live scope object first (Task 1); adjust payload | +| ZimaOS loses IP on next boot | Low | Medium | Task 4 makes ZimaOS static or reserved | +| Active DHCP client in `.100–.240` gets stranded | Low | Low | Task 3 surfaces this; client just requests a new IP from `.241+` | +| Technitium admin password file missing | Low | Medium | `/opt/technitium/admin_password.txt` was created during setup; verify existence | + +## Open questions + +1. Is zimaos (VM 100) currently DHCP or static? The doc says DHCP lease, but it's listed as `lan_ip: 192.168.8.195` in inventory. If it's actually DHCP, it's the one host that needs a static assignment before the pool shrinks. +2. Are there any transient DHCP clients (test laptops, phones) on the homelab subnet that hold `.100–.240` addresses? Check leases before cutting over. +3. Should we widen the pool slightly (e.g., `.230–.254`) for more headroom? Currently 14 IPs. If 3+ transient devices are expected, `.230–.254` = 25 IPs — still safe since the highest static is `.239` and `.230–.239` could be excluded. + +## Execution preference + +All changes are on the Technitium API + homelab-context repo. No LXC/VM restarts needed. The pool shrink takes effect immediately for NEW DHCP requests; existing leases in the old range continue until expiry (24h max). diff --git a/containers/107-dns.md b/containers/107-dns.md index 51440e2..b66adf2 100644 --- a/containers/107-dns.md +++ b/containers/107-dns.md @@ -34,12 +34,12 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur ## DHCP Technitium also runs a DHCP server for the homelab subnet (enabled 2026-06-02): -- **Scope:** `homelab` — `192.168.8.100 – 192.168.8.240` +- **Scope:** `homelab` — `192.168.8.241 – 192.168.8.254` - **Gateway:** `192.168.8.1` (Proxmox `vmbr0` alias) - **DNS:** `192.168.8.2` (self) - **Lease time:** 24 h -Replaces the DHCP that was previously served by the Slate AX router. Static-IP LXCs (below `.100`) are unaffected. +Replaces the DHCP that was previously served by the Slate AX router. Static-IP LXCs (`.101–.239`) are excluded from the pool. Pool narrowed from `.100–.240` to `.241–.254` on 2026-06-03 to eliminate IP conflict risk. ## Related - [124 — authentik](124-authentik.md) — retired host of the old dnsmasq @@ -48,6 +48,9 @@ Replaces the DHCP that was previously served by the Slate AX router. Static-IP L ## Changelog +### 2026-06-03 — DHCP pool narrowed to `.241–.254` +Previous pool `.100–.240` overlapped with all static LXCs/VMs (`.101–.239`). Shrunk via API (`/api/dhcp/scopes/set`). 11 stale DHCP leases in `.101–.110` remain until natural expiry (2026-06-04). See [plan](../plans/2026-06-03-dhcp-pool-exclude-static-ips.md). + ### 2026-06-03 — dns-sync added (Technitium → NetBird managed zone) This Technitium became the single DNS authoring source; `/opt/dns-sync/sync.py` (cron */10) reconciles named A-records into the NetBird managed zone via the API. Fixed previously-broken mesh names (`sso`, `nfs-export`, `mcp`, `secrets`) by adding them to the managed zone; reaped obsolete `files`/`photos-new`. See [dns.md](../infrastructure/dns.md). diff --git a/infrastructure/network.md b/infrastructure/network.md index 44cbab8..4819e71 100644 --- a/infrastructure/network.md +++ b/infrastructure/network.md @@ -48,9 +48,9 @@ hubris internal bridges: ## DHCP - **Household (`192.168.178.x`)**: Fritz!Box built-in DHCP. Proxmox `vmbr1` has a reservation: MAC `84:47:09:6b:e7:58` → `192.168.178.10`. -- **Homelab (`192.168.8.x`)**: Technitium on [CT 107](../containers/107-dns.md) at `192.168.8.2`. Range `192.168.8.100–192.168.8.240`, gateway `192.168.8.1`, DNS `192.168.8.2`. +- **Homelab (`192.168.8.x`)**: Technitium on [CT 107](../containers/107-dns.md) at `192.168.8.2`. Range `192.168.8.241–192.168.8.254`, gateway `192.168.8.1`, DNS `192.168.8.2`. -Most homelab LXCs use static IPs below `.100`. DHCP only covers new/transient containers. +Static IPs span `.101–.239` (all LXCs, VMs, and workstations). DHCP pool narrowed to `.241–.254` (2026-06-03) to avoid overlap and IP conflicts. ## DNS @@ -77,6 +77,9 @@ No NAT on Proxmox — traffic flows without double-NAT. ## Changelog +### 2026-06-03 — DHCP pool narrowed to `.241–.254` +Previous pool `.100–.240` overlapped with all static LXCs/VMs (` .101–.239`), creating IP conflict risk (DHCP could hand out an IP that a static service expects). Shrunk pool to `.241–.254` via Technitium API. No services re-IP'd. 11 stale DHCP leases in `.101–.110` will expire naturally. **Open:** ZimaOS (VM 100) holds DHCP lease `.103` but inventory expects `.195` — needs static IP set inside VM. See [plan](../plans/2026-06-03-dhcp-pool-exclude-static-ips.md). + ### 2026-06-02 — Executed migration; Proxmox as subnet router Fritz!OS 8.x does not support second IP networks on LAN ports, so the final design uses Proxmox as the router: `vmbr1` (eno1 → SODOLA → Fritz!Box) is the uplink at `192.168.178.10`; `vmbr0` is a portless internal bridge with `192.168.8.1` alias as the LXC gateway. Technitium DHCP enabled for `192.168.8.100–240`. Caddy service unit was missing and recreated. See [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). diff --git a/vms/100-zimaos.md b/vms/100-zimaos.md index d428cca..fed7fd3 100644 --- a/vms/100-zimaos.md +++ b/vms/100-zimaos.md @@ -45,7 +45,7 @@ The alternative (dedicated virtual data disk on the `library` lvmthin pool, e.g. ## Open items -- **DHCP lease, not a reservation.** `192.168.8.195` came from the LAN router; the [LAN-side dnsmasq](../infrastructure/dns.md) only does DNS, not DHCP. If the lease rotates, the Caddy upstream needs updating too (`/etc/caddy/Caddyfile` on [caddy (121)](../containers/121-caddy.md)). Better fix: pin a static lease on the LAN router or set a static config inside ZimaOS. +- **DHCP lease, not a reservation — DRIFTED.** ZimaOS was at `192.168.8.195` (Slate AX DHCP). After the Slate AX → Technitium DHCP migration, ZimaOS renewed at `192.168.8.103`. **Caddy 502s** because it reverse-proxies to `.195`. ZimaOS is reachable directly at `http://192.168.8.103`. Fix: set static IP `192.168.8.195/24` inside ZimaOS via web UI (needs credentials) or Proxmox console. DHCP pool narrowed to `.241–.254` on 2026-06-03 — ZimaOS lease at `.103` expires 2026-06-04 and won't renew at that IP. - **No Authentik wiring.** [authentik (124)](../containers/124-authentik.md) isn't enforcing auth in front of ZimaOS yet — ZimaOS handles its own first-run wizard. The Caddyfile block uses bare `reverse_proxy` rather than the `import authentik` pattern used by e.g. artifacto; layer it in once the wizard is complete and a static admin user exists. - **No PBS backup.** No Proxmox Backup Server configured on hubris today; this VM is not backed up. - **qemu-guest-agent not installed.** ZimaOS's installer doesn't bundle it, so `qm guest cmd 100 ...` returns "QEMU guest agent is not running". IP discovery during this install was done via console screendump → `qm monitor` → `screendump`.