2026-09-05 01:27:16 +02:00
# KeyCape scope against intent — 2026-09-05
Assessment timestamp: **2026-09-05 01:17:26 CEST (+02:00)** .
Source baseline: **b989de4e90248ff0782a3e99bc12a6d0c01487a4** .
Inputs: [INTENT ](../INTENT.md ), the previous SCOPE, implementation, tests,
build/operational scaffolding, and open workplans. Result: updated
[SCOPE ](../SCOPE.md ). INTENT is unchanged.
## Assessment
KeyCape substantially implements its identity-tooling purpose: it is a real
issuer with browser and service authentication, identity adapters, MFA policy
handling and caller commands. Its ownership boundary is consistent with INTENT:
it produces identity claims and delegates resource authorization and custody.
The stronger maturity claims are not established. “Strict” complete profile
conformance, stable identity through replacement, “seamless migration,” and
interchangeability without application changes exceed current implementation and
proof. The original 23-task workplan's completion is historical delivery evidence,
not evidence that every current contract, migration path or operational need is
complete. The accurate posture is **an implemented lightweight authentication
subset with tested local behavior and significant conformance, migration and
operational gaps**.
## Method and evidence limits
This is a source and documentation assessment, not a new live security audit or
migration rehearsal. The preceding implementation session at this baseline passed
`make test` , `make lint` , `make build` and `make contract-test` . No code changed in
this assessment and those checks were not rerun solely for wording changes.
Links and source claims were checked directly. Passing tests describe the covered
behavior; they do not establish the missing properties below.
Deployment observations in KEY-WP-0013 are earlier, explicitly dated evidence;
no fresh rollout, secret read, production token exchange or Keycloak migration
was performed here. Native caller behavior was tested locally, not certified as
a completed live consumer handoff. Findings marked as static risks need focused
regression tests before claiming exploitability or remediation.
## Alignment with INTENT
| INTENT commitment | Assessment | Evidence and limit |
| --- | --- | --- |
| Lightweight authentication Tooling | Substantially implemented | [Server composition ](../src/cmd/keycape/main.go ), [OIDC handlers ](../src/internal/server/oidc/ ) and [adapters ](../src/internal/adapters/ ). KeyCape signs its own tokens; it is more than a packaged reverse proxy. |
| Versioned, implementation-independent contract | Partial | Runtime supports newer service/audience policy than the v0.1 machine model and discovery metadata describe (G02). |
| Strong constraints and explicit rejection | Partial | Exact redirects, PKCE, scope checks, client-secret validation and [enforcement middleware ](../src/internal/server/errors/enforcement.go ) exist. Important validation and code-consumption boundaries remain incomplete (G01). |
| Canonical identity normalization | Partial | LDAP identities and group-derived tenant/roles are mapped. Directory portability, complete export and schema enforcement remain limited (G03/G05/G06). |
| Complete migration and interchangeable modes | Not demonstrated | Basic transforms and fixture tests exist. Current identity/client policy is not preserved end to end and live replacement harnesses are incomplete (G03/G04). |
| Deterministic, testable behavior | Strong local coverage; operational limits | Handler and CLI tests exercise actual local protocol code. Shared state, dependency readiness, complete exports and live backend replacement are not demonstrated (G04/G05/G08). |
| Minimal, secure, operationally efficient deployment | Partial | Small Go/container implementation exists; no resource benchmark or production-readiness certification was established. Bootstrap scaffolding and custody lifecycle need work (G08/G09/G10). |
| Authentication without resource authorization ownership | Aligned in scope | Claim issuance and local client/MFA rules are authentication policy; resource decisions remain with access-engine/consumers. This does not prove that every estate caller follows the engine-only integration rule. |
## Gaps and closure criteria
### G01 — Protocol trust and authorization-code consumption need hardening
**Priority: high. Kind: implementation gap / static security risk.**
[TokenHandler ](../src/internal/server/oidc/token.go ) validates PKCE, client ID and
scopes, but the authorization-code path does not authenticate confidential
clients or compare the submitted redirect URI with the stored one. Grant-type
eligibility is explicitly enforced on the service path, not equivalently on the
browser path. [SessionStore ](../src/internal/server/oidc/session.go ) retrieves a
code and deletes it in separate operations after signing; simultaneous requests
can reach the same session before deletion. This is not atomic single-use
consumption.
[UserInfo ](../src/internal/server/oidc/userinfo.go ) verifies an RSA signature and
expiry, but its `Issuer` field is unused in verification and the helper does not
validate the JOSE header algorithm, audience or token purpose. It cannot claim
the same verification contract as the new caller CLI.
The [Authelia adapter ](../src/internal/adapters/authelia/adapter.go ) deliberately
decodes upstream ID-token claims without signature verification and does not
validate their issuer/audience/expiry. Its comment assumes a trusted TLS service
boundary; the configuration permits internal HTTP endpoints. This is an explicit
trust assumption, not independent provider-token verification.
**Close when:** the accepted profile defines these bindings and trust boundaries;
implementation enforces them; negative tests cover confidential clients,
redirect/grant mismatch, issuer/token-purpose mismatch and concurrent code reuse.
Validate upstream provider tokens or explicitly establish and test the chosen
transport/trust contract. This assessment does not claim a demonstrated attack.
Harden the authorization-code grant and UserInfo verification
Closes the local protocol surface of gap G01 from the scope assessment
(KEY-WP-0016). The browser grant validated PKCE, client id and scopes but left
four bindings unenforced, and UserInfo verified less than the caller CLI does.
Authorization-code path: bind the exchange to the redirect URI the code was
issued for, refuse clients whose registration does not permit the grant, and
authenticate confidential clients with a digest-based constant-time comparison
over the same credential sources as the service grant. An empty grantTypes stays
an implicit authorization-code client, matching config validation.
Code consumption: SessionStore.Consume reads and deletes under one lock. The
previous Get/Delete pair spanned JWT signing, and the added test reproduces the
race against that version -- 9 of 16 concurrent exchanges succeeded, and a
failed exchange left the code replayable.
UserInfo: check the JOSE header algorithm before trusting the signature, require
the configured issuer, and require an access token rather than accepting an ID
token of the right shape. Purpose is decided on the scope claim so the issued
token contract, which consumers pin exactly, does not change.
SCOPE.md and the assessment record which bindings are now enforced and that the
Authelia upstream-trust assumption remains open, so G01 is not fully closed and
no profile-conformance claim is made.
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-06 22:43:47 +02:00
**Status 2026-09-06 (KEY-WP-0016): partially closed.** The local protocol
surface is now enforced and covered by negative tests — redirect-URI binding,
grant-type eligibility on the browser path, confidential-client authentication
with a constant-time comparison, atomic single-use code consumption (the
Get/Delete race was reproduced first: 9 of 16 concurrent exchanges succeeded
before the fix), and UserInfo algorithm, issuer and access-token-purpose checks.
Still open: the Authelia adapter's unverified upstream ID-token claims and its
transport-trust assumption, which is a trust-contract decision rather than a
local binding. G01 is not fully closed until that is settled, and none of this
establishes complete profile conformance.
Verify upstream Authelia ID tokens
Closes the remaining half of gap G01. The adapter decoded upstream ID-token
claims without verifying anything, justified in a comment by a server-to-server
TLS boundary that nothing enforced.
Operator decision: verify the token rather than police the transport. The hop is
to be HTTPS as defence in depth, but KeyCape does not monitor, check or gate on
that -- a transport check helps only when it is configured correctly, which is
the assumption it was meant to remove. Verification holds regardless of how the
token arrived, so no HTTPS validation or opt-in flag is added.
HandleCallback now verifies the RS256 signature against Authelia's published
keys, the issuer Authelia advertises, KeyCape's own client ID in the audience,
and a sane validity window, before any claim is trusted. It fails closed: an
unreachable or unparseable key set denies the login. The advertised jwks_uri
path is rebased onto the server-side token base URL so split-horizon deployments
resolve, with config overrides where that inference is wrong, and an unknown key
id triggers one refresh so provider rotation needs no restart.
The reusable half lives in internal/jose rather than being copied from
authclient's verifier, since duplicated verification is how two copies drift and
one misses a fix. Migrating authclient onto it is tracked as KEY-WP-0019-T05,
kept separate so it does not destabilise a tested path in this change.
Thirteen rejection cases plus algorithm and rotation coverage; with the
unverified parse restored all fifteen fail, so they test the fix rather than
merely passing.
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-07 08:51:42 +02:00
**Status 2026-09-07 (KEY-WP-0019): closed.** Upstream ID tokens are now verified
before any claim is trusted — RS256 signature against Authelia's published keys,
the issuer Authelia advertises, KeyCape's own client ID in the audience, and a
sane validity window — failing closed when the key set is unavailable or
unparseable. Provider key rotation is picked up on one refresh without a restart.
The operator decision (2026-09-07) was to verify the token rather than enforce
the transport: the hop is to be HTTPS as defence in depth, but KeyCape does not
monitor, check or gate on that, because a transport check helps only when it is
configured correctly, which is the assumption it was meant to remove.
Verification holds regardless of how the token arrived, so the absence of HTTPS
validation here is a choice, not an oversight.
2026-09-07 09:05:16 +02:00
Both verification paths — the upstream adapter and the caller-side
`authclient` — now run on one implementation in `internal/jose` , so the strict
RS256/JWKS rules cannot drift apart or be fixed in only one copy. Claim policy
stays with each caller, whose issuer, audience and nonce bindings genuinely
differ.
Verify upstream Authelia ID tokens
Closes the remaining half of gap G01. The adapter decoded upstream ID-token
claims without verifying anything, justified in a comment by a server-to-server
TLS boundary that nothing enforced.
Operator decision: verify the token rather than police the transport. The hop is
to be HTTPS as defence in depth, but KeyCape does not monitor, check or gate on
that -- a transport check helps only when it is configured correctly, which is
the assumption it was meant to remove. Verification holds regardless of how the
token arrived, so no HTTPS validation or opt-in flag is added.
HandleCallback now verifies the RS256 signature against Authelia's published
keys, the issuer Authelia advertises, KeyCape's own client ID in the audience,
and a sane validity window, before any claim is trusted. It fails closed: an
unreachable or unparseable key set denies the login. The advertised jwks_uri
path is rebased onto the server-side token base URL so split-horizon deployments
resolve, with config overrides where that inference is wrong, and an unknown key
id triggers one refresh so provider rotation needs no restart.
The reusable half lives in internal/jose rather than being copied from
authclient's verifier, since duplicated verification is how two copies drift and
one misses a fix. Migrating authclient onto it is tracked as KEY-WP-0019-T05,
kept separate so it does not destabilise a tested path in this change.
Thirteen rejection cases plus algorithm and rotation coverage; with the
unverified parse restored all fifteen fail, so they test the fix rather than
merely passing.
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-07 08:51:42 +02:00
Verified the tests catch the original defect: with the unverified parse restored,
2026-09-07 09:05:16 +02:00
all thirteen rejection cases plus the algorithm and rotation cases fail, and with
the shared signature check disabled both callers' suites fail. Complete profile
conformance is still not claimed.
Verify upstream Authelia ID tokens
Closes the remaining half of gap G01. The adapter decoded upstream ID-token
claims without verifying anything, justified in a comment by a server-to-server
TLS boundary that nothing enforced.
Operator decision: verify the token rather than police the transport. The hop is
to be HTTPS as defence in depth, but KeyCape does not monitor, check or gate on
that -- a transport check helps only when it is configured correctly, which is
the assumption it was meant to remove. Verification holds regardless of how the
token arrived, so no HTTPS validation or opt-in flag is added.
HandleCallback now verifies the RS256 signature against Authelia's published
keys, the issuer Authelia advertises, KeyCape's own client ID in the audience,
and a sane validity window, before any claim is trusted. It fails closed: an
unreachable or unparseable key set denies the login. The advertised jwks_uri
path is rebased onto the server-side token base URL so split-horizon deployments
resolve, with config overrides where that inference is wrong, and an unknown key
id triggers one refresh so provider rotation needs no restart.
The reusable half lives in internal/jose rather than being copied from
authclient's verifier, since duplicated verification is how two copies drift and
one misses a fix. Migrating authclient onto it is tracked as KEY-WP-0019-T05,
kept separate so it does not destabilise a tested path in this change.
Thirteen rejection cases plus algorithm and rotation coverage; with the
unverified parse restored all fifteen fail, so they test the fix rather than
merely passing.
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-07 08:51:42 +02:00
2026-09-05 01:27:16 +02:00
### G02 — Machine-readable contract and discovery lag the runtime
**Priority: high. Kind: contract drift.**
[spec/canonical-model.yaml ](../spec/canonical-model.yaml ) restricts grants to
`authorization_code` , requires redirect URIs for every client, and omits newer
service subject, tenant, audience, lifetime and MFA/handoff policy fields present
in [domain/model.go ](../src/internal/domain/model.go ) and
[config.go ](../src/internal/config/config.go ). Both the YAML and Go comments claim
to be the source of truth, without a demonstrated generation/conformance link.
[Discovery ](../src/internal/server/oidc/discovery.go ) advertises both grants but
uses a fixed basic scope/claim list that omits newer core claims and configured
resource scopes. It is not a complete inventory of the current profile surface.
**Close when:** select a canonical version, reconcile schema/runtime/discovery,
and add executable compatibility checks for human and service registrations.
Distinguish required profile claims from optional discovery metadata rather than
assuming every omission has the same protocol impact.
Reconcile the canonical model and discovery with the runtime
Closes gap G02 of the scope assessment for the client-registration and discovery
surface. spec/canonical-model.yaml and domain/model.go both claimed to be the
source of truth and disagreed: the spec restricted grants to authorization_code,
required redirect URIs of every client, and omitted the audience, service
subject, tenant, role, MFA and handoff fields the runtime reads.
The durable part is the link, not the edit. A two-way conformance test compares
the spec against the Go model by reflection and fails when a runtime field has
no spec entry or a spec entry is not read by the runtime, the latter unless
marked runtime: false. It found drift beyond the assessment's list on its first
run -- User.tenant was undeclared -- which is the argument for the check over a
one-time reconciliation.
Discovery now advertises the core profile claims that appear on every token and
derives scopes_supported from the registered clients rather than a fixed list.
The Go model is stated as the runtime authority and the YAML as the reviewed
contract, in both files. This covers client registration and discovery, not
schema enforcement in general, which remains G06.
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-07 00:22:49 +02:00
**Status 2026-09-07 (KEY-WP-0017): closed for client registration and
discovery.** The Go model is now stated as the runtime authority and the
canonical model as the reviewed contract, with a two-way conformance test
holding them together — it rejects a runtime field with no spec entry and a spec
field the runtime does not read, the latter unless marked `runtime: false` . The
check found drift beyond the assessment's list on its first run (`User.tenant`
was undeclared), which is the argument for the check over the one-time edit. The
`Client` entity gained the audience, service-subject, tenant, role, MFA and
handoff fields, `grantTypes` gained `client_credentials` , and `redirectUris` is
no longer required of service-only clients. Discovery now advertises the core
profile claims and derives `scopes_supported` from the registered clients.
Still open: this covers the client-registration and discovery surface, not
schema enforcement in general, which remains G06.
2026-09-05 01:27:16 +02:00
### G03 — Migration does not preserve the current authentication contract
**Priority: high. Kind: implementation gap.**
The [Keycloak CLI ](../src/cmd/keycape-to-keycloak/main.go ) calls `Transform` , which
passes no clients. Only the library's `TransformWithClients` accepts them.
The [transformer ](../src/internal/migration/tokeycloak/transformer.go ) leaves realm
roles and client-scope definitions empty, always enables standard flow, and lacks
mapping for service-account issuance, resource audiences, tenant/role claims,
per-client lifetime, MFA policy and secret references. Its user mapping omits the
canonical ID, tenant and roles; retaining the same `sub` is not established.
LLDAP uses a DN as the canonical user ID, so directory relocation itself requires
an explicit identity continuity strategy. Passwords and MFA credentials are not
migrated; absence of MFA data is not proof that no re-enrollment is needed.
**Close when:** the CLI accepts the required complete snapshot/registrations,
transforms preserve or explicitly reject every relevant policy/identity field,
and migration proof demonstrates subject continuity, claims, MFA and client
behavior. Until then, call these artifact generators rather than full migration.
Make the Keycloak transform preserve or name every policy field
keycape-to-keycloak called Transform, which passes no clients, so it wrote a
realm with an empty clients array and nothing said the service-identity contract
had not been migrated. Where clients were supplied, mapClient hardcoded
standardFlowEnabled — silently giving every client_credentials registration the
browser flow — and dropped audience, service subject, tenant, roles, lifetime,
MFA policy, secret reference and handoff URLs. Realm roles and client scopes were
emitted as empty containers.
The defect was not the missing mapping but that a dropped field and an
inapplicable one looked identical in the output.
Add -clients, reading registrations through a new config.Registrations() that
converts without resolving secrets, so migration tooling cannot load material it
has no business holding. Derive flows from the declared grants. Carry the profile
claims as protocol mappers, since Keycloak has no native concept for them, and
lifetime, handoff URLs and the secret reference as attributes — the reference,
never a value. Derive realm roles and client scopes from what is present.
Report what cannot be carried, in UnpreservedReport, kept deliberately separate
from ValidationReport: consistency with the snapshot and completeness of the
migration are different questions and one list cannot answer both. It names the
unmigrated secret, the unenforceable MFA policy, passwords and factor enrolment,
and subject continuity. An incomplete transform emits partial telemetry.
Closes the semantic-preservation half of G03; proof against a live provider is
G04 and stays open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WAsfsfQmDu4vcBhiMcmQp
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 867844@bnt-lap001
Assistant-Session: 3d45905e-0016-4b49-b828-231406881f7b
2026-09-07 13:48:48 +02:00
**Status 2026-09-07 (KEY-WP-0020): semantic preservation closed; proof remains
G04.** The CLI takes `-clients` and reaches `TransformWithClients` , so the realm
carries the registration contract instead of an empty array. Flows are derived
from the declared grants rather than hardcoded — the previous mapping enabled the
browser flow on every service-only client, widening it during migration. The
audience, tenant, service subject and roles ride as protocol mappers, per-client
lifetime and handoff URLs as attributes, and the secret *reference* as an
attribute, never a value; a test asserts no resolved secret can appear in the
JSON. Realm roles and client scopes are derived from the identities and
registrations present, replacing empty containers that made a realm dropping
every role look like one that had none.
What cannot be carried is now named rather than dropped, which was the real
defect: `UnpreservedReport` is deliberately separate from `ValidationReport` , so
"the realm matches the snapshot" and "the migration is complete" stay distinct
questions. It names the unmigrated secret, the unenforceable MFA policy,
passwords and factor enrolment, and subject continuity — Keycloak mints its own
`sub` , so relying parties keyed on it will not recognise migrated users. An
incomplete transform emits `partial` telemetry, matching KEY-WP-0018.
Not closed: this is preservation and honest reporting, not proof. Whether an
imported realm actually issues profile-conformant tokens is G04, and password and
MFA credential migration remains out of scope.
2026-09-05 01:27:16 +02:00
### G04 — The replacement test harness does not prove a live provider swap
**Priority: high. Kind: verification and tooling gap.**
[Scenario B ](../scripts/test-scenario-b.sh ) and
[Scenario C ](../scripts/test-scenario-c.sh ) reference absent
`docker-compose.scenario-b.yml` / `docker-compose.scenario-c.yml` . They expect
`src/bin/` , while [src/Makefile ](../src/Makefile ) builds into root `bin/` , and invoke
a workstation-specific Go path from outside the Go module. Scenario C passes
`--base-dn` to a generator whose flag is `--basedn` .
Both scripts set `KEYCAPE_TEST_ISSUER` , but the
[profile suite ](../src/tests/profile/profile_test.go ) constructs its own
`httptest` server and does not consume that variable. The
[migration suites ](../src/tests/migration/ ) validate generated structures with
fixtures. Thus even repairing the shell prerequisites would not make those
profile tests exercise the external Keycloak issuer.
**Close when:** reproducible stacks and an externally targeted conformance suite
exercise actual replacement providers, directory migration, claims and MFA;
record unchanged relying-party behavior and explicit migration limitations.
Make the replacement harness runnable and target a live issuer
Closes the runnable half of gap G04. The Scenario B and C scripts could not
execute: absent compose files, binaries sought at src/bin where the Makefile
builds to bin/, --base-dn passed to a generator whose flag is --basedn, and a
hardcoded workstation Go path invoked from outside the module.
Repairing the shell alone would have proved nothing. Both scripts set
KEYCAPE_TEST_ISSUER while the profile suite built its own httptest server and
never read it, so they passed identically whether or not a provider was running.
A harness that cannot fail for the reason it exists is worse than a missing one.
src/tests/conformance targets the issuer named by KEYCAPE_TEST_ISSUER over HTTP:
discovery, the profile authorization surface, published keys parsed under the
runtime's own rules, excluded grants, and -- with credentials -- a real token
exchange verified against those keys. It skips when the variable is unset, so
make test is unchanged.
Run against Keycloak 26.0 rather than asserted to work. Discovery, authorization
surface and key checks passed, and a client_credentials exchange produced a token
that verified against Keycloak's published JWKS through internal/jose.
It also failed, correctly: stock Keycloak advertises the excluded implicit and
password grants, and in Keycloak those are server capabilities rather than
per-client toggles, so no emitted realm removes them. A migrated Keycloak has a
wider grant surface than KeyCape, which substantiates with evidence what SCOPE
previously asserted without it. Scenario B legitimately reports failure today.
Directory migration, credential and MFA preservation and relying-party behaviour
remain unexercised, and Scenario C has never been run end to end, so G04 does not
fully close.
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-07 23:32:11 +02:00
**Status 2026-09-07 (KEY-WP-0022): partially closed.** The harness now runs. The
shell prerequisites are fixed (root `bin/` , `--basedn` , module-relative `go -C
src`, prerequisite checks before anything starts), the two missing compose files
exist and pass `docker compose config` , and `src/tests/conformance` reads
`KEYCAPE_TEST_ISSUER` and exercises a running issuer over HTTP — discovery,
profile authorization surface, published keys parsed under the runtime's own
rules, and, with credentials, a real token exchange verified against those keys.
It skips when the variable is unset, so `make test` is unchanged.
It was run against an actual Keycloak 26.0, not only asserted to work: discovery,
authorization surface and key checks passed, and a `client_credentials` exchange
against a created service client produced a token that verified against
Keycloak's published JWKS through `internal/jose` .
**Finding that bears on the replacement claim:** stock Keycloak fails
`TestExcludedGrantsAreNotAdvertised` . Its discovery advertises `implicit` and
`password` alongside the profile's grants, and those are server capabilities in
Keycloak rather than per-client toggles, so no realm configuration removes them.
A migrated Keycloak therefore presents a wider grant surface than KeyCape does.
This substantiates with evidence what SCOPE already said was unestablished, and
means Scenario B legitimately reports a failure today rather than a green run.
Prove the migration against live directories and fix what that surfaced
Finishes the unproven half of gap G04. Running the harness against real LLDAP,
OpenLDAP and Keycloak found four defects that the full unit suite passed over.
Every LLDAP user search pointed at a branch that does not exist. Config.userOU()
defaulted to ou=users while LLDAP stores users under ou=people, and nothing set
UserOU. LookupUser, ListUsers and ValidatePassword all derive from it, so all
three silently found nothing against a stock LLDAP -- human login included, not
only the export. Exposed by an export returning zero users while still emitting a
membership referencing uid=admin, a snapshot the repo's own validator rejects.
raw_attributes_well_formed, added one workplan earlier, rejected the LLDAP
adapter's own _validation_warning annotation, so the exporter's output failed its
own validation. Tooling annotations are exempt now, and a test proves the
exemption does not weaken the rule.
Migrated group memberships were dangling: resolveMemberDN passed a source DN
through unchanged while entries were written to the target branch, so groups
named entries the migrated directory does not contain. And empty groups could
not load at all, since groupOfNames makes member a MUST -- they reference a
placeholder entry the LDIF creates, an organizationalRole rather than a person,
emitted only when some group needs it.
The scenario-c compose file could not start: bitnami/openldap:2.6 does not
exist, though it passed docker compose config. Pinned to the image the scenario
was proved against.
Proof: LLDAP -> export -> validate -> LDIF -> ldapadd into OpenLDAP 1.5.0, every
entry added and every member resolving; a realm from the same export imports into
Keycloak and serves discovery. KeyCape passes 5/5 conformance checks, a migrated
Keycloak 4/5.
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.
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-08 00:29:34 +02:00
**Status 2026-09-08 (KEY-WP-0023): substantially closed.** Directory migration is
now proved end to end into a real directory: LLDAP -> export -> validate -> LDIF
-> ldapadd into OpenLDAP 1.5.0, every entry added without error and every member
value in the loaded directory resolving to an entry that exists. The migrated
Keycloak realm built from that same live export imported cleanly and served
discovery, with the migrated user carrying keycape.canonicalId. Both sides of the
swap are measured: KeyCape passes 5/5 conformance checks, a migrated Keycloak
passes 4/5, failing only the excluded-grant check.
Running it found four defects that the full unit suite passed over:
1. `Config.userOU()` defaulted to `ou=users` while LLDAP stores users under
`ou=people` , and nothing set `UserOU` . LookupUser, ListUsers and
ValidatePassword all derive from it, so every user search against a stock
LLDAP silently found nothing — human login included. Exposed by an export that
returned zero users while emitting a membership referencing uid=admin.
2. `raw_attributes_well_formed` , added in KEY-WP-0021, rejected the adapter's own
`_validation_warning` annotation, so the exporter's output failed its own
validation. Tooling annotations are now exempt and documented.
3. LDIF members kept their source DNs while entries were written to the target
branch, so migrated groups referenced entries the target directory does not
contain.
4. `groupOfNames` requires a member, so every empty group aborted the load. Empty
groups — preserved deliberately since KEY-WP-0018 — now reference a placeholder
entry the LDIF creates.
A fifth was a harness defect: `bitnami/openldap:2.6` does not exist, so the
compose file added in KEY-WP-0022 passed `docker compose config` and could not
start. That is the distance between a file that parses and a stack that runs.
Still open: no browser login was performed against the migrated realm, so
unchanged relying-party behaviour is not established, and MFA was not exercised.
Credential and MFA migration is a scope exclusion rather than a test gap — it is
not supplied, so no harness can demonstrate it. Subject continuity is explicitly
not preserved, which keycape-to-keycloak already reports.
Make the replacement harness runnable and target a live issuer
Closes the runnable half of gap G04. The Scenario B and C scripts could not
execute: absent compose files, binaries sought at src/bin where the Makefile
builds to bin/, --base-dn passed to a generator whose flag is --basedn, and a
hardcoded workstation Go path invoked from outside the module.
Repairing the shell alone would have proved nothing. Both scripts set
KEYCAPE_TEST_ISSUER while the profile suite built its own httptest server and
never read it, so they passed identically whether or not a provider was running.
A harness that cannot fail for the reason it exists is worse than a missing one.
src/tests/conformance targets the issuer named by KEYCAPE_TEST_ISSUER over HTTP:
discovery, the profile authorization surface, published keys parsed under the
runtime's own rules, excluded grants, and -- with credentials -- a real token
exchange verified against those keys. It skips when the variable is unset, so
make test is unchanged.
Run against Keycloak 26.0 rather than asserted to work. Discovery, authorization
surface and key checks passed, and a client_credentials exchange produced a token
that verified against Keycloak's published JWKS through internal/jose.
It also failed, correctly: stock Keycloak advertises the excluded implicit and
password grants, and in Keycloak those are server capabilities rather than
per-client toggles, so no emitted realm removes them. A migrated Keycloak has a
wider grant surface than KeyCape, which substantiates with evidence what SCOPE
previously asserted without it. Scenario B legitimately reports failure today.
Directory migration, credential and MFA preservation and relying-party behaviour
remain unexercised, and Scenario C has never been run end to end, so G04 does not
fully close.
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-07 23:32:11 +02:00
2026-09-05 01:27:16 +02:00
### G05 — Directory export can omit data without reporting it
**Priority: medium. Kind: implementation gap.**
The [exporter ](../src/internal/migration/lldapexport/exporter.go ) discovers groups
through each user's memberships. Empty/unreferenced groups are not enumerated.
Group lookup errors are skipped even though a comment says they are recorded in
the incompatibility report. The emitted success event and report therefore do
not establish a complete export. Iterating the group map also leaves group order
unspecified.
**Close when:** enumerate all groups independently, report or fail on incomplete
reads, define ordering, and test empty groups and backend failures. Preserve
completeness evidence before claiming deterministic full snapshots.
2026-09-07 08:45:50 +02:00
**Status 2026-09-07 (KEY-WP-0018): closed.** `LDAPAdapter` gained a
`ListGroups` enumeration over the group subtree, offered to the exporter through
an optional `domain.GroupLister` rather than by widening `UserRepository` , so
groups with no members are in the snapshot and `Group.Members` is populated from
the directory. Reading the adapter to write it surfaced a defect the assessment
had not listed: `LookupGroups` never set `Members` , and the exporter built every
membership from that field, so a real export's `memberships` block was always
empty while the fixture-backed tests passed. Each result now carries
`groupEnumeration` (`directory` or `membership-derived` ) and a `Complete()`
predicate; a failed enumeration aborts instead of writing a smaller snapshot, a
failed per-user lookup on the fallback path is reported rather than dropped, an
incomplete run emits `partial` telemetry, and the CLI names the mode. Users,
groups and memberships are sorted on stable keys. Tests cover the empty group,
the enumeration failure, the fallback lookup failure and repeat-run determinism.
This is completeness evidence for the user/group/membership surface only —
credential migration remains out of scope under G03.
2026-09-05 01:27:16 +02:00
### G06 — The validator is narrower than schema enforcement
**Priority: medium. Kind: implementation/claim gap.**
[validator.go ](../src/internal/validator/validator.go ) implements useful structural
and semantic checks. However, `checkNoUnknownAttributes` is a placeholder that
rejects blank keys rather than enforcing an attribute allow-list; group
membership validation checks nonempty member IDs rather than complete referenced
identity existence. It does not derive its checks from the YAML schema.
**Close when:** implement the intended allow-list/reference constraints or narrow
the normative contract to the actual checks; add invalid-snapshot cases that
prove each stated rule. SCOPE now calls this limited snapshot validation.
Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
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-07 23:22:55 +02:00
**Status 2026-09-07 (KEY-WP-0021): closed.** The placeholder is replaced by
`raw_attributes_well_formed` , which requires each key to be a valid LDAP
attribute descriptor (RFC 4512 descr or numeric OID), forbids shadowing a
mapping the canonical model owns (uid, cn, mail) in any casing, and rejects two
keys differing only by case. `spec/ldap-schema.yaml` carries the same wording, so
the rule's name no longer overpromises. Invalid-snapshot cases cover every
constraint plus accepting legitimate raw attributes; neutering the rule fails
four of them.
Two corrections to this gap's own description. First, an allow-list of permitted
attribute names is not derivable: `ldapAttributes` is defined as the attributes
the canonical model does *not* cover, so the schema cannot enumerate what may
appear there — the constraints above are what is actually checkable. Second, the
reference constraint was already implemented: `checkValidGroupMemberships` does
only check emptiness, but the semantic rule `checkReferencedUsersExist` resolves
every member against the user set and fails on an unknown one. The rule existed
in a different function than the one this assessment examined.
Still not claimed: validation of attribute *values* against a directory schema.
2026-09-05 01:27:16 +02:00
### G07 — Optional tenant-role support is not wired into the server
**Priority: medium. Kind: integration gap.**
The [tenant-engine client ](../src/internal/adapters/tenantengine/adapter.go ) and
`TokenHandler.TenantEngine` are implemented and tested, including omission on
failure. [main.go ](../src/cmd/keycape/main.go ) supplies no such client and exposes
no configuration for it. The stock server therefore leaves it nil and omits
`tenant_roles` .
**Close when:** wire an explicit opt-in configuration and verify the built
executable, or document this as library-only support. It is an optional cache
claim, so absence is not itself a resource authorization failure.
2026-09-08 08:59:40 +02:00
**Status 2026-09-08 (KEY-WP-0024): closed.** A `tenantEngine` block with
`baseURL` and optional `timeout` now wires the client; an empty `baseURL` leaves
the stock server's behaviour unchanged. Verified in the built executable rather
than at the wiring: with a stub source a real token carries `tenant_roles` , with
no block the claim is absent, and with the source down issuance succeeds without
it — the documented fail-open path, confirmed end to end. Validation rejects an
unusable URL, an out-of-range timeout, and a timeout set without a base URL.
2026-09-05 01:27:16 +02:00
### G08 — Runtime lifecycle and readiness are intentionally minimal
**Priority: medium. Kind: operational maturity gap.**
[Authorization state ](../src/internal/server/oidc/authorize.go ),
[code sessions ](../src/internal/server/oidc/session.go ),
[login sessions ](../src/internal/server/oidc/login_session.go ) and
[handoffs ](../src/internal/server/oidc/handoff.go ) reside in memory. Restart loses
in-flight and login state; multi-replica behavior is not supported by a shared
store. `/healthz` in [main.go ](../src/cmd/keycape/main.go ) returns a constant process
response without testing dependencies. The server uses `ListenAndServe` ; TLS
termination is external. The signing key and registrations load
at startup; a fixed `key-1` identifier is used by token issuance.
[Logout ](../src/internal/server/oidc/logout.go ) clears the local login session;
it is not upstream logout or JWT revocation. No general refresh/introspection/
revocation or automatic rotation service is exposed. These need explicit
operational limits rather than an unqualified “high stability” label.
**Close when:** document/test the supported deployment topology, readiness and
restart behavior, and coordinate key/client lifecycle and consumer refresh.
Shared storage or refresh tokens need not be added if the accepted profile
explicitly excludes them. Benchmark before asserting resource-efficiency bounds.
Give the runtime real readiness, graceful shutdown and stated limits
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
2026-09-08 09:43:49 +02:00
**Status 2026-09-08 (KEY-WP-0025): closed.** `/readyz` probes LLDAP (by bind, so
a revoked service password is caught), Authelia and privacyIDEA, with results
cached 2s and each probe bounded at 3s; `/healthz` stays liveness and probes
nothing, so a dependency blip cannot trigger a restart that also discards every
in-flight login. Failures name the check but not the reason, since the endpoint is
unauthenticated. `SIGTERM` /`SIGINT` drain in-flight requests for 15s. Verified in
the running executable: 503 with all three failed while `/healthz` returned 200,
the LLDAP check flipping to ok once started, and 40/40 requests succeeding across
a SIGTERM.
`docs/operations.md` states the single-replica topology and why, restart
behaviour, 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; `/logout` is local only. No throughput or resource
figures are asserted, since nothing benchmarks KeyCape. Shared storage and
refresh tokens remain deliberately excluded.
2026-09-05 01:27:16 +02:00
### G09 — Packaging/bootstrap and older CLI credential handling need reconciliation
**Priority: medium. Kind: operational/tooling gap.**
[Development Compose ](../docker-compose.dev.yml ) refers to `config/dev-key.pem`
and `config/authelia` , neither supplied in the checkout. It is a scaffold requiring
bootstrap material. The [Dockerfile ](../Dockerfile ) packages only `keycape` , not
the migration/validator binaries. Image publication still points at the older
registry address in [.gitea/workflows/image.yaml ](../.gitea/workflows/image.yaml );
that requires reconciliation with the Forgejo image location recorded by the
live workplan, not an assumption that every push deployed the current source.
The older [LLDAP exporter CLI ](../src/cmd/lldap-export/main.go ) accepts the bind
password on argv, and the migration scripts use that path. This falls short of
the newer caller CLI's private credential transport posture. Snapshot and realm
output are written with mode 0644, so operators must also consider identity-data
handling even though credential secrets are not part of those exports.
**Close when:** supply a reproducible bootstrap procedure, correct executable/
artifact locations and release references, and adopt safe credential input and
appropriate export permissions. A documented external bootstrap may satisfy
scope without storing secrets in this repository.
Reconcile packaging, bootstrap and migration credential handling
Closes gap G09: five loosely related defects.
lldap-export took the service account password on argv, where ps exposes it to
any local user and shell history and process accounting capture it. It now
prefers KEYCAPE_LLDAP_BIND_PW or --bind-pw-file; --bind-pw still works but warns,
deprecated rather than removed because existing runbooks use it and breaking them
silently would be worse than one more cycle of exposure. Conflicting sources are
rejected instead of silently ranked, since an operator otherwise cannot tell
which bind was attempted. Both migration scripts pass the password by
environment now.
The canonical export, generated LDIF and Keycloak realm were written 0644. None
carries credential material, but the snapshot is every username, display name,
email and group membership in the estate, and it tends to land in /tmp. All three
are 0600.
The image packaged keycape alone, so the validator and migration binaries needed
a Go toolchain on the host -- which defeats shipping an image for the cutover
work they exist to support. All five ship; the issuer stays the entrypoint.
Verified by building the image and running each binary inside it.
The publish workflow named 92.205.130.254:32166 while the cluster runs
forgejo.coulomb.social/coulomb/key-cape. It now defaults to the recorded name and
stays overridable by a repository variable. This repository cannot verify that
the runner resolves that hostname or that the registry credentials are valid for
it; if the next publish fails, set the REGISTRY variable back to the address.
docker-compose.dev.yml mounts a private key and Authelia material that are
correctly absent from the checkout. scripts/bootstrap-dev.sh generates them
locally under a restrictive umask rather than chmodding afterwards, so the key is
never briefly world-readable. Everything it writes is git-ignored.
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-08 09:51:59 +02:00
**Status 2026-09-08 (KEY-WP-0026): closed, with one item an operator must
confirm.** `scripts/bootstrap-dev.sh` generates the key and Authelia material the
dev stack mounts, all git-ignored and created under a restrictive umask rather
than chmodded after. The image now ships all five binaries, verified by running
each inside a built image. `lldap-export` prefers `KEYCAPE_LLDAP_BIND_PW` or
`--bind-pw-file` and warns on the deprecated `--bind-pw` , which stays working for
existing runbooks; both migration scripts pass the password by environment. The
export, LDIF and realm artifacts are written `0600` .
The publish workflow now defaults to `forgejo.coulomb.social` rather than
`92.205.130.254:32166` , overridable by a repository variable. **This is the one
part not verifiable from here:** whether the runner resolves that name and
whether the registry credentials are valid for it can only be established by a
publish. If it fails, set the `REGISTRY` variable back to the address.
2026-09-05 01:27:16 +02:00
### G10 — Source capability is ahead of live custody and consumer adoption
**Priority: high for rollout; medium for handoff hygiene. Kind: external dependency/proof gap.**
[KEY-WP-0013 ](../workplans/KEY-WP-0013-approval-engine-resource-audience.md ) records
approval clients awaiting custody admission, exact human callback and live proof.
Its earlier deployment observation identifies `main-153258b` , not the assessed
source revision. The [provisioning packet ](../docs/approval-engine-provisioning-request.yaml )
is proposed metadata, not executable authorization or a live registration.
[KEY-WP-0014 ](../workplans/KEY-WP-0014-native-credential-lane-handoff.md ) records
native JWT commands as implemented, but coordinated Qonto rotation and consumer
handoff remain open. The existing ops-warden login route obtains an **OpenBao
token**, so replacing it with issuer-JWT output would change the consumer
contract. [KEY-WP-0009 ](../workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md )
was reopened because claimed handoff delivery lacks matching current receipts.
**Close when:** named custody/platform owners admit and provision exact lanes,
register the real callback, deploy and verify the new contracts, reconcile token
types at consumer boundaries, and retain handoff receipts. Repo-local source
changes cannot alone establish these outcomes.
Establish the live state and find a rollout precondition for G10
G10 waits on custody and platform owners and cannot close from here. What was
doable: verify the handoffs actually went out, replace a remembered live state
with an observed one, and find out whether main is safe to deploy. The last
question found a defect in this repository's own recent work.
Handoffs verified independently rather than trusted: all seven messages are in
the hub with receipt ids. This gap was reopened once for claimed-but-unsent
delivery, so the claim deserved the same scrutiny.
Live state read from the cluster read-only: image main-153258b, only the Qonto
secret materialized so the approval clients remain unprovisioned, four registered
clients, no tenantEngine block. That also corrects an earlier claim of mine --
the deployed config sets userOU explicitly, so the KEY-WP-0023 default fix was
never a production issue.
The precondition: KEY-WP-0019 discovers the expected issuer from
authelia.tokenBaseURL, and the deployed Authelia derives its advertised issuer
from the request Host, advertising the in-cluster address to KeyCape and the
browser-facing one to browsers. Verification fails closed, so a mismatch breaks
every human login and looks like a broken login rather than a misconfiguration.
Which value the token carries needs a real login against production to settle and
was not determined here.
Two mitigations: docs/operations.md documents pinning authelia.issuer and
jwksUrl, with the curl that reveals what the provider advertises for a given
Host; and the authentication failure event now carries a specific reason, so
id_token_issuer_mismatch is distinguishable from a signature failure or an
unreachable key set. The browser still learns nothing.
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-08 11:41:42 +02:00
**Status 2026-09-08 (KEY-WP-0027): still open, and correctly so.** The handoff
hygiene half is discharged: all seven messages are present in the hub with
receipt ids, verified independently rather than taken on trust, since this gap
was reopened once for exactly that reason. No replies yet.
Live state observed read-only rather than remembered: image `main-153258b` ,
only the Qonto secret materialized (so the two approval clients remain
unprovisioned), four registered clients, no `tenantEngine` block. An earlier
claim of mine is corrected there: the deployed config sets `userOU: "ou=people"`
explicitly, so the KEY-WP-0023 default fix was never a production issue.
**Rollout precondition found in this repository's own work.** Upstream
verification (KEY-WP-0019) discovers the expected issuer from
`authelia.tokenBaseURL` , and the deployed Authelia derives its advertised issuer
from the request Host — in-cluster it advertises
`http://authelia.sso.svc.cluster.local:9091` , browser-facing
`http://auth.coulomb.social` . Verification fails closed, so a mismatch breaks
every human login and presents as a broken login rather than a misconfiguration.
Which value the token carries was not settled, because doing so needs a real
login against production. `docs/operations.md` documents pinning
`authelia.issuer` /`jwksUrl` , and the failure now reports a specific
`id_token_issuer_mismatch` reason so it is diagnosable in seconds.
The rest is owner work and stays open.
2026-09-05 01:27:16 +02:00
## Deliberate exclusions are not defects
INTENT excludes general-purpose IAM, weakened flows and expanded-mode operations.
Dynamic registration, implicit/password grants, wildcard redirects and arbitrary
brokering should stay excluded unless the accepted profile changes. Resource
policy decisions and secret custody likewise remain with their owners.
The meaningful gaps are incomplete delivery or proof of the repo's claimed
subset, plus unqualified maturity claims. Full Keycloak feature parity, building
an authorization engine, or taking over OpenBao is not the proposed remedy.
## Recommended order
1. Resolve G01 protocol bindings and G02 canonical contract drift before claiming
complete profile conformance or widening rollout.
2. Treat G03– G06 as a migration workstream: completeness and semantic preservation
first, then actual provider-replacement proof.
3. Decide G07 opt-in wiring and document/test the G08 supported topology.
4. Reconcile G09 bootstrap/release paths and complete G10 owner admissions and
live proof. Do not confuse passing local tests with those handoffs.
The findings above are an assessment backlog, not completed fixes or newly
approved production changes. Existing workplan references are retained where
applicable; new engineering work needs scoped implementation plans. This task
changes documentation only.