key-cape/SCOPE.md
tegwick d6ecb2b7aa 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
2026-09-09 20:06:38 +02:00

14 KiB

SCOPE

Reviewed 2026-09-09 against source revision 5ae80d3. The preceding review was 2026-09-05 against b989de4; the reassessment records what changed and how each claim below was checked.

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. 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). 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, "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 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 for the architectural target, native authentication for caller constraints, operations for the supported topology and the unreleased fail-closed changes, docs/evidence/ for what has been proven against the live deployment, and the reassessment — with the 2026-09-05 assessment it supersedes — for evidence, priorities and remaining gaps.

Provided capabilities

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]
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]
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]