All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
245 lines
12 KiB
Markdown
245 lines
12 KiB
Markdown
---
|
|
id: KEY-WP-0013
|
|
type: workplan
|
|
title: "Approval-engine resource audience and client registrations"
|
|
domain: infotech
|
|
repo: key-cape
|
|
status: blocked
|
|
owner: codex
|
|
topic_slug: approval-engine-resource-audience
|
|
created: "2026-09-05"
|
|
updated: "2026-09-08"
|
|
state_hub_workstream_id: "6e815d88-b0e3-5ce0-be5d-13ab15917f7f"
|
|
---
|
|
|
|
Source: approval-engine inbox request 5583e896-52f2-45bd-895f-02f227b7e836,
|
|
reviewed against its local registration and caller-authentication contracts.
|
|
|
|
## Implement static resource audiences
|
|
|
|
```task
|
|
id: KEY-WP-0013-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "a92432a2-4e92-5b90-be2b-d82b784bf8f0"
|
|
```
|
|
|
|
Added optional static audience configuration for access tokens in both grants;
|
|
ID tokens retain the relying-party audience. Added human access-token scope.
|
|
Published bounded approval service fragments and the human registration contract.
|
|
Regression tests cover the default audience, request override resistance, JWKS
|
|
signature validation, ID-token separation and service registration scope isolation.
|
|
Browser requests and token exchanges now enforce the client scope allow-list,
|
|
including grants removed after authorization.
|
|
|
|
## Provision and prove live registrations
|
|
|
|
```task
|
|
id: KEY-WP-0013-T02
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "607897c5-bad9-55e5-86df-7802f592d6e8"
|
|
```
|
|
|
|
Needs deployment-owned custody for both new service secret references and the
|
|
upstream issuer precondition. Deploy the implementation and service registrations together,
|
|
then prove live JWKS verification and denied excess scopes without logging values.
|
|
Local signature proof is not live rollout evidence. See docs/approval-engine-auth-contract.md.
|
|
The separate human UI callback gate is retained in T05; a bearer-only resource
|
|
server does not own a redirect and its absence does not block these service clients.
|
|
|
|
|
|
2026-09-05 follow-up: read-only deployment metadata shows the current image is
|
|
forgejo.coulomb.social/coulomb/key-cape:main-153258b and only the Qonto service
|
|
secret environment reference is present. The two approval clients are not
|
|
materialized through deployment environment references. Published a concrete
|
|
non-secret admission packet at docs/approval-engine-provisioning-request.yaml.
|
|
Custody routing has no exact admitted lane for these two clients. `warden plan`
|
|
returned founder_required but matched an unrelated generic database lane; that
|
|
mismatch is not authority to provision. Human callback clarification is pending.
|
|
No secrets were read or production resources changed.
|
|
|
|
2026-09-08: the two blocking requests were sent and their receipts are readable
|
|
back via `GET /messages/?from_agent=key-cape`.
|
|
|
|
- `278a3ebe-b529-49f6-bd1a-e3ebcf318260` -> railiance-platform. Custody admission
|
|
for both clients, quoting `docs/approval-engine-provisioning-request.yaml`
|
|
verbatim: exact OpenBao paths and `client_secret` field, the two Kubernetes
|
|
delivery references, the two `KEYCAPE_*_CLIENT_SECRET` environment names,
|
|
`tenant:platform` per decision `5ed3fb35-eca9-413a-82b9-95171ba85bf6`, and the
|
|
15m lifetime. It asks for four things explicitly: confirm/correct the paths and
|
|
fields, confirm the delivery references, name the authority for the attended
|
|
first provision, and agree a rollout window in which registrations and the
|
|
KeyCape version that reads them deploy together. It restates that the
|
|
`warden plan` `founder_required` result matched an unrelated generic database
|
|
lane and is not being treated as authorization.
|
|
- `43fbd997-6e22-4859-8433-58b784c2df6c` -> approval-engine. Requests the two
|
|
missing human-registration inputs — the exact `client_id` and the full callback
|
|
URI — noting redirects are matched exactly and a near-miss fails closed at
|
|
`/authorize`. Restates the human client shape (public, S256 PKCE,
|
|
`audience: approval-engine`, `allowedScopes: [openid, approval:approve]`,
|
|
`mfaRequired: true`, no consume grant) and asks approval-engine to confirm it
|
|
validates the access token, not the ID token, against the deployed `/jwks`.
|
|
|
|
Deployment state is unchanged from the 2026-09-05 observation: neither approval
|
|
client is materialized. Task remains `wait` — the two admissions are owned by
|
|
railiance-platform and approval-engine respectively, and live provisioning proof
|
|
cannot begin until both land. Still no secret read and no production resource
|
|
changed.
|
|
|
|
2026-09-08, second pass. Custody and the human callback are still owed, but one
|
|
part of this task was ours all along and was not built: the task requires proving
|
|
"live JWKS verification and denied excess scopes without logging values", and
|
|
there was no way to do that except by hand, against production, at the moment
|
|
custody lands — which is the worst time to be improvising a check.
|
|
|
|
Shipped `keycape verify-client` (`src/internal/authclient/verify.go`, documented
|
|
in `docs/native-authentication.md`). Per registration it verifies discovery
|
|
origin, the `client_credentials` exchange and its RS256 signature against the
|
|
deployed JWKS, exact `sub`/`tenant`/`roles`/`principal_type`, that every
|
|
`-deny-scope` is refused, and that the token carries no scope that was not
|
|
requested — the last being a gap the caller commands do not cover, since they
|
|
prove requested scopes were granted and not that nothing extra came back.
|
|
Failures name the claim, never the observed value, and nothing is written to
|
|
disk, so it is safe to run against production. The exact invocation for each of
|
|
the two clients is now recorded in the `verification:` block of
|
|
`docs/approval-engine-provisioning-request.yaml`, so admission hands back a
|
|
command rather than a description.
|
|
|
|
Tests: `src/internal/authclient/verify_test.go` covers the passing case, an
|
|
over-broad registration, a live predecessor secret, an identical "rotation" and
|
|
output non-disclosure. Verified with teeth — neutering `mustFail` makes the suite
|
|
fail rather than pass silently.
|
|
|
|
Task stays `wait`. What is owed from elsewhere is unchanged and unreduced: the
|
|
two secret values through an admitted custody path, and the exact human
|
|
`client_id` and callback URI. Nothing here provisions anything.
|
|
|
|
## Reconcile tenant vocabularies across approval layers
|
|
|
|
```task
|
|
id: KEY-WP-0013-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "e98da2eb-1f05-5c7f-bc84-711fa38b8c4a"
|
|
```
|
|
|
|
Source: glas-harness inbox message 356f6977-d361-4e3b-83ab-b2c7f4759286
|
|
(GLAS-WP-0015), which asks for the exact store tenant, JWT tenant, CheckRequest
|
|
tenant, any permitted mapping and wrong-tenant denial evidence.
|
|
|
|
Published `docs/tenant-claim-contract.md`. The KeyCape-owned JWT tenant for all
|
|
four reviewed service registrations is `tenant:coulomb`, bound at registration
|
|
and required by config validation. Approval store `platform` and policy
|
|
`tenant:platform` are owned by approval-engine and flex-auth; KeyCape performs no
|
|
normalization or aliasing, so exact comparison does not match today. No mapping
|
|
was invented and no live registration or policy subject was changed — the two
|
|
admissible resolutions are recorded for the owning parties to decide.
|
|
|
|
Added `src/internal/server/oidc/tenant_test.go`: request-supplied `tenant` and
|
|
`tenant_hint` cannot alter the claim; two registrations never carry each other's
|
|
tenant (the wrong-tenant denial basis); human tokens default to `tenant:coulomb`
|
|
rather than an empty claim. Local issuance proof only, not live-rollout evidence.
|
|
|
|
## Align approval registrations to tenant:platform
|
|
|
|
```task
|
|
id: KEY-WP-0013-T04
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "d93e21cc-a422-5e78-981f-9c00f3161375"
|
|
```
|
|
|
|
Source: glas-harness inbox message f487c63a-dd7e-4ad8-9188-eec9fbea59c6,
|
|
operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 ("Use tenant:platform for
|
|
the Glas approval dependency chain", verified resolved in the hub). This closed
|
|
resolution 2 of the two options recorded in KEY-WP-0013-T03.
|
|
|
|
Changed the `tenant` field on exactly `secrets-engine-approval` and
|
|
`approval-engine-operator` in `config/service-clients.example.yaml` and the two
|
|
matching entries in `docs/approval-engine-provisioning-request.yaml` to
|
|
`tenant:platform`, each annotated with the decision id. `codex-railiance-platform`,
|
|
`secrets-engine-openbao` and the human directory default keep `tenant:coulomb`.
|
|
No audience, scope, subject, role, lifetime or MFA change; no cross-tenant grant.
|
|
|
|
Evidence: `TestApprovalClientIssuesExactPlatformTenantAndRejectsAliases` (exact
|
|
`tenant:platform` with `aud=approval-engine` even when the caller requests
|
|
`platform`, `tenant:coulomb` or `TENANT:PLATFORM`),
|
|
`TestUnrelatedServiceClientKeepsCoulombTenant`, and
|
|
`TestServiceRegistrationTenantsAreExactPerDecision`, which pins every reviewed
|
|
client's tenant against the real fixture so a reintroduced alias fails the build.
|
|
Full `go test ./...` and `go vet ./...` pass. Choice only — live provisioning and
|
|
the other admission gates remain with KEY-WP-0013-T02.
|
|
|
|
## Assign and register the human approver browser client
|
|
|
|
```task
|
|
id: KEY-WP-0013-T05
|
|
status: wait
|
|
priority: high
|
|
assignee: the-custodian
|
|
blocking_reason: "An actual approver UI owner, client ID and deployed callback are not yet supplied. approval-engine is a bearer-only resource server."
|
|
state_hub_task_id: "9a782909-91db-59fa-aae7-83766f4fbb0d"
|
|
```
|
|
|
|
Own the residual human registration separately from service-client T02. Reuse
|
|
an existing accepted UI if available; do not invent a callback on approval-engine.
|
|
Require public S256 PKCE, exact redirect, MFA, approval:approve without consume,
|
|
and a real human access-token proof. No service credential substitutes for this.
|
|
HFACT-WP-0001-T03 consumes the acceptance where a human approval is required.
|
|
|
|
## Make negative rollout evidence discriminate actual issuer refusal
|
|
|
|
```task
|
|
id: KEY-WP-0013-T06
|
|
status: done
|
|
priority: high
|
|
assignee: the-custodian
|
|
state_hub_task_id: "65132e1d-10db-5915-a664-83beaf30365a"
|
|
```
|
|
|
|
Critical-path review found `mustFail` accepted any Exchange error: timeout,
|
|
HTTP 5xx, invalid token or JWKS failure could falsely prove excess-scope denial
|
|
or predecessor rejection after a successful positive exchange. Preserve a bounded
|
|
typed provider refusal, require the exact token endpoint/status/error/feature,
|
|
and prove the checker rejects unrelated failures without exposing provider bodies.
|
|
Publish the verified image candidate and return its immutable digest to T02.
|
|
|
|
Platform's 2026-09-08 return is RPF-WP-0035-T05 and proposed CCR-2026-0017/0018.
|
|
The custody field is `CLIENT_SECRET`; Kubernetes key `client-secret` and both env
|
|
names remain as requested. These records cover verifier-side delivery only.
|
|
Owner approval, client-side retrieval and the actual upstream ID-token issuer
|
|
proof remain distinct gates. Public discovery currently advertises
|
|
`https://auth.coulomb.social`; that alone is not the signed-token observation.
|
|
|
|
T06 completion: full Go suite and vet passed; published code `dcebd46` and pulled the image by digest `sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`. Container `keycape verify-client --help` confirms the command is present. `docs/approval-clients-rollout.md` and its proposed deployment patch record configuration/issuer, first-provision, single-instance rollout, acceptance and rollback. This closes verifier preparation only; T02 and T05 retain the explicit live dependencies.
|
|
|
|
|
|
## Prepare a bounded upstream issuer observation before client rollout
|
|
|
|
```task
|
|
id: KEY-WP-0013-T07
|
|
status: progress
|
|
priority: high
|
|
assignee: the-custodian
|
|
```
|
|
|
|
2026-09-08: T02's actual upstream signed-token issuer gate had no executable
|
|
observation path. A downstream OpenBao/KeyCape login cannot return the upstream
|
|
Authelia token; it remains inside the adapter. Prepare `probe-upstream-issuer`
|
|
and an exact-state temporary route so one attended flow can be verified before
|
|
changing the normal KeyCape Deployment or its configuration.
|
|
|
|
Acceptance: signed issuer/audience/time/nonce proof without token or user-claim
|
|
output; browser/state binding and one-shot refusal; finite issuer allowlist;
|
|
ten-minute bound; no downstream credential; current Traefik route isolation;
|
|
immutable image; reviewed temporary Job/config projection, network policies and
|
|
owner-reference cleanup. Normal callbacks and the production issuer stay on
|
|
the existing Deployment. The probe mounts only `config.yaml`, not `key.pem`,
|
|
and has no Kubernetes API token. That file contains existing credential data;
|
|
its use by the temporary diagnostic needs deployment-owner admission.
|
|
|
|
Source tests, image and deployment packet close this preparation task. The
|
|
attended live receipt and issuer pin remain in T02, alongside the named CCR
|
|
reviews and custody/rollout acceptance. Preparing the diagnostic does not
|
|
approve CCR-2026-0017/0018 or close any live factory gate.
|