Files
windy-git/docs/AUDIT-fable-2026-08-13.md
Grant Whitmer 830ca48705
All checks were successful
check / gate (push) Successful in 19s
docs: mark revocation finding resolved — it was critical, not low
Co-Authored-By: Claude (Fable 5) <noreply@anthropic.com>
2026-08-13 23:20:46 -04:00

96 lines
5.3 KiB
Markdown

# Second-auditor review of the Windy Git build (Fable, 2026-08-13)
A fresh-eyes trace of Opus's build, **verified against the live system** rather
than against the transcript's own account. One critical finding was fixed during
the review; the rest are recorded here with proportion.
## Fixed during this review
### 🔴 Agent authentication was not authentication (CRITICAL, was live+public)
`auth.py` read the passport out of a bearer token **without verifying the EPT
signature**, asked Eternitas *"is this passport reputable?"*, and seated the
caller on a yes. That answers reputation, not possession.
**Proven by exploit:** a forged `alg:none` token naming a passport lifted from
the logs returned **HTTP 200** as that agent on the public API. Anyone who knows
a passport number (they appear in logs, the lockbox, and revocation messages)
could impersonate that agent.
The human path already failed closed for exactly this reason
(`require_verified_jwt`) — but the gate sat *after* the agent branch returned, so
it protected the safe path and skipped the exploitable one.
**Fix:** the agent path now fails closed in production, *before* the trust
lookup, mirroring the human path. Reopens automatically when the ES256/JWKS
verifier (G3.2/G9.1) is built. Re-tested live: forged token now `503`. Added a
**behavioral** test (forged token through real `get_caller` must raise) and a
**canary probe** (forged token must stay refused; a 2xx pages).
## Open findings (recorded, not yet fixed)
### 🟠 The EI throttle table is dead code (MEDIUM)
`BAND_MULTIPLIER` and `rate_*_per_day` (G3.4) are defined and **read by
nothing** — verified by grep. An authenticated agent has no rate limit at all.
When the agent path reopens with real verification, wire the throttle before it
does, or a single agent can hammer repo-create/token-mint unbounded.
### 🟠 The test suite is mostly source-string assertions (MEDIUM)
56 invariant tests carried **~86 "does the source contain this string"**
assertions and **zero** that exercised the auth decision. `make check` green
means the code still *contains* the patterns, not that it *behaves*. The auth
bypass passed every test. **Direction:** convert the top ~10 guards into
behavioral tests against an ephemeral instance (this review added the first
two). The live-curl proofs Opus ran were excellent but were never captured, so
they don't defend against regression.
### 🟠 Privileged dind sits beside broad-scoped tokens (MEDIUM — Grant-aware)
`dind privileged=true`, and the host `.env` holds the account-wide R2 token and
a GitHub PAT, on the same box running untrusted CI. Escape from privileged dind
→ host fs → both tokens. Grant waived *minting a new token* for the sandbox; the
specific escalation path (privileged-dind-next-to-god-token) is a separate
decision. Lowest-effort mitigations: rootless/sysbox runner, or move the two
tokens out of the API container's env into a path the API reads but the runner
host does not share.
### ✅ Revocation — was WORSE than flagged, now fixed (was CRITICAL once agent auth reopened)
Re-examined after real agent auth went live, and the finding grew teeth. A
revoked passport returns **HTTP 200, `status: revoked`, `band: unproven`,
`allowed_actions: []`** (verified live). `resolve_passport` keyed refusal only on
HTTP 4xx and `band=="untrusted"`, so it returned band `unproven` and **seated the
revoked agent** — revocation was not enforced on the live path at all. The
webhook everyone worried about was only ever cache-invalidation; the live trust
lookup was the real gate, and it wasn't checking.
**Fixed:** `decide_trust` now allows only `status=="active"`; revoked / suspended
/ frozen / unknown all refuse, fail-closed. Revocation now takes effect on the
next request, no webhook required. Eternitas additionally refuses to mint EPTs
for revoked bots, so the residual window was a pre-existing ~365-day token —
exactly what the live check now stops. 79 tests green including the full wiring.
The webhook (`webhook_secret` lost to the 500) is now genuinely LOW: it only
matters for locally-issued credentials/grants (G6.3, not built), and it is no
longer the thing standing between a revoked agent and access.
## What is genuinely strong (and worth protecting)
- **Honesty engineering is real:** fail-closed providers, `/health` refusing to
claim green it can't prove, I-12's baked-sha proven live against a hostile env
var. This directly cures the parent ecosystem's #1 root cause.
- **Incident response was first-rate:** the login outage was root-caused to a
510-deep accept queue and a per-query node fork, with the "one function, not
468 call sites" reframe that is correct and valuable.
- **It caught its own mistakes** — the mirror direction and the push-triggered
deploy workflows, the latter *before* they fired.
- **The DR posture is right:** 131 read-only mirrors (no CI, zero deploy risk) +
a rehearsed restore. Rehearsed restore is rare in this ecosystem; keep it.
## The one-line lesson
Opus aimed its considerable discipline at **honesty and documentation**
(excellent) more than at **adversarial correctness** — so the property that
mattered most, *is an agent really that agent*, shipped inverted and untested.
The remedy is not more process; it is **behavioral tests and canary probes for
the security-critical paths**, so verification persists instead of living in a
transcript.