All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 45s
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
516 lines
32 KiB
Markdown
516 lines
32 KiB
Markdown
# 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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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](../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.
|
||
|
||
**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.
|
||
|
||
## 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.
|