Closes gap G08. /healthz returned a constant without probing anything, the server called ListenAndServe with no signal handling, and the operational limits of in-memory state, startup-loaded keys and local-only logout lived in code comments rather than anywhere an operator would look. /readyz probes LLDAP, Authelia and privacyIDEA; /healthz stays liveness and probes nothing. Keeping them distinct matters: wiring liveness to dependency health means an orchestrator restarts KeyCape when a dependency blinks, and a restart also discards every in-flight login, so the reaction is worse than the condition it reacts to. LLDAP is probed with a bind rather than a dial, since a rotated or revoked service password leaves the port open and every lookup failing -- exactly what readiness should catch and exactly what a dial would miss. The response names the failing check but never the reason: the endpoint is unauthenticated and upstream error text carries hostnames and sometimes credentials-in-URLs. Results are cached for 2s so an unauthenticated endpoint cannot be used to drive unbounded upstream traffic, and probes run concurrently under a 3s bound so a hung dependency makes the endpoint answer rather than hang with it. SIGTERM and SIGINT now drain in-flight requests for 15s, under the 30s read/write timeouts so a stuck request cannot outlive the window before SIGKILL. docs/operations.md states the single-replica topology and why, and three limits easy to get wrong: the constant key-1 key ID makes same-kid rotation a trap for consumers caching JWKS, removing a client does not revoke its issued tokens, and /logout is local only. No throughput figures are given, since nothing here benchmarks KeyCape. Shared storage and refresh tokens stay excluded, as G08 allows. Verified in the running executable: 503 naming all three checks failed while /healthz returned 200, the LLDAP check flipping to ok once started, and 40/40 requests succeeding across a SIGTERM. 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
11 KiB
SCOPE
Reviewed 2026-09-05 against source revision b989de4.
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, 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. |
| 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. |
| 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, a process health response, Go build/test/vet targets, a container containing the KeyCape binary, 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_rolesadapter is wired through atenantEngineconfig block and is off unlessbaseURLis set (KEY-WP-0024). When enabled it fails open: an unreachable source omits the claim rather than failing issuance, sotenant_rolesis 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
groupEnumerationfield 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
-clientsconfig 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 ownsub, 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_urion 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/conformancetargets a live issuer named byKEYCAPE_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 excludedimplicitandpasswordgrants, 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 ownsub. - The server listens on HTTP; HTTPS termination is deployment-owned.
/healthzis liveness and probes nothing;/readyzprobes LLDAP, Authelia and privacyIDEA and gates traffic.SIGTERMdrains 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 configuration/key material absent from the checkout. Production deployment and custody are external; a source implementation or example client fragment does not prove live registration or consumer cutover. - Approval-client provisioning, coordinated Qonto rotation, native consumer handoffs and notification receipt closure remain open in KEY-WP-0009/0013/0014.
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 and keycape service-token.
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/.
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.
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, and the scope/intent assessment 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]