Reassess SCOPE against the current revision

SCOPE.md declared itself reviewed against b989de4, 63 commits behind, with its
limits kept current by appending to bullets. That works until the header asserts
a review date that no longer describes what was reviewed -- the drift this file
exists to prevent, in the document whose only job is to be accurate.

Every claim was rechecked against source rather than carried forward: routes from
the mux registrations, subcommands from the dispatch, flags from the flag sets,
claim behaviour from token.go and userinfo.go. Three were false. 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. Capabilities that had
reached SCOPE only piecemeal are now in the capability table: the
authorization-code bindings, upstream token verification, readiness and drain, the
human tenant rules, authentication-time assurance, the shared verifier.

The substantive addition is a distinction SCOPE did not draw: source behaviour
and deployed behaviour are different claims with different evidence -- the test
suite and harness runs for one, committed receipts in docs/evidence/ for the
other. What is proven live is narrower and now stated exactly, including that the
issuer pin's pre-pin state did not match, so that risk was real rather than
hypothetical, and that the deployed image predates this revision so several
described behaviours are running nowhere yet.

That distinction is the fix for a repeated error: four times in two days a claim
about current state was written from a workplan paragraph or a partial view
instead of the authoritative artifact. A SCOPE that does not separate "the code
does this" from "this is running" invites the same mistake from the next reader.

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
This commit is contained in:
tegwick 2026-09-09 20:06:38 +02:00
parent 7a73352368
commit d6ecb2b7aa
3 changed files with 210 additions and 11 deletions

View file

@ -1,6 +1,8 @@
# SCOPE
Reviewed 2026-09-05 against source revision `b989de4`.
Reviewed 2026-09-09 against source revision `5ae80d3`. The preceding review was
2026-09-05 against `b989de4`; the [reassessment](history/2026-09-09-scope-reassessment.md)
records what changed and how each claim below was checked.
## Purpose and boundary
@ -20,11 +22,11 @@ Keycloak interchangeability are not established.
| Surface | What the current source provides |
| --- | --- |
| Issuer HTTP API | Discovery, `/authorize` and its callback/registration-return routes, `/token`, `/jwks`, `/userinfo`, local `/logout`, and `/healthz`. |
| Human authentication | Authorization code with S256 PKCE, exact registered redirects, scope allow-lists, Authelia login, privacyIDEA MFA challenges, client-specific assurance requirements, browser session reuse, step-up/fresh-login handling, and configured registration/enrollment handoffs. |
| Issuer HTTP API | Discovery, `/authorize` and its callback/registration-return routes, `/token`, `/jwks`, `/userinfo`, local `/logout`, `/healthz` (liveness, probes nothing) and `/readyz` (probes LLDAP by bind, Authelia and privacyIDEA). |
| Human authentication | Authorization code with S256 PKCE, exact registered redirects, scope allow-lists, Authelia login, privacyIDEA MFA challenges, client-specific assurance requirements, browser session reuse, step-up/fresh-login handling, and configured registration/enrollment handoffs. The exchange is bound to the redirect URI the code was issued for, refused for clients not registered for the grant, authenticated for confidential clients, and the code is consumed atomically so exactly one exchange can succeed. Upstream Authelia ID tokens are verified — signature against the provider's published keys, expected issuer, own client ID in the audience, validity window — failing closed and independently of transport. |
| Service authentication | Static confidential `client_credentials` clients authenticated with form-encoded `client_secret_basic`; configured subject, tenant, roles, scopes and per-client token lifetime. Secrets are resolved from environment references at startup. |
| Tokens and identity | Locally signed RS256 JWTs; configurable access-token resource audience while ID tokens retain the client audience; human/service principal types, tenant, groups, roles, scope and assurance claims. UserInfo resolves canonical directory subjects and filters profile/email/groups by scope. |
| Caller commands | `keycape login` for public-client browser PKCE login and `keycape service-token` for service exchange. HTTPS discovery/JWKS verification and private JSON token-file delivery outside Git; no token output on stdout. |
| Tokens and identity | Locally signed RS256 JWTs; configurable access-token resource audience while ID tokens retain the client audience; human/service principal types, tenant, groups, roles, scope and assurance claims. A service tenant is bound at registration; a human tenant comes from the directory, or from a client-declared zone when the directory has placed the user nowhere, and issuance is refused when the two conflict. `assurance.at` is the authentication time, not the mint time. UserInfo verifies algorithm, issuer and access-token purpose, resolves canonical directory subjects, and filters profile/email/groups by scope. |
| Caller commands | `keycape login` for public-client browser PKCE login, `keycape service-token` for service exchange, and `keycape verify-client` to check a live registration's subject, tenant, roles, lifetime and scope refusals without emitting values. HTTPS discovery/JWKS verification and private JSON token-file delivery outside Git; no token output on stdout. |
| Validation and migration | Canonical snapshot checks; deterministic LLDAP user/group/membership export that records whether it enumerated the whole directory; basic Keycloak realm JSON; LDIF generation for OpenLDAP, 389 Directory Server and AD targets. These generate artifacts rather than execute a complete migration. |
| Diagnostics and packaging | Structured authentication/enforcement/migration events, liveness and dependency-probing readiness endpoints, Go build/test/vet targets, a container packaging the issuer and all four migration/validation binaries, and development/CI scaffolding. |
@ -92,6 +94,23 @@ Keycloak interchangeability are not established.
unexercised, and credential/MFA migration is not supplied at all, so no harness
can establish it. Subject continuity is explicitly not preserved: the canonical
ID survives as an attribute while Keycloak mints its own `sub`.
- **Source behaviour and deployed behaviour are not the same claim, and this
document describes source.** Everything above is what the code in this revision
does, established by 390 Go tests across 23 packages plus the harness runs
named. What has been demonstrated against the live deployment is narrower and is
recorded as committed receipts in `docs/evidence/`: the upstream issuer was
probed and then pinned in `sso/keycape-config`
(`2026-09-09-upstream-issuer-pin.json` — the pre-pin state did **not** match, so
this was a real fix), and both approval service clients were verified live in an
attended owner process, showing JWKS signature verification, exact claims,
excess-scope and wrong-secret denial at a 900s lifetime
(`2026-09-09-keycape-verifier-admission.json`). Those receipts also decline what
they did not test: no real predecessor rotation and no observed wall-clock
expiry. The deployed image is older than this revision, so the
authorization-code bindings, upstream verification, readiness, graceful
shutdown and the two tenant-related changes are **not yet running anywhere**
see [operations](docs/operations.md), "Unreleased fail-closed startup changes".
Read `docs/evidence/` before describing any of this as proven live.
- The server listens on HTTP; HTTPS termination is deployment-owned.
`/healthz` is liveness and probes nothing; `/readyz` probes LLDAP, Authelia and
privacyIDEA and gates traffic. `SIGTERM` drains in-flight requests for 15s.
@ -124,23 +143,31 @@ or factor enrollment themselves.
Run the issuer with `keycape --config PATH`; `keycape server` and
`keycape migrate` are not implemented subcommands. Caller commands are
`keycape login` and `keycape service-token`.
`keycape login`, `keycape service-token` and `keycape verify-client`.
Separate binaries under `src/cmd/` are `validator`, `lldap-export`,
`keycape-to-keycloak`, and `lldap-to-ldap`. `make -C src build` places binaries in
root `bin/`; the container packages only `keycape`. Tests live under `src/tests/`
and alongside packages in `src/internal/`.
root `bin/` — the root `make build` only compiles and installs nothing. The
container packages all five binaries with the issuer as its entrypoint
(KEY-WP-0026). Tests live under `src/tests/` and alongside packages in
`src/internal/`.
`make test`, `make lint` (Go vet), `make build`, and `make contract-test` are the
root checks. The capability-contract check requires the sibling NetKingdom
validator. These checks passed for the reviewed revision in the preceding
implementation session; the scope assessment is a source/documentation review.
implementation session and were rerun for this one; `src/tests/conformance` is
additionally runnable against a live issuer via `KEYCAPE_TEST_ISSUER` and skips
when it is unset. The scope assessment is a source review: it establishes what
the code does, not what any deployment is currently running.
Use this repo for its bounded issuer, caller JWT acquisition, identity adapters
and migration preparation. Consult [INTENT.md](INTENT.md) for the architectural
target, [native authentication](docs/native-authentication.md) for caller
constraints, and the [scope/intent assessment](history/2026-09-05-011726-scope-intent-assessment.md)
for evidence, priorities and remaining gaps.
constraints, [operations](docs/operations.md) for the supported topology and the
unreleased fail-closed changes, `docs/evidence/` for what has been proven against
the live deployment, and the [reassessment](history/2026-09-09-scope-reassessment.md)
— with the [2026-09-05 assessment](history/2026-09-05-011726-scope-intent-assessment.md)
it supersedes — for evidence, priorities and remaining gaps.
## Provided capabilities

