Problem: docs-lint (added in the wiki-hq reorg) surfaced 126 broken relative
links that predated this session — a container rename, incident/plan docs
that moved into archive/done subfolders without their inbound links being
updated, and a handful of relative-depth bugs in files nested under
containers/archive/ and plans/done/.
Fixes applied, by category:
- 124-authentik.md -> 106-auth-outpost.md (container was renamed; ~40 refs).
- investigations/{2026-04-21-hubris-crash-loop,2026-05-31-authentik-vps-migration}.md
-> archive/ prefix (both moved to investigations/archive/ previously).
- plans/{2026-06-01-slate-ax-to-sodola-migration,2026-06-04_130000-deprecate-claudio-bot,
2026-06-25-yuvomi-deployment}.md -> plans/done/ prefix.
- Depth bugs in files nested one level deeper than their siblings assumed
(investigations/archive/*, knowledge/wiki/containers/archive/*,
plans/done/*) — corrected relative-path depth.
- Destroyed containers with no surviving page (126-plato) delinked to the
containers/index.md archaeology row instead of a 404.
- ludo-mini.yaml -> strong.yaml (host was renamed, same physical machine).
- netbird-vps.md (no narrative page exists) -> netbird-vps.yaml (substrate
record, matching the existing convention for hosts without a wiki page).
- runbook-dpkg-interrupted.md refs -> .agents/skills/runbook-dpkg-interrupted/SKILL.md
(missed in the phase-4 runbook move because the referencing files used a
bare filename, not a runbooks/ prefix).
- One dangling forward-reference to a never-written investigation delinked
to the actual incident record it was describing.
Left alone: two links in knowledge/wiki/containers/101-jellyfin.md into
devops/homelab-authentik-admin/ — an intentional reference to a sibling repo,
not present in this checkout.
Verification: broken-link count 126 -> 2 (real remainder is the cross-repo
reference above); gen-topology.py --check still exit 0; build_host_files.py
still idempotent; all inventory.yaml doc_page targets still resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
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 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–.240stays for static hosts,.2for Technitium,.1for gateway - Zero changes to any LXC, VM, Caddy, or Proxmox config.
Why .241–.254:
- Highest static IP is
.239(elementsynapse) —.241gives a 1-IP gap .255is 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
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
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
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
curl -sk "http://192.168.8.2:5380/api/dhcp/scopes/list?token=$TOKEN" | jq '.response.scopes[0] | {startingAddress, endingAddress}'
Expected:
{
"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
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
.195or 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
.195is 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:
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:
-
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.`
- Old:
-
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
- Line 37: Update pool range:
-
containers/107-dns.md— Add changelog entry:### 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). -
infrastructure/network.md— Line 51: Update pool range in the DHCP table row. -
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:
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.
# 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
- Is zimaos (VM 100) currently DHCP or static? The doc says DHCP lease, but it's listed as
lan_ip: 192.168.8.195in inventory. If it's actually DHCP, it's the one host that needs a static assignment before the pool shrinks. - Are there any transient DHCP clients (test laptops, phones) on the homelab subnet that hold
.100–.240addresses? Check leases before cutting over. - 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.239and.230–.239could 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).