The allowlist entry is REQUIRED, not cosmetic: handleEvent 400s an unknown event_type, so controller v0.173.0's new drift alert would be silently inert without it. Shipped with the controller that emits it. Docs: - RUNBOOK-local-api-endpoint-drift.md — how to repair a drift, including the step everyone will want to skip (establish which value is CORRECT from what the agent is actually bound to, rather than assuming bootstrap.json wins) and what success looks like (SILENCE, not a "recovered" line, because a fresh controller's healthy first observation is not logged). Records both 2026-07-26 repairs. - ROADMAP: R-77 shipped; R-78 the local_api authority ruling, with the clobber-a-working-channel risk spelled out in BOTH directions so it is not resolved opportunistically; R-79 the whole-surface English-strings sweep; R-80 expected_backup_missed, flagged as likely outranking R-77 because 7.3 days of stale backup materially exceeds the ~1.5-day channel outage, so the causal link the DIAG hedged on cannot be the whole story. - Capability map: note against the drive-wizard row (every agent-backed capability rides this channel) that a silent drift class is now detected. NO row status flips — detection is not prevention.
5.0 KiB
RUNBOOK — repairing a local_api endpoint drift
When to run: the controller logs
[ERROR] bootstrap: local_api endpoint DRIFT — … at startup, or the operator receives a
local_api_endpoint_drift event, or the „A tárolókezelő ügynök címe elavult a beállításokban."
banner appears. Detection shipped in controller v0.173.0 (R-77).
What drift means: controller.yaml and bootstrap.json disagree on local_api.endpoint. The
controller dials controller.yaml. The agent listens wherever it was last configured — which,
after the R-50 island migration, is 169.254.253.1:8443.
Why the controller does not fix this itself: auto-reconcile would clobber a correct
controller.yaml from a stale bootstrap.json — a half-completed re-provision or a hand-repaired
guest would lose a working channel on its next restart, fleet-wide and silently. Which file is
authoritative is R-78, unresolved. Until it is ruled, repair is a deliberate operator action.
Operator-present. This edits a live guest's config and restarts its controller. The channel being down also degrades the drive gate, guest-reboot recovery and the quiesce/backup loop, so it is worth doing promptly — but not blind.
1. Establish which value is correct — do NOT assume bootstrap.json
bootstrap.json is right when the drift came from a migration or re-provision — the common case.
It is wrong if someone hand-repaired controller.yaml after a failed provision. Decide with
evidence, not by default:
# What is the agent ACTUALLY listening on? This is the ground truth.
ssh <pve-host> "ss -lntH 'sport = :8443'" # post-island: 169.254.253.1:8443
The correct endpoint is whatever the agent is bound to. Both files are just claims about it.
2. Compare all three fields, not only the endpoint
mergeLocalAPI replaces the whole block, so a migration may have moved more than the address.
pct exec <VMID> -- python3 - <<'PY'
import json, re, hashlib
CY="/var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml"
BJ="/etc/felhom-bootstrap/bootstrap.json"
def yget(path):
blk={}; inb=False
for line in open(path):
if re.match(r'^local_api:', line): inb=True; continue
if inb and re.match(r'^\S', line): break
if inb:
m=re.match(r'\s+(\w+):\s*(.*)\s*$', line)
if m: blk[m.group(1)]=m.group(2).strip().strip('"').strip("'")
return blk
cy=yget(CY); bj=(json.load(open(BJ)).get("local_api") or {})
print("endpoint cy/bj :", repr(cy.get("endpoint")), repr(bj.get("endpoint")))
print("fingerprint AGREE :", cy.get("fingerprint")==bj.get("fingerprint"))
print("token AGREE :", cy.get("token")==bj.get("token"))
PY
Never print the fingerprint or token values — booleans only, and never into a committed file.
STOP if
fingerprintortokendisagree. A moved pin or a rotated token is a different failure: correcting only the address would then fail closed on the pin (agent_channel_pin_mismatch) or 401 (agent_channel_unauthorized). Diagnose that separately before touching anything.
3. Back up, edit the endpoint only, restart
CY=/var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml
pct exec <VMID> -- cp -a $CY $CY.pre-fix.bak
# edit ONLY local_api.endpoint to the value from step 1
pct exec <VMID> -- systemctl restart felhom-controller-bootstrap.service
4. Verify — and know what success looks like
pct exec <VMID> -- docker logs felhom-controller 2>&1 | grep -i "\[channel\]"
Expect SILENCE, not a "recovered" line. On a freshly restarted controller the first probe
succeeds and Check returns early (healthy first-obs → no notify), so nothing is logged. A
recovered line only appears when a running controller transitions down→up.
Positive confirmation is therefore:
- zero
[channel]lines after ~90 s (a broken channel logstransient downwithin 60 s andDOWNwithin 120 s); - the dashboard banner cleared;
- zero new
agent_channel_*events hub-side; - no
local_api endpoint DRIFTline at startup (the two files now agree — if they still disagree because you correctedcontroller.yamlrather thanbootstrap.json, the drift alert will keep firing and that is correct: it is telling you the two sources still disagree. Align the other file too, or accept the alert until R-78).
Executed instances
| date | boxes | before → after | fingerprint/token | outcome |
|---|---|---|---|---|
| 2026-07-26 | demo-felhom 9201 | 192.168.0.162:8443 → 169.254.253.1:8443 |
both AGREE | channel healthy; 0 [channel] lines in 90 s; banner cleared |
| 2026-07-26 | demo-hp 9201 | 192.168.0.87:8443 → 169.254.253.1:8443 |
both AGREE | channel healthy; 0 [channel] lines in 90 s |
Root cause of both: the R-50 island migration (2026-07-25 12:42–12:46) rewrote bootstrap.json and
restarted the controllers, which re-read the untouched controller.yaml. Full analysis:
audits/DIAG-agent-channel-2026-07-26.md.