View file

@ -0,0 +1,96 @@
# KeyCape scope reassessment — 2026-09-09
Source baseline: **5ae80d3**. 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.
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.

View file

@ -0,0 +1,76 @@
---
id: KEY-WP-0029
type: workplan
title: "Reassess SCOPE against the current revision"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: scope-reassessment
created: "2026-09-09"
updated: "2026-09-09"
---
SCOPE.md declared itself reviewed against `b989de4`, 63 commits behind. Its
limits section had been kept current by appending to bullets, which works until
the header asserts a review date that no longer describes what was reviewed.
## Recheck every claim against source
```task
id: KEY-WP-0029-T01
status: done
priority: medium
```
Routes read from the mux registrations, 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`. Where the existing prose and the
code disagreed, the code won.
Three claims were false and are corrected: caller commands omitted
`verify-client`; the container was described as packaging only `keycape`, untrue
since KEY-WP-0026; and the issuer API row omitted `/readyz`. Capabilities that
had only reached SCOPE piecemeal — the authorization-code bindings, upstream
token verification, readiness and drain, the human tenant rules,
authentication-time assurance, the shared verifier — are now stated in the
capability table rather than only in the limits prose.
## Separate source behaviour from deployed behaviour
```task
id: KEY-WP-0029-T02
status: done
priority: medium
```
The document now says plainly that it describes source, and that the two are
different claims with different evidence: the test suite and harness runs for
one, committed receipts in `docs/evidence/` for the other.
What is proven live is narrower and is stated exactly — the issuer pin, whose
pre-pin state did *not* match, so that risk was real rather than hypothetical;
and the two approval clients verified in an attended owner process. Both receipts
decline real predecessor rotation and observed wall-clock expiry, and SCOPE
declines them too. The deployed image predates this revision, so several
behaviours SCOPE describes are running nowhere yet, two of them fail-closed at
startup.
This is the correction that matters. Four times in two days 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.
A SCOPE that does not distinguish "the code does this" from "this is running"
invites exactly that error from the next reader.
## Record the reassessment
```task
id: KEY-WP-0029-T03
status: done
priority: low
```
`history/2026-09-09-scope-reassessment.md` records the method, what changed since
2026-09-05, the corrections, and what remains. It supersedes rather than replaces
the 2026-09-05 assessment, which stays as the record of the ten-gap backlog and
its closures; both are linked from SCOPE.