key-cape/SCOPE.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

212 lines
16 KiB
Markdown

# SCOPE
Reviewed 2026-09-09 against source revision `7a73352`. 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.
The capability review was performed against `5ae80d3`. `7a73352` landed a minute
before it and changes documentation and workplans only, no source; its substance
reached this document as a peer report and was afterwards confirmed by reading
the commit, which is why the baseline names it. That distinction — read versus
told — is worth keeping in a header whose whole purpose is to let a reader ask
"what changed since?" and get a usable answer.
## Purpose and boundary
KeyCape is Go identity **Tooling** for NetKingdom's lightweight IAM deployment.
It runs its own OIDC-style issuer, uses Authelia for upstream browser
authentication, reads identities from LLDAP, and delegates factor checks to
privacyIDEA. It also provides caller authentication commands, directory snapshot
validation, and migration artifact generators.
It issues identity, role, scope and assurance claims. Resource authorization
belongs to access-engine and consuming services; secret custody and OpenBao
policy/token enforcement belong to their platform owners. The implemented subset
supports NetKingdom integrations, but complete profile conformance and drop-in
Keycloak interchangeability are not established.
## Implemented capabilities
| Surface | What the current source provides |
| --- | --- |
| 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. 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. |
## Material limits
- Client registrations, service secrets and the signing key are loaded at startup.
Login, authorization and handoff state are process-local. There is no shared
session store, general refresh-token flow, token introspection/revocation API,
or automatic signing-key/client-secret rotation service. Logout clears the
local KeyCape session, not every upstream or downstream session/token.
- The optional tenant-engine `tenant_roles` adapter is wired through a
`tenantEngine` config block and is off unless `baseURL` is set (KEY-WP-0024).
When enabled it fails open: an unreachable source omits the claim rather than
failing issuance, so `tenant_roles` is a cache and must not be trusted for
privileged decisions.
- The LLDAP export enumerates the group subtree directly, so groups with no
members are present, and every snapshot carries a `groupEnumeration` field
saying whether that enumeration ran or the membership-derived fallback did
(KEY-WP-0018). A failed enumeration aborts the export rather than writing a
smaller snapshot, and read failures are reported rather than skipped. Users,
groups and memberships are sorted, so an unchanged directory exports
identically. Completeness beyond users, groups and memberships — passwords and
MFA credentials in particular — is still not covered.
- The Keycloak CLI takes a `-clients` config and migrates client registrations:
flows follow the declared grants, and audience, tenant, service subject and
roles are carried as protocol mappers with per-client lifetime, handoff URLs
and the secret *reference* as attributes (KEY-WP-0020). What it cannot carry it
names, in a report separate from the consistency check: secret values, MFA
policy enforcement, passwords and factor enrolment, and subject continuity —
Keycloak mints its own `sub`, so the LLDAP canonical ID survives only as an
attribute. The output is a reviewed artifact, not a proven migration; that
proof needs a live provider swap and is not established.
- Snapshot validation is a limited Go rule set, not full machine-readable schema
enforcement. Raw LDAP attribute keys are checked for descriptor validity,
canonical-mapping shadowing and case-only duplicates (KEY-WP-0021); attribute
values are not validated against a directory schema, and no allow-list of
permitted attribute names exists, since that field carries what the canonical
model does not name. The canonical YAML model and discovery metadata now match the
runtime client-registration surface and are held there by a conformance check
(KEY-WP-0017); the Go model is the runtime authority and the YAML the reviewed
contract. That is narrower than schema enforcement in general.
- The authorization-code grant now binds the redirect URI,
enforces grant-type eligibility, authenticates confidential clients and
consumes codes atomically, and UserInfo enforces algorithm, issuer and
access-token purpose (KEY-WP-0016). Upstream Authelia ID tokens are verified
before any claim is trusted — signature against the provider's published keys,
advertised issuer, own client ID in the audience, validity window — failing
closed and independently of transport (KEY-WP-0019). KeyCape deliberately does
not check or gate on the upstream transport. Enforcing these bindings
is not complete profile conformance. Relying parties must repeat `redirect_uri`
on the token exchange and present the access token, not the ID token, to
`/userinfo`; see [authorization-code bindings](docs/authorization-code-bindings.md).
See the assessment below.
- Tests cover local handlers, adapters, transformations and CLI protocol behavior.
They do not establish complete replacement against a running Keycloak/full-LDAP
stack. The Scenario B/C harnesses now run, and `src/tests/conformance` targets a
live issuer named by `KEYCAPE_TEST_ISSUER`, skipping when it is unset
(KEY-WP-0022). Run against Keycloak 26.0 it verified discovery, the
authorization surface, the published keys and a real token exchange — and
showed that stock Keycloak advertises the excluded `implicit` and `password`
grants, so it is not a drop-in for this profile. Directory migration is proved
end to end into a real OpenLDAP, with every migrated membership resolving, and a
realm built from a live export imports into Keycloak and serves discovery
(KEY-WP-0023). Relying-party behaviour and MFA against a migrated realm remain
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`), independently confirmed by the
custody owner. That is live evidence for those two clients specifically —
more than a source claim, less than a general one. On the custody boundary:
client-side retrieval of those secrets is unadmitted and stays so, but the
attended operator path is not a client-side read and never required one;
conflating the two is what left this work recorded as waiting on a run that had
already happened. Those receipts also decline what they did not test: no real
predecessor rotation and no observed wall-clock expiry. Those name a **third
state** this document otherwise has no words for — *implemented and unproven*,
distinct from both missing and done. `verify-client`'s predecessor rejection is
written and unit-tested but has never run against a genuinely distinct
predecessor, so this repository holds a receipt that reads like rotation
evidence and is not. Both owners state that limit independently. Read "there is
a test" and "it has been exercised against the real thing" as separate claims
wherever this document makes one. 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.
The supported topology is a single replica, since login and authorization state
is process-local — see [operations](docs/operations.md) for that and for the
key-rotation, client-removal and logout limits. Development
Compose needs key and Authelia material absent from the checkout by design;
`scripts/bootstrap-dev.sh` generates it locally and git-ignores it
(KEY-WP-0026). Production
deployment and custody are external; a source implementation or example client
fragment does not prove live registration or consumer cutover.
- Notification receipt closure is done: KEY-WP-0009 is finished, its four handoffs
delivered with receipts readable via `GET /messages/?from_agent=key-cape`.
Approval-client provisioning (KEY-WP-0013-T02) and coordinated Qonto rotation
plus the login-proxy consumer split (KEY-WP-0014-T04) remain open. Both requests
are now delivered and both wait on owners outside this repository —
railiance-platform for custody admission, approval-engine for the exact human
callback, ops-warden for the route contract and a rotation execution/rollback
authority. No client is materialized in the deployed image and no route changed.
## Deliberate exclusions
General-purpose IAM, dynamic client registration, implicit/password grants,
wildcard redirects, arbitrary identity brokering, resource authorization policy,
OpenBao credential custody, and Keycloak operations are outside this repository's
ownership. Registration/enrollment handoffs do not implement user provisioning
or factor enrollment themselves.
## Entry points and verification
Run the issuer with `keycape --config PATH`; `keycape server` and
`keycape migrate` are not implemented subcommands. Caller commands are
`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 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 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, [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
```capability
type: security
title: Bounded OIDC and service-token issuance
description: Implements static-client browser PKCE and client-credentials authentication with RS256 identity claims through Authelia, LLDAP and privacyIDEA; complete Keycloak interchangeability remains unproven.
keywords: [oidc, pkce, authentication, iam, sso, mfa, identity, service-token]
```
```capability
type: security
title: Verified caller JWT acquisition
description: Provides native browser login and service-token exchange commands with HTTPS discovery, JWKS verification and private token-file delivery.
keywords: [oidc, pkce, jwt, cli, authentication, jwks]
```
```capability
type: security
title: Directory validation and migration artifact generation
description: Checks canonical snapshots and produces LLDAP exports, basic Keycloak realm JSON and target-specific LDIF; does not perform or prove a complete live migration.
keywords: [migration, identity, lldap, keycloak, ldif, validation]
```