diff --git a/CLAUDE.md b/CLAUDE.md index dd9d2ea..27f5667 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # CLAUDE.md Working notes for an agent picking this up. Read `flit-spec.md` §13 before -changing anything — it encodes nine change guards and fourteen standing rules, +changing anything — it encodes nine change guards and sixteen standing rules, each derived from a defect that actually occurred here. ## The rule that outranks everything else here @@ -132,6 +132,13 @@ Each of these was a deliberate decision; changing it re-breaks something. - **`laptop/healthchecks.sh` and `laptop/backup-credentials.sh` are not in `server/lib/`** — they need a laptop-only key and an authenticated vault session. A server-side script cannot do that work. +- **`tailscale-api-token` lives in the operator vault, not the deployment + vault** — it is full-access, so it can delete or reconfigure every node in the + tailnet including the laptop, and the server's token is granted viewer on + `VAULT_NAME` alone. Moving it to `VAULT_NAME` for symmetry with the two + Tailscale auth keys makes it readable by the server; that was confirmed + against the live box while it briefly sat there. `laptop/prune-tailnet.sh` is + in `laptop/` for the same reason. - **Escrow uses the operator's authenticated session, not the access token** — the token is read-only by design. Widening it would let an on-disk credential rewrite the whole vault (§6.2). diff --git a/flit-spec.md b/flit-spec.md index 4c6c938..db0d185 100644 --- a/flit-spec.md +++ b/flit-spec.md @@ -4,7 +4,7 @@ **Audience:** An agent executing the build, with a human available to approve interactive steps. > **Amending this spec? Read §13 first.** It encodes seven review techniques, two checks that -> only work once an implementation exists, and fourteen standing rules — each derived from a defect +> only work once an implementation exists, and sixteen standing rules — each derived from a defect > that actually occurred here. Several of those defects were introduced *by fixes to earlier > ones*, so amendments carry the same risk as the original. @@ -1089,7 +1089,8 @@ The two deferrals that path leaves behind — removing the superseded restic key promoted `-next` vault items — are closed out by hand, not by `laptop/backup-credentials.sh`: `restic key remove` drops the superseded key from the repository, and the `-next` items, once confirmed byte-identical to what they were promoted over, move to the vault's trash. -`confirm()`'s interactive removal branch is therefore still unexercised in practice, and +The restic-key removal behind `confirm()` is therefore still unexercised in practice — `confirm()` +itself has since run interactively elsewhere, which is how standing rule 16 in §13 was found — and `lib/common.sh` still has no vault-deletion primitive — both remain deferred as a design fact for the next rotation. The superseded Storage Box public keys were untouched by any of this run: `storagebox_withdraw_key()` did not exist yet. Withdrawal is scripted now — see the rotation design @@ -2161,6 +2162,25 @@ Violating any of these has broken something before: matches what it captured. The gap between "image taken" and "rotation run" produces no failure until something restores from that image, so it ships green and fires only during a real recovery. +15. **Name resolution is not reachability, and a probe by name must fall back to an address.** + A bare MagicDNS name stops resolving whenever the laptop loses the tailnet search domain — + macOS on a phone hotspot is the ordinary case — while the node stays up and answers on its + tailnet address. Three sites treated the two as one thing: `bootstrap.sh` + `phase_2_deliver()` concluded the tailnet was down and fell back to a public address that a + converged box firewalls, so the run hung rather than failed; `phase_3_network()` refused to + close port 22 over a tailnet that was not broken; and `lib/drill-common.sh` `drill_wait()` + spent forty attempts before blaming the ephemeral auth key. Ask `tailscale ip -4` before + giving up. Try the name first — when it works it proves what the operator types — and warn + when the address is what answered. This weakens no identity check: what those defend against + is a NAME reaching some other machine, and an address cannot be repointed by DNS. +16. **A prompt must not open its own reader on the terminal.** Every credential-touching script + here runs under `pass-cli run`, which hands its child a pipe for stdin and reads the terminal + itself to fill it. A prompt that reads `/dev/tty` directly becomes a second reader racing + that wrapper: the first answer typed is consumed by the wrapper and delivered into a pipe + nobody drains, so the prompt looks ignored and has to be answered twice. Read the stdin the + process was given. Writing the prompt to `/dev/tty` is a separate matter and still required, + because `run_log_start` routes fd 2 through an awk filter that emits only complete lines and + a prompt has no trailing newline. `lib/common.sh` `confirm()` is the one implementation. ### When an amendment is large