REPORT: installer v1.28.0 published and verified live; evidence, and the Part 1 logs I lost again
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
This commit is contained in:
@@ -1,169 +1,170 @@
|
||||
# REPORT — The door, part one: a correct code stops being called wrong (2026-08-12 night)
|
||||
# REPORT — The removal that works once, and a status page that says what it is asking for (2026-08-13)
|
||||
|
||||
**Three repos.** hub **v0.103.0** · agent **v0.129.0** · controller **v0.214.0** · golden **0.214.0**.
|
||||
Register: **R-311 CLOSED**, R-307 CLOSED, **R-312 / R-313 / R-314 / R-315 opened**. Ceiling
|
||||
R-310 → **R-315**.
|
||||
**Shipped:** `installer-v1.28.0`, published and verified against the live URL.
|
||||
**Register:** R-305 CLOSED (by R-316), **R-316 / R-317 / R-318 opened**; ceiling R-315 → **R-318**.
|
||||
**Venue:** `drill-r50` only. Neither demo box reinstalled. `peti-felhom` not contacted. Nothing deleted.
|
||||
|
||||
---
|
||||
|
||||
## 1. The spike's answer, first and in plain language
|
||||
## 1. Cycle 3, before and after
|
||||
|
||||
**Can a customer restore from a set-aside store with the machinery that already exists? NO — and
|
||||
building it is new surface, not wiring.** Established read-only, at `file:line`, before a line was
|
||||
written.
|
||||
**Before — on the PUBLISHED v1.27.0, from `virgin`, exit 1:**
|
||||
|
||||
Every restore entry point resolves the repository from `settings.GetOffboxTarget()` and the password
|
||||
from the single `offboxPwPath()` file: `offboxLatestSnapshot` (`offbox_restore.go:85-86`),
|
||||
`offboxSnapshotSize` (`:139-140`), `RestoreOffboxScratch` (`:206+`). **A grep for a repo-path
|
||||
parameter anywhere in the restore chain returns nothing.** The only seam that installs a recovered
|
||||
password, `InjectOffboxPassword` (`offbox.go:667`), writes that same one file — i.e. **adoption**.
|
||||
```
|
||||
[ERROR] a resolver is already bound to :53 on this host:
|
||||
udp UNCONN 0 0 0.0.0.0:53 … users:(("dnsmasq",pid=7076,fd=4)) …
|
||||
[ERROR] a resolver is already bound to :53 on this host — Felhom needs the guest reachable by name on your LAN.
|
||||
Stop or reconfigure that resolver, OR point your LAN DNS at the guest's address, then re-run.
|
||||
(Felhom does NOT touch DNS services on a host it does not own — this is a refusal, not a change.)
|
||||
THIS LOOKS LIKE OURS. A previous Felhom install leaves the dnsmasq PACKAGE installed and its unit
|
||||
enabled (only our config snippet is removed), and unconstrained it binds 0.0.0.0:53 — which is what
|
||||
this gate is seeing. If this host had no dnsmasq before Felhom, clear it with:
|
||||
systemctl disable --now dnsmasq
|
||||
Then re-run this installer. If dnsmasq is YOURS, leave it and use one of the two routes above.
|
||||
[ERROR] PRE-FLIGHT FAIL (exit 1) — fix the finding above and re-run
|
||||
```
|
||||
|
||||
What the drill did to read the set-aside store was `restic` **by hand**, with `-r <alt repo>` and an
|
||||
overridden `RESTIC_PASSWORD_FILE`. **That distance is exactly what (b)-to-(c) costs.**
|
||||
**After — v1.28.0, three fresh cycles from `virgin`, exit 0:**
|
||||
|
||||
**So the session halted at Part 3 by its own rule, shipped Part 2, and hands back options → R-312.**
|
||||
Cost to find out: ~35 minutes, read-only.
|
||||
```
|
||||
:53 now: 0
|
||||
CYCLE 3 rc=0
|
||||
[INFO] host DNS (:53): free
|
||||
[OK] pre-flight passed
|
||||
[OK] PRE-FLIGHT PASS (mode=byo) — no state written, no install step executed
|
||||
```
|
||||
|
||||
## 2. Part 0 — the countdown, cancelled on your ruling
|
||||
The three cycles were run on bytes **sha256-identical** to what the live URL now serves
|
||||
(`cca1dedd9b7c152c…`, compared three ways: live URL, tested file, repo main).
|
||||
|
||||
Through the product's own operator path (`--abandon-stop`, which refuses rather than silently
|
||||
no-opping), with the container **stopped first** so the running controller could not overwrite
|
||||
`settings.json` from memory. **Proved, not trusted to the exit code:**
|
||||
|
||||
- `abandon_started_at` and `abandon_at` — **gone**. `AbandonStatus` returns `Active=false` when
|
||||
`AbandonAt` is empty (`offbox_abandon.go:111-113`), so no countdown renders.
|
||||
- `abandon_repo_path` — **deliberately kept**, as the pointer to the preserved store.
|
||||
- The set-aside store — **still there**: 36 snapshot objects, full `config/data/index/keys/locks/
|
||||
snapshots` structure. Both repositories still on the endpoint. **Nothing deleted anywhere.**
|
||||
|
||||
**And the thing you should know about what was preserved (R-313):** it holds **36 snapshots and one
|
||||
key slot**, and it does **not** open with the box's current password (`Fatal: wrong password or no key
|
||||
found`, exit 1 — measured). Its key is the one hashed `48741892f0ef…` — retained row id 4,
|
||||
`identity_blob` **NULL**, a pre-v0.93.0 row. **The material was dropped by the R-198 defect during its
|
||||
two-month window, so no recovery code in existence opens that store.** Keeping it is still the right
|
||||
call — deleting is irreversible and a decision, not an accumulation — but it is 36 unreadable
|
||||
snapshots, and that is the concrete, still-present cost of R-198 sitting on the endpoint.
|
||||
|
||||
## 3. Part 2 — what shipped, and a correction to the premise
|
||||
|
||||
**The premise needed correcting first.** The task described the customer being told *"the recovery code
|
||||
did not open the sealed bundle"*. That is the **agent's local-API** reply. The **customer-facing
|
||||
screen already hedged** (R-222/R-226) — it named both causes, named the kept package and its date, and
|
||||
said it could not tell them apart. That was **honest**; it could not tell them apart **because nothing
|
||||
ever looked**. So what shipped is smaller and more precise than "stop the lie": **the hedge becomes an
|
||||
answer.**
|
||||
|
||||
- **hub v0.103.0** — `GET /hosts/<id>/escrow/retained`, the **first production caller
|
||||
`ListSupersededEscrow` has ever had**. Self-scoped identically, same recovery-mode gate, same audit
|
||||
event written before the bytes leave, capped at 16. Rows with a NULL `identity_blob` are **withheld
|
||||
and counted** (`unopenable_count`): they can never open what the caller is asking about, and serving
|
||||
them would let the screen promise recovery on exactly the boxes the original defect hurt.
|
||||
- **agent v0.129.0** — retained packages tried **only after** the current one refuses; `422` with
|
||||
`superseded_at`; bounded at 6 attempts (~1 s of scrypt each); fail-safe in every direction.
|
||||
- **controller v0.214.0** — class `RecoveryCodeOpensRetained`, gated on MinAgent **0.129.0** via a
|
||||
**second, separate** trust flag (a box can sit between 0.126.0 and 0.129.0). The message says the
|
||||
code is correct, names the date, says the package is kept, says the **current** backups are
|
||||
unaffected, and **promises no restore** — it routes to support, which can do it.
|
||||
|
||||
**The trade you should see stated:** the hub still cannot read any of it — sealed bytes in, sealed
|
||||
bytes out, no decrypt path, no recovery code ever held. What widens is **volume**: a host key that
|
||||
could fetch one opaque package can now fetch N, bounded by self-scope, the recovery-mode gate and the
|
||||
cap.
|
||||
|
||||
## 4. Red-proofs — and where the lie actually lives
|
||||
|
||||
Every mutation asserted to have applied before its run.
|
||||
|
||||
| Repo | Mutation | Outcome |
|
||||
|---|---|---|
|
||||
| hub | serve the CURRENT row instead of retained | FAILS (count 2→1) |
|
||||
| hub | drop the unopenable guard | FAILS (count 1→2, unopenable 1→0) |
|
||||
| hub | drop self-scope | FAILS (403→200) |
|
||||
| hub | collapse the route suffix | FAILS (count 1→0) |
|
||||
| **agent** | **remove the retained lookup** | **FAILS — the fail-closed wrong-code error returns. THE LIE COMES BACK.** |
|
||||
| agent | + 6 more (nil fetcher, wrong code, fetch failure, bounded attempts, predates-field, success path) | all pinned |
|
||||
| controller | delete the new case | FAILS — but the customer gets the **neutral** message, because R-224's safe default catches it |
|
||||
| controller | make 422 unconditional | FAILS — an agent that never looked is read as having looked |
|
||||
| controller | route 400 to the new class | FAILS — a mistype is congratulated |
|
||||
|
||||
**Answering the question directly:** the lie returns when the **agent's** retained lookup is removed,
|
||||
not when the controller's case is. R-224's safe default is doing its job one layer up.
|
||||
|
||||
## 5. The claim guard, and a gate whose positive control failed
|
||||
|
||||
**The claim guard had a blind spot the size of the recovery screen** — it scanned templates only,
|
||||
while every recovery message is a Go string in a handler. It now scans `recovery_handlers.go` too, and
|
||||
**on its first run convicted a pre-existing unregistered claim**. 8 → 10 registered claims.
|
||||
|
||||
**The wire-contract gate: declared, and honestly weaker than it looks (R-315).** The hub response was
|
||||
made a **named type** so the gate could resolve it; the wire is declared as a fourth ROOT and the tag
|
||||
count rose **174 → 182**, so the fields are inspected. But a positive control — renaming the
|
||||
agent-side `superseded_at` tag — **still passed**, because the check is repo-wide name-presence and
|
||||
the string also occurs as a map key elsewhere. The gate documents this ("name-reachability is not
|
||||
use"), so it is a known limit, not a regression — **but declaring this wire bought documentation, not
|
||||
enforcement**, and saying otherwise would have been false.
|
||||
|
||||
## 6. Live state
|
||||
## 2. The mechanism, at `file:line`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| controller | **0.214.0** on guest 9201, `Up … (healthy)` |
|
||||
| agent | **0.129.0** on `felhom-pve`, unit active, journal clean |
|
||||
| hub | **0.103.0** — see §7 |
|
||||
| golden | **0.214.0** baked + published |
|
||||
| ownership **recorded** | `felhom-host-install.sh:1718` — preflight, `dpkg-query -W -f='${Status}' dnsmasq \| grep -q "install ok installed"` |
|
||||
| ownership **read** | `:1097` (`_state_get dnsmasq_preexisting`) at uninstall |
|
||||
| state file **deleted** | `:1268` — *after* the read. **The order was already correct.** |
|
||||
| Felhom's dnsmasq snippets removed | `:1162`, in the loop just above the ownership decision |
|
||||
|
||||
### Live proof on hardware — the 422, end to end
|
||||
**Why cycle 2 concludes "pre-existing": package presence alone.** The preflight asks dpkg one question
|
||||
and nothing else — it does **not** consult the absence of a record, and it cannot, because the record
|
||||
was deleted with the state file. v1.27.0 stopped the unit and left the package, so the answer stayed
|
||||
`yes` and our own package became "the household's" one cycle later.
|
||||
|
||||
**What the first uninstall did and did not remove:** snippets — removed. Unit — stopped + disabled.
|
||||
State file (and with it the record) — removed. **Package — left.** That last one is the whole defect.
|
||||
|
||||
## 3. What v1.28.0 changes
|
||||
|
||||
When the record says we installed it, the uninstall **removes the package** as well as stopping the
|
||||
unit. Order unchanged: read the record → act → delete the state.
|
||||
|
||||
**Two packages are recorded, not one.** `dnsmasq` ships the systemd unit; **`dnsmasq-base` ships
|
||||
`/usr/sbin/dnsmasq`** (`dpkg -S`, measured on the box). Separately installable, so each is recorded at
|
||||
preflight and taken back only if we added it.
|
||||
|
||||
**Guard rails:** ownership **read, never inferred**; the dependency check is an **`apt-get -s purge`
|
||||
simulation** that proceeds only if the removal set is a subset of ours, else stop+disable **naming the
|
||||
blocking package**; never interactive; **never fatal** — a wedged apt is recorded and restated in the
|
||||
closing NOTE; and the success is **re-queried** rather than read off apt's exit code.
|
||||
|
||||
## 4. Scenarios and red-proofs
|
||||
|
||||
| Scenario | Result |
|
||||
|---|---|
|
||||
| **A** three cycles, fixed | install 3 **PASSES**, `:53` free at every uninstall |
|
||||
| **B** resolver pre-dates Felhom | **untouched** — `install ok installed`, unit active, both packages recorded `yes` |
|
||||
| **C** something depends on it | **not purged**; log named `household-dns-thing`; stop+disable; `:53` free; dependent survived |
|
||||
| **D** no ownership record | **untouched**, reason logged, exact command named |
|
||||
|
||||
**Red-proofs — mutation asserted applied before each run:**
|
||||
|
||||
| Mutation | Outcome |
|
||||
|---|---|
|
||||
| remove the purge call | cycle 2 records `yes`; **cycle 3 refuses, exit 1, in those exact words** |
|
||||
| remove the ownership check | **the household's resolver is PURGED** (`unknown ok not-installed`) |
|
||||
| infer ownership when no record | **the guess is taken** — a field box loses its own DNS |
|
||||
|
||||
**One honest note on red-proof B.** Its first run appeared to pass *for the wrong reason*: apt failed
|
||||
with `dpkg was interrupted` (a broken state my own earlier manual package juggling left), so the
|
||||
resolver survived by accident rather than by the guard. I repaired dpkg and re-ran, and it then failed
|
||||
as required. Worth stating twice over: the mutation looked like a passing guard, and **my own fix's
|
||||
apt-failure path was incidentally observed doing exactly what it should** — logging, continuing, naming
|
||||
the command.
|
||||
|
||||
## 5. The machines already in the field
|
||||
|
||||
**Measured, not reasoned.** A field box is one with the package installed and **no record** — scenario
|
||||
D. Its uninstall leaves the resolver running with the reason logged, and its next install refuses with
|
||||
the message quoted in §1.
|
||||
|
||||
**Is there an honest durable marker? No, and none can be invented.** The Felhom `/etc/dnsmasq.d/
|
||||
felhom-*.conf` snippets are deleted by the uninstall's own loop *before* the ownership decision; the
|
||||
state file carrying the record is deleted at `:1268`; nothing under `/etc/felhom*` survives.
|
||||
`/var/log/dpkg.log` does record the install and is a **timestamp** — refused by the standing rule as a
|
||||
heuristic dressed as a fact. → **R-318**
|
||||
|
||||
**The preflight message, judged as a customer would:** it states the finding, keeps its two routes and
|
||||
its written promise not to touch DNS on a host we do not own, and adds the one thing that gets a person
|
||||
moving — *"THIS LOOKS LIKE OURS"* plus one exact command. It hedges correctly (*looks like*). Its
|
||||
weakness is that it asks them *"did this host have dnsmasq before Felhom?"* — precisely the question we
|
||||
can no longer answer for them. **That is a mechanism, not a rule: the command is on the screen at the
|
||||
moment it is needed.**
|
||||
|
||||
## 6. The defect I nearly shipped
|
||||
|
||||
The agent decides whether to install dnsmasq with `os.Stat("/usr/sbin/dnsmasq")`
|
||||
(`felhom-agent/internal/lanresolver/lanresolver.go:105`) — but that path belongs to **`dnsmasq-base`**,
|
||||
while the unit comes from **`dnsmasq`**. Purging only `dnsmasq` would leave the binary, so the next
|
||||
install would skip the apt step and then fail to enable a unit that is gone — **a silent resolver where
|
||||
today there is at least a visible refusal.** That is why ownership is recorded per package.
|
||||
|
||||
It remains reachable where `dnsmasq-base` pre-dated Felhom (we correctly keep it). **Pre-existing, not
|
||||
introduced here**, filed as **R-317**, and the uninstall now says so out loud rather than leaving it to
|
||||
be found from a resolver that never came up. Fixing it is a one-line agent change, deliberately **not**
|
||||
made here to keep this session to one repo.
|
||||
|
||||
## 7. Publication
|
||||
|
||||
`installer-v1.28.0` tagged; **both** `--ref`s in `manifests/webpage.yaml` moved (sidecar 327, init 372);
|
||||
ArgoCD synced to `823cd29`; live deployment carries the new ref.
|
||||
|
||||
```
|
||||
OLD code (opens retained row 11) HTTP 422 opens_retained: True
|
||||
superseded_at: 2026-08-12T15:18:55Z
|
||||
retained_has_restic_pw: True
|
||||
"the recovery code is correct, but it belongs to an
|
||||
EARLIER sealed package (superseded …), not the one
|
||||
currently held"
|
||||
WRONG code (negative control) HTTP 400 "the recovery code did not open the sealed bundle"
|
||||
live URL serves: SCRIPT_VERSION="1.28.0" (was 1.27.0)
|
||||
_dnsmasq_purge_owned present in the served file: 3
|
||||
```
|
||||
|
||||
The hub half measured directly too: `GET …/escrow/retained` → **200**, `count=2`,
|
||||
**`unopenable_count=1`** — that one being retained row id 4, the pre-v0.93.0 row whose material R-198
|
||||
destroyed. The withholding rule is doing exactly what it was written for, on real data.
|
||||
**Pushing publishes nothing here** — `/scripts/` follows the tag. The runbook still says otherwise;
|
||||
**R-309 remains open and is not forgotten**, deliberately out of scope.
|
||||
|
||||
**What was NOT walked, and why.** The customer's rendered sentence was **not** produced end-to-end.
|
||||
`recoveryUnlockHandler` redirects to `/backups/remote` when `!recoveryOffer()`, and `demo-felhom`
|
||||
holds its own repository password again (restored yesterday), so it is correctly **not** in the
|
||||
offered state. Walking it would mean removing that password to fake a rebuilt box — destabilising a
|
||||
healthy machine to render a sentence whose logic is pinned by six handler tests and whose upstream 422
|
||||
is proven live. I did not. **Method stated: endpoint-level for the agent and hub, handler-level for the
|
||||
message.** What the customer DOES see on this box today is the orphan card, and it is honest:
|
||||
*„Megnyitni innen egyelőre nem lehet, és ez nem a kódodon múlik."*
|
||||
## 8. Teardown — four layers
|
||||
|
||||
### A correction I have to make about my own last report — R-308 was wrong
|
||||
| Layer | State |
|
||||
|---|---|
|
||||
| **machine** | `drill-r50`: 0 guests, no `/var/lib/felhom-install`, dnsmasq `not-installed` at the end |
|
||||
| **host** | `drill-r50` **reverted to snapshot `virgin`, powered off**; `qemu.pid` removed. DooPlex was drill host only |
|
||||
| **hub** | `drill-r50-0a4f9a` **RETAINED** — pre-existing since 2026-07-25, re-used not duplicated. **No new host or customer row was created this session.** Nothing deleted |
|
||||
| **off-site** | **Not contacted at all** — no restic, sftp or endpoint call was made. `demo-felhom`: `last_status=ok`, `escrow=escrowed`, **no abandon countdown** |
|
||||
|
||||
I reported that the stored controller password no longer opens `demo-felhom`. **It does.** I had
|
||||
stripped only DOUBLE quotes from the `~/.config/credentials` value; the values are wrapped in
|
||||
**SINGLE** quotes, so I was sending a literal `'` as part of the password. Unquoted correctly it is 13
|
||||
characters and logs in first try — **HTTP 302 with a session cookie**.
|
||||
Both demo boxes untouched and healthy: controller **0.214.0**, agent **0.129.0**, `Up … (healthy)`.
|
||||
|
||||
The same bug then made this session's first live R-311 test read as a **failure** (HTTP 400) for
|
||||
twenty minutes, and I nearly filed the fix as broken. It is the **third** wrong "the credential is
|
||||
stale" verdict this project has produced from that one trap. R-308 is **withdrawn**; the real lesson
|
||||
is filed with it — never let a shell decide what a secret is.
|
||||
## 9. Evidence, and a repeated miss
|
||||
|
||||
## 7. What was dropped, named plainly
|
||||
Logs at `documentation/audits/evidence-r316-2026-08-13/`: the fixed cycles (`F1`, `F2`, `F3`), all four
|
||||
scenarios (`B`, `C`, `D`), and every red-proof (`RPA*`, `RPB*`, `RPD*`).
|
||||
|
||||
- **Part 3 (the route) — HALTED at the spike, by the task's own rule.** → R-312.
|
||||
- **§7's fixture walk was not re-run end-to-end.** Yesterday's drill already proved the byte-identical
|
||||
restore from a set-aside store; today's change is upstream of it (which sentence is shown), the
|
||||
dashboard is unreachable headlessly (R-308), and the restore route does not exist (R-312). What was
|
||||
proved live is the 422 itself.
|
||||
- Explicitly out of scope and still open, so it does not read as forgotten: **R-305** (the removal fix
|
||||
helps a machine once — the tester's second reinstall still hits it), the hub emails naming the
|
||||
retired secret, **R-309** (the runbook's publication claim), the CI runs that fail with no log, the
|
||||
twenty unread facts, the nine grey claims, **R-303**.
|
||||
**The Part 1 logs did not survive.** They were on the drill VM's disk and were destroyed by the revert
|
||||
to `virgin` between Part 1 and Part 2 — **the same mistake as Tuesday, in the same place.** The §1
|
||||
quotation is verbatim from the live run as read at the time, and `RPA1/RPA2/RPA3.log` are an
|
||||
independent reproduction of the identical three-cycle failure, retained. Recorded rather than glossed;
|
||||
the fix is procedural and I have now got it wrong twice.
|
||||
|
||||
## 8. Bypass, stated as required
|
||||
## 10. Observations — noticed, not acted on
|
||||
|
||||
`git push --no-verify` was used **once**, on `felhom-agent`. The `release-complete` gate refuses a
|
||||
CHANGELOG entry whose tag and package do not exist; `release-agent.sh` refuses a tree that is not
|
||||
pushed. Circular by construction. The bypass was immediately followed by the real release
|
||||
(`release-agent.sh 0.129.0`), and the gates were re-run afterwards: **green**.
|
||||
- **`dnsmasq-base` is flagged "automatically installed and no longer required"** after `dnsmasq` goes.
|
||||
We deliberately do not `autoremove` — that would be a blast radius nobody asked for.
|
||||
- **`apt-get -s purge` exits 0 even when it prints `E: dpkg was interrupted`.** The simulation output is
|
||||
the signal, never the exit code — which is why the guard parses `Remv`/`Purg` lines rather than
|
||||
trusting `$?`. Another entry for the exit-codes-that-lie class.
|
||||
- The drill VM's `pveam` index is stale on `virgin` and needs `pveam update` before listing templates —
|
||||
already recorded yesterday, hit again today in passing.
|
||||
|
||||
Reference in New Issue
Block a user