key-cape/history/2026-09-05-011726-scope-intent-assessment.md
tegwick 7dda967c27
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 45s
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

32 KiB
Raw Blame History

KeyCape scope against intent — 2026-09-05

Assessment timestamp: 2026-09-05 01:17:26 CEST (+02:00).
Source baseline: b989de4e90.
Inputs: INTENT, the previous SCOPE, implementation, tests, build/operational scaffolding, and open workplans. Result: updated SCOPE. 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, OIDC handlers and 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 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 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 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 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 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.

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.

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.

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.

Verified the tests catch the original defect: with the unverified parse restored, 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.

G02 — Machine-readable contract and discovery lag the runtime

Priority: high. Kind: contract drift.

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 and config.go. Both the YAML and Go comments claim to be the source of truth, without a demonstrated generation/conformance link.

Discovery 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.

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.

G03 — Migration does not preserve the current authentication contract

Priority: high. Kind: implementation gap.

The Keycloak CLI calls Transform, which passes no clients. Only the library's TransformWithClients accepts them. The transformer 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.

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.

G04 — The replacement test harness does not prove a live provider swap

Priority: high. Kind: verification and tooling gap.

Scenario B and Scenario C reference absent docker-compose.scenario-b.yml / docker-compose.scenario-c.yml. They expect src/bin/, while 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 constructs its own httptest server and does not consume that variable. The migration suites 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.

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.

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.

G05 — Directory export can omit data without reporting it

Priority: medium. Kind: implementation gap.

The exporter 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.

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.

G06 — The validator is narrower than schema enforcement

Priority: medium. Kind: implementation/claim gap.

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.

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.

G07 — Optional tenant-role support is not wired into the server

Priority: medium. Kind: integration gap.

The tenant-engine client and TokenHandler.TenantEngine are implemented and tested, including omission on failure. 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.

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.

G08 — Runtime lifecycle and readiness are intentionally minimal

Priority: medium. Kind: operational maturity gap.

Authorization state, code sessions, login sessions and handoffs reside in memory. Restart loses in-flight and login state; multi-replica behavior is not supported by a shared store. /healthz in 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 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.

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.

G09 — Packaging/bootstrap and older CLI credential handling need reconciliation

Priority: medium. Kind: operational/tooling gap.

Development Compose refers to config/dev-key.pem and config/authelia, neither supplied in the checkout. It is a scaffold requiring bootstrap material. The Dockerfile packages only keycape, not the migration/validator binaries. Image publication still points at the older registry address in .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 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.

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.

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 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 is proposed metadata, not executable authorization or a live registration.

KEY-WP-0014 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 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.

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.

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.

  1. Resolve G01 protocol bindings and G02 canonical contract drift before claiming complete profile conformance or widening rollout.
  2. Treat G03G06 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.