docs: host-addresses audit + capability-map row + REPORT (agent 0.119.0 / hub 0.85.0)
This commit is contained in:
@@ -117,6 +117,7 @@
|
||||
| Forgot dashboard password → instant reset code | controller v0.123, hub | **PROVEN-LIVE** | `DRILL-day0-take2-2026-07-12` F-15 (live re-run of the exact failure path: hash applied 1s after request, code accepted first try) | |
|
||||
| Multiple household users / per-person accounts | — | **MISSING** | — | Single dashboard password; acceptable for alpha → R-15 |
|
||||
| WireGuard base infra always-on; OOB operator access (felhom-sshd, /32 peer) | agent v0.72, hub v0.35 | **IMPLEMENTED** | `SPIKE-oob-wg-operator-peer-2026-07-05`, `SPIKE-felhom-sshd-2026-07-05` | Mutual-repair desired-state arc not built → R-13 |
|
||||
| The operator can see WHERE a managed host is — its LAN address and its WireGuard address, on the host page | agent **v0.119.0**, hub **v0.85.0** | **PROVEN-LIVE** (2026-07-31) | `audits/host-addresses-visible-2026-07-31.md` | Before this the LAN IP was **not reportable at all** — `HostMetrics` carried no address of any kind — and the WG IP existed only in `/offsite`'s peer table keyed by pubkey (peer→host, never host→peer). New wire field `addresses[]`, one row per (interface, address); `IsGlobalUnicast()` is the whole filter, chosen by MEASURING both demo boxes, and it needs no veth/fwbr denylist because that plumbing carries no IP. Rendered live on both 0.119.0 hosts matching their `ip addr` ground truth exactly. **Two honesty properties carry the risk and are both red-proofed:** WireGuard shows the hub ALLOCATION and whether the box CONFIRMS holding it (allocation alone cannot tell a live tunnel from a peer never applied), and an agent below 0.119.0 renders **UNKNOWN, never "no addresses"** — proven live on `drill-r50-0a4f9a` (0.113.0). **Not covered:** a two-LAN-bridge box and a real WG drift, neither of which exists to observe |
|
||||
| Break-glass management-plane recovery | agent v0.71, hub v0.84 | **IMPLEMENTED** | `runbooks/break-glass.md` | hub v0.84.0 adds an **operator-SESSION** retrieval path (host page → Console access → Reveal; `POST /hosts/{id}/reveal-recovery-credential`, CSRF-gated, writes a customer-visible `recovery_credential_revealed` event) beside the pre-existing **global-key** one (`GET /api/v1/admin/hosts/{id}/recovery-credential`), which is untouched and stays the route for when the hub UI itself is down. Still IMPLEMENTED, not PROVEN-LIVE: the UI path has not been exercised on a real lockout, and that the revealed password authenticates at `:8006` is operator-verified only. The vaulted secret is plaintext at rest → **R-133** |
|
||||
|
||||
## F. Notifications & monitoring
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# AUDIT — a managed host's addresses are visible in the hub (2026-07-31)
|
||||
|
||||
**Artifacts:** felhom-agent **v0.119.0** + hub **v0.85.0**. Method: **endpoint-level** on live
|
||||
hardware (no browser on DooPlex), plus direct `ip addr` reads on both boxes as the ground truth the
|
||||
rendered page is checked against.
|
||||
|
||||
## 1. The finding that started it
|
||||
|
||||
The operator asked to see the LAN and WireGuard IPs on the hosts page, "because it looks like it is
|
||||
not visible anywhere". That was very nearly exact:
|
||||
|
||||
- **LAN IP — not visible, and not reportable.** `HostReport`/`HostMetrics` carried node, cpu, memory,
|
||||
disk, loadavg, uptime, cpu temp and the wrapper sha — **no address of any kind**. The hub could not
|
||||
render what it was never sent.
|
||||
- **WireGuard IP — visible in one place, in the wrong direction.** `/offsite` renders a peer table
|
||||
with an Assigned IP column and a `Host` link, so an operator could go peer→host but never
|
||||
host→peer, which is the direction the question is actually asked in. `GetWGPeerForHost` already
|
||||
existed with **no UI consumer**.
|
||||
|
||||
## 2. Two plausible LAN-IP sources were rejected BEFORE any code, on measurement
|
||||
|
||||
| Candidate | Why it fails |
|
||||
|---|---|
|
||||
| `lan_resolver.host_ip` (agent config) | OPTIONAL — set only when the split-horizon DNS feature is configured. Absent on a box that never enabled it. |
|
||||
| `DeriveHostIP(local_api.listen_addr)` | Returns **`169.254.253.1`** — since the R-50 island migration the local API binds a link-local address that is **byte-identical on every box**. |
|
||||
|
||||
Both would have produced a confident wrong answer, which is worse than the blank they replace. The
|
||||
agent now reads the kernel's own interface table instead.
|
||||
|
||||
## 3. Ground truth — what the boxes actually hold
|
||||
|
||||
`ip -o addr show`, 2026-07-31:
|
||||
|
||||
```
|
||||
demo-felhom demo-hp
|
||||
lo 127.0.0.1/8 + ::1/128 (both)
|
||||
tailscale0 100.70.170.35/32 + ULA + fe80 100.76.96.79/32 + ULA + fe80
|
||||
vmbr0 192.168.0.162/24 + fe80 192.168.0.87/24 + fe80
|
||||
vmbr9 169.254.253.1/30 + fe80 169.254.253.1/30 + fe80 <- IDENTICAL on both
|
||||
wg-felhom 10.77.0.2/32 10.77.0.3/32
|
||||
(no address at all: enp*/wlp*/wlo1, veth9201i0, veth9201i1)
|
||||
```
|
||||
|
||||
**The measurement decided the design.** `IsGlobalUnicast()` alone drops loopback, `fe80::/10` and
|
||||
`169.254/16` — that last one being the island literal above. And **no veth/fwbr/tap denylist is
|
||||
needed**, because on a Proxmox host that per-guest plumbing carries no IP at all and self-excludes.
|
||||
The unit-test fixtures are these tables transcribed verbatim, **including the address-less rows**,
|
||||
because the no-denylist claim rests on those rows really being empty.
|
||||
|
||||
## 4. The wire, from the agent that will ship
|
||||
|
||||
`felhom-agent --selftest=hub` on demo-felhom running 0.119.0 — "the report it would send":
|
||||
|
||||
```json
|
||||
"addresses": [
|
||||
{ "iface": "tailscale0", "cidr": "100.70.170.35/32" },
|
||||
{ "iface": "tailscale0", "cidr": "fd7a:115c:a1e0::5236:aa24/128" },
|
||||
{ "iface": "vmbr0", "cidr": "192.168.0.162/24" },
|
||||
{ "iface": "wg-felhom", "cidr": "10.77.0.2/32" }
|
||||
]
|
||||
```
|
||||
|
||||
Correctly present: the LAN address. Correctly absent: `lo`, `vmbr9`'s island address, every `fe80::`.
|
||||
|
||||
## 5. Rendered, live, on every enrolled host
|
||||
|
||||
Read off `GET /hosts/<id>` on the deployed hub 0.85.0:
|
||||
|
||||
| Host | Agent | Network card |
|
||||
|---|---|---|
|
||||
| `demo-felhom-8363b5` | 0.119.0 | WireGuard **10.77.0.2 confirmed**; `vmbr0 192.168.0.162/24`, `tailscale0 100.70.170.35/32` + ULA |
|
||||
| `demo-hp-bb76ea` | 0.119.0 | WireGuard **10.77.0.3 confirmed**; `vmbr0 192.168.0.87/24`, `tailscale0 100.76.96.79/32` + ULA |
|
||||
| `drill-r50-0a4f9a` | 0.113.0 | WireGuard 10.77.0.4 **allocated**; *"This host's agent (0.113.0) does not report its addresses — they are **unknown**, not absent. Needs agent 0.119.0 or newer."* |
|
||||
| `sess-f-2670b5` | old | same UNKNOWN rendering |
|
||||
|
||||
Both 0.119.0 rows match the `ip addr` ground truth in §3 exactly.
|
||||
|
||||
## 6. The two design points that carry the risk
|
||||
|
||||
**WireGuard is rendered as TWO facts.** `WGAssignedIP` is the hub's allocation (`wg_peers`, desired
|
||||
state); `WGConfirmed` is whether the box reports holding it. Rendering the allocation alone would
|
||||
make a peer that was **never applied** look healthy — the presence-is-not-result trap, in the shape
|
||||
this project has already been bitten by twice (F-CRIT-2, R-100). A mismatch renders `not confirmed by
|
||||
the box`, and a red-proof pins it: hard-wiring `WGConfirmed = true` turns that test red.
|
||||
|
||||
The split is keyed on the **allocation**, never on the interface name `wg-felhom` — that is a systemd
|
||||
unit name, and a UI keyed on it would silently mis-render the day it changes.
|
||||
|
||||
**An old agent renders UNKNOWN, never "no addresses."** Below `0.119.0` the field is absent from the
|
||||
wire. An absent signal and a negative result are different facts, and conflating them would have the
|
||||
page state something false about a live host. Proven live on `drill-r50-0a4f9a` (§5) and red-proofed:
|
||||
deleting the branch makes the page claim the host has no routable address.
|
||||
|
||||
## 7. Release hygiene (R-115's leg, walked deliberately)
|
||||
|
||||
Deployed **and** installable, in the same session — the two states R-115 says drift silently:
|
||||
|
||||
- built v0.119.0, deployed to **both** demo boxes (`systemctl is-active` → `active`, `--version` → 0.119.0)
|
||||
- **published** to Gitea (`scripts/publish-agent.sh`), sha `e37aca824d0f57bb…`, **round-trip GET verified**
|
||||
- **vouched** in the Day-0 manifest and **read back**: `agent=0.119.0`, with golden `0.188.0`,
|
||||
`min_agent 0.113.0` and `wrapper_sha256` preserved **verbatim**
|
||||
- `min_agent` deliberately **NOT** raised: the field has no controller coupling, so holding the
|
||||
controller floor for it would be an unnecessary fleet-wide write
|
||||
|
||||
A trap hit on the way, worth recording: the Gitea package list sorted **lexicographically** put
|
||||
`0.96.0` above `0.118.1` — the exact shape R-120 recorded for SQL `MAX()`. Re-sorted by semver before
|
||||
choosing the version.
|
||||
|
||||
## 8. Not covered
|
||||
|
||||
- **A box with two LAN bridges.** The design handles it (one row per interface) and a unit test covers
|
||||
it, but no such box exists to observe.
|
||||
- **IPv4-only or IPv6-only hosts** — every box here is dual-stacked via tailscale's ULA.
|
||||
- **A real WireGuard drift** — the `not confirmed` branch is unit-tested and red-proofed, never seen
|
||||
in production, because no live peer is currently unapplied.
|
||||
- The card is **operator-facing only**; no customer surface changed.
|
||||
Reference in New Issue
Block a user