key-cape/history/2026-09-09-scope-reassessment.md
tegwick f12d24e079 Name the baseline the reassessment actually covers
The header cited 5ae80d3, but 7a73352 landed 58 seconds earlier, so a reader
applying the convention would ask what changed since 5ae80d3, find that commit
and be unable to tell its substance was already accounted for. Honest about what
was read and misleading about what is covered, which is the failure the
convention exists to prevent.

7a73352 changes documentation and workplans only, no source, and its substance
reached this document as a peer report before being confirmed by reading the
commit. The baseline now names it and records that distinction rather than
smoothing it over: read and told are different, which is the same distinction the
third state draws and the one behind four corrections this week.

Also records why the unreleased fail-closed changes stay a pointer to
docs/operations.md rather than being restated here. That section is built from a
deployed digest, a config resource version and a dated re-check note -- facts
with a short shelf life. SCOPE is a durable claims document, and putting
perishable facts in a durable one is how it drifted 63 commits to begin with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-09 20:11:56 +02:00

124 lines
6.7 KiB
Markdown

# KeyCape scope reassessment — 2026-09-09
Source baseline: **7a73352**. The capability review was performed against
`5ae80d3`; `7a73352` landed 58 seconds earlier, touches documentation and
workplans only, and its substance arrived here as a peer report before being
confirmed by reading the commit. The baseline names it so a reader asking "what
changed since?" gets an answer that is not immediately misleading, and the
distinction between read and told is recorded rather than smoothed over — it is
the same distinction the third state below draws, and the one that cost four
corrections this week.
Supersedes the
[2026-09-05 assessment](2026-09-05-011726-scope-intent-assessment.md) against
`b989de4`, which remains the record of the ten-gap backlog and its closures.
## Why now
SCOPE.md still declared itself reviewed against `b989de4`, 63 commits behind. Its
limits section had been kept honest by appending to bullets, which works until the
header asserts a review date that no longer describes what was reviewed. That is
the drift this document exists to prevent, in the file whose only job is to say
accurately what this repository does.
## Method
Every capability claim was checked against source at this revision rather than
carried forward from the previous text: routes from the mux registrations in
`cmd/keycape/main.go`, subcommands from the dispatch in `main.go` and
`internal/authclient/cli.go`, flags from the flag sets, claim behaviour from
`token.go` and `userinfo.go`. Live claims were checked against `docs/evidence/`.
Where the existing prose and the code disagreed, the code won.
That method was chosen because the opposite failed repeatedly over the preceding
two days: four times a statement about current state was written from a workplan
paragraph, a partial `git status`, or an inference about what was admitted,
without reading the authoritative artifact. Every one was wrong and every one was
cheap to check.
## What changed since 2026-09-05
Gaps G01 through G09 are closed; G10 is external and remains open. The
capabilities that reached SCOPE only piecemeal, now stated:
- the authorization-code grant binds the redirect URI, refuses clients not
registered for it, authenticates confidential clients and consumes codes
atomically; UserInfo checks algorithm, issuer and token purpose (G01);
- upstream Authelia ID tokens are verified — signature, issuer, audience,
validity window — failing closed and independently of transport, on an explicit
operator decision not to police the transport instead (G01);
- `/readyz` probes LLDAP by bind, plus Authelia and privacyIDEA, distinct from
`/healthz`; `SIGTERM` drains for 15s (G08);
- `keycape verify-client` checks a live registration's subject, tenant, roles,
lifetime and scope refusals without emitting values;
- a human tenant may be supplied by a client registration where the directory has
placed the user nowhere, and issuance is refused when the two conflict;
`assurance.at` is the authentication time rather than the mint time;
- `internal/jose` is the single RS256/JWKS verifier behind both the caller path
and upstream verification;
- the container packages all five binaries, and the dev stack is bootstrappable
from a clean checkout.
Three corrections to claims SCOPE previously made: caller commands omitted
`verify-client`; the container was said to package only `keycape`, untrue since
KEY-WP-0026; and the issuer API row omitted `/readyz`.
## The distinction this revision adds
SCOPE now states plainly that it describes **source**, and that source behaviour
and deployed behaviour are different claims. The evidence for each is different
too: 390 Go tests across 23 packages and the named harness runs for the first;
committed receipts in `docs/evidence/` for the second.
What is actually proven live is narrower than what the source does, and worth
stating exactly:
- the upstream issuer was probed and pinned in `sso/keycape-config`. The pre-pin
state did not match, so the fail-closed login outage that risk described was
real and was averted rather than hypothetical;
- both approval service clients were verified in an attended owner process: JWKS
signature verification, exact claims, excess-scope and wrong-secret denial at a
900s lifetime, no credential values emitted.
Those receipts also decline what they did not test — no real predecessor rotation,
no observed wall-clock expiry — and this document declines it with them. They also
force a distinction SCOPE had no vocabulary for: *implemented and unproven*, a
third state between missing and done. `verify-client`'s predecessor rejection is
written and unit-tested and has never run against a genuinely distinct
predecessor, so the repository holds a receipt that reads like rotation evidence
and is not. Both owners state that limit independently, which is why it belongs
in SCOPE rather than only in a workplan.
One perishable-fact boundary, decided rather than defaulted: the unreleased
fail-closed startup changes are described in `docs/operations.md` and only
pointed at from SCOPE. That section is built from a deployed image digest, a
config resource version and a dated "re-check if it changed" — facts with a short
shelf life. SCOPE is a durable claims document, and putting perishable facts in a
durable one is how it drifted 63 commits in the first place. The pointer stays a
pointer.
One framing correction worth propagating, from the custody owner: client-side
retrieval of those secrets is unadmitted and remains so, but the attended
operator path is not a client-side read and never required one. Conflating the
two is what left T02 recorded here as waiting on a run that had already happened.
The deployed image predates this revision, so the authorization-code bindings,
upstream verification, readiness, graceful shutdown and both tenant-related
changes are running nowhere yet. Two of them fail closed at startup, which is why
they are listed in `docs/operations.md` under "Unreleased fail-closed startup
changes" rather than only in a hub message.
## What remains
G10 is owner work: KEY-WP-0013-T05 waits on `informed-decision` for the approver
client ID and deployed callback, and KEY-WP-0014-T04 has no admitted execution
authority for a real predecessor rotation. Neither can be closed from this
repository.
From G04, relying-party behaviour and MFA against a migrated realm are still
unexercised, and credential and MFA migration is not supplied at all, so no
harness can establish it. Complete profile conformance and drop-in Keycloak
interchangeability remain unestablished — and the conformance suite found a
concrete reason they are not the same question: stock Keycloak advertises the
`implicit` and `password` grants this profile excludes, as server capabilities no
realm configuration removes.