2026-08-23 11:27:10 +02:00
|
|
|
|
---
|
|
|
|
|
|
id: SECRETS-WP-0007
|
|
|
|
|
|
type: workplan
|
|
|
|
|
|
title: "Production-safe provisioning, authorization, and lifecycle hardening"
|
|
|
|
|
|
domain: infotech
|
|
|
|
|
|
repo: secrets-engine
|
2026-08-23 12:05:58 +02:00
|
|
|
|
status: active
|
2026-08-23 11:27:10 +02:00
|
|
|
|
owner: codex
|
|
|
|
|
|
topic_slug: custodian
|
|
|
|
|
|
created: "2026-08-23"
|
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00
|
|
|
|
updated: "2026-09-06"
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_workstream_id: "68a39be1-bd9c-5133-ad64-e7bca892aaf3"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# SECRETS-WP-0007 - Production-safe provisioning, authorization, and lifecycle hardening
|
|
|
|
|
|
|
|
|
|
|
|
## Goal
|
|
|
|
|
|
|
|
|
|
|
|
Close the highest-risk gaps between the proven CLI MVP and the production
|
|
|
|
|
|
workflow described by `INTENT.md`, before expanding secrets-engine into a
|
|
|
|
|
|
service or moving additional high-risk lanes from interim routing to native
|
|
|
|
|
|
operation.
|
|
|
|
|
|
|
|
|
|
|
|
The source assessment is
|
|
|
|
|
|
`history/2026-08-23-scope-intent-capability-gap-assessment.md`. The most relevant
|
|
|
|
|
|
gaps are:
|
|
|
|
|
|
|
|
|
|
|
|
1. single-field `bao kv put` can replace siblings on a multi-field path and puts
|
|
|
|
|
|
the value in the local subprocess argument vector;
|
|
|
|
|
|
2. KV `revoke` destroys metadata while leaving policy/AppRole access behind,
|
|
|
|
|
|
and its dry-run does not match live behavior;
|
|
|
|
|
|
3. production approval checks are coarse status checks, accept local fixtures,
|
|
|
|
|
|
and do not bind actor, action, lane, fields, expiry, or dual control;
|
|
|
|
|
|
4. AppRole delivery tokens are not explicitly revoked after exec/verification;
|
|
|
|
|
|
5. route and verification can attest only the first field and use a garbage
|
|
|
|
|
|
token rather than a real unrelated identity for KV denial;
|
|
|
|
|
|
6. evidence is best-effort and incomplete on failure paths.
|
|
|
|
|
|
|
|
|
|
|
|
## Scope and sequencing
|
|
|
|
|
|
|
|
|
|
|
|
This workplan hardens the existing CLI contract first. It covers KV write
|
|
|
|
|
|
transport and merge semantics, lifecycle command semantics, decision binding,
|
|
|
|
|
|
short-lived delivery sessions, verification/readiness, and evidence. It then
|
|
|
|
|
|
provides the safe implementation gate for resuming `SECRETS-WP-0006-T05/T06`.
|
|
|
|
|
|
|
|
|
|
|
|
The first task is containment. No task may exercise live production values to
|
|
|
|
|
|
develop or test a primitive. Use fake values and a throwaway OpenBao instance
|
|
|
|
|
|
until the relevant behavior has passed unit and integration tests. Existing ESO,
|
|
|
|
|
|
Kubernetes/OIDC delivery, provider rotation, and application health remain with
|
|
|
|
|
|
their current owners.
|
|
|
|
|
|
|
|
|
|
|
|
## Non-goals
|
|
|
|
|
|
|
|
|
|
|
|
- A service API, daemon, UI, scheduler, or remote multi-user protocol.
|
|
|
|
|
|
- Runtime tenant authentication, quotas, or a tenant control plane.
|
|
|
|
|
|
- A second custody backend or backend-portability project.
|
|
|
|
|
|
- Provider-specific rotation automation.
|
|
|
|
|
|
- Replacing flex-auth, State Hub decisions, key-cape/user-engine identity,
|
|
|
|
|
|
ops-warden routing, ops-bridge transport, or OpenBao enforcement.
|
|
|
|
|
|
- Unparking the standalone `warden-sign` AppRole as a break-glass path.
|
|
|
|
|
|
|
|
|
|
|
|
Service mode remains deferred until the exit criteria here are satisfied; it
|
|
|
|
|
|
must not expose the current bootstrap and lifecycle shortcuts as an API.
|
|
|
|
|
|
|
|
|
|
|
|
## Safety invariants
|
|
|
|
|
|
|
|
|
|
|
|
- No raw value in Git, State Hub, chat, prompts, workplans, evidence, normal
|
|
|
|
|
|
logs, environment diagnostics, or a subprocess argument vector.
|
|
|
|
|
|
- No write to a shared multi-field path unless sibling preservation is proven.
|
|
|
|
|
|
- No destructive metadata deletion through a command presented as ordinary
|
|
|
|
|
|
revoke/deactivate.
|
|
|
|
|
|
- Production actions require canonical, exact-action approval. Offline fixtures
|
|
|
|
|
|
cannot authorize steady-state production mutations or delivery.
|
|
|
|
|
|
- Existing workload delivery stays in place until separately approved owner
|
|
|
|
|
|
coordination says otherwise.
|
|
|
|
|
|
- Tests use fake values and throwaway OpenBao only; live checks are
|
|
|
|
|
|
metadata/capability-safe and approval-gated.
|
|
|
|
|
|
|
|
|
|
|
|
## Contain unsafe lifecycle surfaces
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T01
|
2026-08-23 12:05:58 +02:00
|
|
|
|
status: done
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "5ddba36d-bc86-5670-baf1-50c872e6577e"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-23 12:05:58 +02:00
|
|
|
|
Completed 2026-08-23. During containment, existing multi-field provisioning
|
|
|
|
|
|
failed before file read or backend access. Ordinary `revoke` builds one
|
|
|
|
|
|
lifecycle plan used by both
|
|
|
|
|
|
dry-run and live execution, deletes only engine-managed native AppRole/policy
|
|
|
|
|
|
objects, and explicitly preserves KV custody, external delivery auth, and
|
|
|
|
|
|
workload delivery. No destructive KV command remains on the ordinary CLI.
|
|
|
|
|
|
Focused lifecycle, admission, auth-capability, and throwaway OpenBao integration
|
|
|
|
|
|
tests pass. The temporary multi-field rejection was then superseded by the
|
|
|
|
|
|
merge-safe primitive completed in T02.
|
|
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Add immediate fail-closed guards before implementing replacement primitives:
|
|
|
|
|
|
|
|
|
|
|
|
- reject `provision` on an existing multi-field lane;
|
|
|
|
|
|
- reject or rename the current destructive KV `revoke` path so it cannot be
|
|
|
|
|
|
mistaken for soft deactivation;
|
|
|
|
|
|
- make KV revoke dry-run render exactly the operations the live path would run;
|
|
|
|
|
|
- document that `SECRETS-WP-0006` may apply/verify metadata but must not provision
|
|
|
|
|
|
or revoke shared values through the current commands.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- A regression test proves no backend call occurs for multi-field existing-lane
|
|
|
|
|
|
provisioning.
|
|
|
|
|
|
- A regression test proves dry-run and live operation plans contain the same KV,
|
|
|
|
|
|
AppRole, and policy targets.
|
|
|
|
|
|
- The destructive action requires an explicit name/flag and a separately
|
|
|
|
|
|
approved action; ordinary revoke cannot destroy KV metadata.
|
|
|
|
|
|
- CLI/help/runbook language no longer describes metadata deletion as reversible
|
|
|
|
|
|
deactivation.
|
|
|
|
|
|
|
|
|
|
|
|
## Implement merge-safe, non-argv provisioning
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T02
|
2026-08-23 12:05:58 +02:00
|
|
|
|
status: done
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "ddf3ed54-9cbc-5e18-b15a-5da14cda5d9b"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-23 12:05:58 +02:00
|
|
|
|
Completed 2026-08-23. Replaced raw `field=value` argv writes with a CAS-aware
|
|
|
|
|
|
backend primitive: new paths use CAS-zero create; existing paths use server-side
|
|
|
|
|
|
HTTP patch with the observed metadata version. Values travel through a
|
|
|
|
|
|
mode-0600 temporary JSON reference removed in `finally`, and never appear in
|
|
|
|
|
|
argv. Unit tests inspect the full request/cleanup; throwaway OpenBao integration
|
|
|
|
|
|
proves sibling preservation and stale-CAS rejection. Stage policies now include
|
|
|
|
|
|
the narrow KV `patch` capability needed by this operation.
|
|
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Replace the raw `field=value` subprocess call with a backend write primitive
|
|
|
|
|
|
that never places the value in argv and safely updates one or more declared
|
|
|
|
|
|
fields without removing unmentioned siblings. The design may use OpenBao CAS,
|
|
|
|
|
|
a merge/patch operation, an input file reference with strict cleanup, or direct
|
|
|
|
|
|
request input, but it must remain isolated behind `OpenBaoClient` and preserve
|
|
|
|
|
|
the catalog field allowlist.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- Unit tests inspect the complete subprocess request and prove the fake value is
|
|
|
|
|
|
absent from argv, environment, output, exceptions, and evidence.
|
|
|
|
|
|
- Throwaway OpenBao integration proves updating one field preserves every
|
|
|
|
|
|
sibling field and fails closed on a CAS conflict.
|
|
|
|
|
|
- Multi-field input is atomic from the caller's perspective; a partial failure
|
|
|
|
|
|
cannot silently leave an unreported mixed version.
|
|
|
|
|
|
- Source and generated inputs retain mode/path guards and guaranteed temporary
|
|
|
|
|
|
cleanup.
|
|
|
|
|
|
- Production generation remains forbidden, and high-risk production
|
|
|
|
|
|
provisioning is still decision- and dual-control-gated.
|
|
|
|
|
|
|
|
|
|
|
|
## Split suspend, deactivate, and destroy semantics
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T03
|
2026-08-29 14:36:38 +02:00
|
|
|
|
status: done
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "bb6073e1-c952-5357-b707-5574cef50f2f"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-29 14:36:38 +02:00
|
|
|
|
Completed 2026-08-29. Suspend, deactivate, and destroy share one plan object
|
|
|
|
|
|
for dry-run and live execution. Ordinary `revoke` aliases native deactivation
|
|
|
|
|
|
and preserves KV custody plus externally managed workload delivery. Live
|
|
|
|
|
|
destroy stays fail-closed until T04's exact-action contract; that is the
|
|
|
|
|
|
designed containment, not unfinished T03 work. Issued delivery sessions
|
|
|
|
|
|
self-revoke in T05.
|
|
|
|
|
|
|
2026-09-02 10:08:33 +02:00
|
|
|
|
Residual from this task, now implemented: `secrets-engine session revoke`
|
|
|
|
|
|
`--accessor-file` or `--lease-id-file` (hardening H4a).
|
2026-08-29 14:36:38 +02:00
|
|
|
|
|
2026-08-23 12:05:58 +02:00
|
|
|
|
Progress 2026-08-23. Added one lifecycle plan model shared by dry-run and live
|
|
|
|
|
|
execution. Ordinary `revoke` safely aliases native deactivation; suspend and
|
|
|
|
|
|
deactivate preserve KV custody and externally managed workload delivery.
|
|
|
|
|
|
Destroy has an unmistakable dry-run plan and exact-id confirmation, but its live
|
|
|
|
|
|
handler is deliberately fail-closed before coarse lane approval or backend
|
|
|
|
|
|
access until T04 supplies distinct action authorization. Scoped issued-session
|
2026-09-02 01:24:08 +02:00
|
|
|
|
self-revocation is implemented in T05; a general known-accessor operator
|
|
|
|
|
|
command is `session revoke --accessor-file` (H4a).
|
2026-08-23 12:05:58 +02:00
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Replace the overloaded `revoke` behavior with explicit lifecycle operations and
|
|
|
|
|
|
plans. Define at least:
|
|
|
|
|
|
|
|
|
|
|
|
- **suspend delivery**: stop minting new native delivery sessions while
|
|
|
|
|
|
preserving custody and a documented rollback;
|
|
|
|
|
|
- **deactivate native access**: remove/disable the lane AppRole and consumer
|
|
|
|
|
|
policy without deleting the KV value;
|
|
|
|
|
|
- **revoke issued sessions**: revoke known tokens/leases by non-secret accessor
|
|
|
|
|
|
where the backend supports it;
|
|
|
|
|
|
- **destroy custody metadata**: irreversibly delete KV metadata/versions only
|
|
|
|
|
|
through an unmistakable, separately approved action.
|
|
|
|
|
|
|
|
|
|
|
|
Preserve compatibility only where it cannot mislead. A legacy `revoke` alias
|
|
|
|
|
|
may map to a safe non-destructive action; it must never silently retain the
|
|
|
|
|
|
current destructive meaning.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- Each operation has a guarded dry-run plan, exact targets, distinct approval
|
|
|
|
|
|
action, non-secret evidence, and documented rollback/irreversibility.
|
|
|
|
|
|
- Dry-run and live use the same plan object rather than separate handwritten
|
|
|
|
|
|
descriptions.
|
|
|
|
|
|
- Integration tests prove suspend/deactivate preserve KV data and destroy does
|
|
|
|
|
|
not run without its explicit approval and confirmation contract.
|
|
|
|
|
|
- Auth-capability and KV lanes have coherent but kind-appropriate behavior.
|
|
|
|
|
|
- Existing workload delivery is reported as an external dependency and is not
|
|
|
|
|
|
deleted by native lifecycle commands.
|
|
|
|
|
|
|
|
|
|
|
|
## Bind approvals to exact production actions
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T04
|
2026-08-29 14:36:38 +02:00
|
|
|
|
status: wait
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "4b58edec-c705-55e5-9ece-362e1ff13079"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-29 14:36:38 +02:00
|
|
|
|
Wait 2026-08-29. The consumer validator and production fail-closed gate are
|
|
|
|
|
|
shipped. What remains is not local engine work: State Hub / `access-engine`
|
|
|
|
|
|
must serve the durable ActionAuthorization object. Paired with
|
|
|
|
|
|
`SECRETS-WP-0008-T02`. Do not reopen live production on hub status or local
|
|
|
|
|
|
fixtures.
|
|
|
|
|
|
|
2026-09-02 01:06:50 +02:00
|
|
|
|
Progress 2026-09-02. `GH-DEC-2026-003` consume-before-OpenBao is now a
|
|
|
|
|
|
shared production gate: `_require_lane_approval` CAS-consumes through
|
|
|
|
|
|
approval-engine before any OpenBao client is resolved. Tests prove 409,
|
|
|
|
|
|
unreachable, and missing-binding paths never call OpenBao. Live production
|
|
|
|
|
|
stays fail-closed until the durable object is served; this task remains
|
|
|
|
|
|
`wait`.
|
|
|
|
|
|
|
2026-08-23 14:15:42 +02:00
|
|
|
|
Progress 2026-08-23. flex-auth revision `c473f19` now supplies execute-time
|
|
|
|
|
|
structured binding and the proposed durable `ActionAuthorization` schema. The
|
|
|
|
|
|
engine builds and fail-closed validates the exact secrets-engine profile,
|
|
|
|
|
|
validity, State Hub authority, decision digest/binding, accepted policy
|
|
|
|
|
|
revision, and a caller-selected minimum distinct-approver threshold. Every live
|
|
|
|
|
|
production handler now carries its action into the shared gate and fails before
|
|
|
|
|
|
OpenBao while State Hub lacks the durable endpoint. Legacy local decisions are
|
|
|
|
|
|
reachable for prod-labeled lanes only with an explicit unsafe-demo switch,
|
|
|
|
|
|
disabled Hub URL, and loopback OpenBao. The throwaway scripts declare that
|
|
|
|
|
|
exception. Live destroy remains disabled independently. State Hub endpoint and
|
2026-08-23 14:38:51 +02:00
|
|
|
|
authenticated approval storage are still outstanding. flex-auth corrected its
|
|
|
|
|
|
example digest and added a complete binding regression assertion in `d402408`.
|
2026-08-23 12:05:58 +02:00
|
|
|
|
|
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00
|
|
|
|
Correction 2026-09-06. The 2026-08-29 note above ("What remains is not local
|
|
|
|
|
|
engine work") was wrong. `resolve_consume_binding` was a `return None` stub, so
|
|
|
|
|
|
protocol step 1 (PIP `GET /v1/approvals/{id}/claim`) and the validation join
|
|
|
|
|
|
were never implemented, and `validate_action_authorization` — the validator this
|
|
|
|
|
|
plan called "shipped" — had no caller in `src/` at all. Only step 3 (CAS
|
|
|
|
|
|
consume) and the OpenBao gate were real. Production fail-closed was therefore
|
|
|
|
|
|
correct but for a second, undocumented reason.
|
|
|
|
|
|
|
|
|
|
|
|
The join is now implemented. `resolve_consume_binding` reproduces the exact
|
|
|
|
|
|
CheckRequest via `build_action_request`, fetches the durable
|
|
|
|
|
|
ActionAuthorization, and validates request binding, digest, validity, authority,
|
|
|
|
|
|
policy pin, and distinct-approver threshold before returning a binding.
|
|
|
|
|
|
`_require_lane_approval` threads the exact field set for provision, rotate,
|
|
|
|
|
|
verify, and exec so the digest covers the real proposed action. Eight tests in
|
|
|
|
|
|
`tests/test_consume_binding_join.py` cover unconfigured, half-configured, digest
|
|
|
|
|
|
mismatch, action mismatch, unreachable, and superseded paths.
|
|
|
|
|
|
|
|
|
|
|
|
Two deliberate choices, both recorded in `docs/approval-consumption.md`:
|
|
|
|
|
|
the approval-engine object id is never inferred from a State Hub decision UUID
|
|
|
|
|
|
(flex-auth: `GET /decisions/{uuid}` is not the durable object); and there is no
|
|
|
|
|
|
default policy pin, because flex-auth stated `secrets-engine.lifecycle`/`v1` is
|
|
|
|
|
|
example vocabulary rather than a published package.
|
|
|
|
|
|
|
|
|
|
|
|
Behavior today is bit-for-bit unchanged: every new input is absent by default,
|
|
|
|
|
|
so an unconfigured engine still fails production closed, and `plan`/`--dry-run`
|
|
|
|
|
|
still work. This task stays `wait`, but the remaining constraint is now purely
|
|
|
|
|
|
deployment: approval-engine must serve the claim endpoint and access-engine must
|
|
|
|
|
|
serve Check. Probed 2026-09-06 — neither is reachable, and State Hub exposes
|
|
|
|
|
|
only `/decisions/`.
|
|
|
|
|
|
|
2026-09-06 01:29:13 +02:00
|
|
|
|
Inbound 2026-09-06 (approval-engine `APPROVAL-IN-0002`, flex-auth
|
|
|
|
|
|
`FLEX-DEC-2026-005`). The join committed in `627810b` validates the wrong
|
|
|
|
|
|
artifact and must not be extended until gate-house rules.
|
|
|
|
|
|
|
|
|
|
|
|
- `ActionAuthorization` is a PROPOSED, unratified object from flex-auth's own
|
|
|
|
|
|
contract. It appears zero times in gate-house and state-hub. `GH-DEC-2026-003`
|
|
|
|
|
|
already names step 1 as `GET /v1/approvals/{id}/claim -> valid_now`, an
|
|
|
|
|
|
approval-claim field that `ActionAuthorization` does not carry. The claim body
|
|
|
|
|
|
is an approval-claim envelope, not an `ActionAuthorization`.
|
|
|
|
|
|
- Confirmed defect, ours, independent of the ruling: `authorization.py` pins
|
|
|
|
|
|
`AUTHORITY = "state-hub"` and enforces it unconditionally, contradicting
|
|
|
|
|
|
flex-auth's statement that State Hub decision records are not the runtime
|
|
|
|
|
|
approval authority and our own read-model doctrine. Not patched yet: under
|
|
|
|
|
|
option A the authority check belongs on the claim issuer, so it moves with the
|
|
|
|
|
|
split rather than being guessed twice.
|
|
|
|
|
|
- If gate-house confirms option A, `validate_action_authorization` splits across
|
|
|
|
|
|
the two artifacts already fetched: the claim carries approval fact (binding
|
|
|
|
|
|
digest, validity window, consumption state, freshness, issuer); the step-2
|
|
|
|
|
|
DecisionEnvelope carries exact CheckRequest match and the package/version pin.
|
|
|
|
|
|
No check is lost. Do not start until the ruling.
|
|
|
|
|
|
- Auth gap, tracked separately so it does not ride the envelope decision: we
|
|
|
|
|
|
send a static mode-0600 token as Bearer. Production verifies a KeyCape RS256
|
|
|
|
|
|
JWT against JWKS and refuses opaque tokens. Requested registration clientId
|
|
|
|
|
|
`secrets-engine-approval`, scopes `approval:read` + `approval:consume`,
|
|
|
|
|
|
client_credentials, confidential. `aud` MUST be the resource server
|
|
|
|
|
|
`approval-engine`, never the clientId. `service_auth.py` already implements
|
|
|
|
|
|
this exchange for `secrets-engine-openbao`, so this is a second registration.
|
|
|
|
|
|
- approval-engine `APPROVAL-WP-0002-T03` will publish the base URL; it is not
|
|
|
|
|
|
deployed anywhere today and proceeds on its own evidence. The envelope
|
|
|
|
|
|
question gates our first live consume, not their rollout.
|
|
|
|
|
|
- flex-auth endorsed the no-default-pin decision explicitly. The reserved
|
|
|
|
|
|
coordinate `secrets-engine.catalog-lane.lifecycle` / `v1` is a RESERVATION,
|
|
|
|
|
|
not a publication; our pin stays unset. `docs/gated-actions.md` delivers the
|
|
|
|
|
|
authoritative twelve-action list `FLEX-WP-0021-T01` was blocked on. No
|
|
|
|
|
|
estate-wide PDP exists by design: flex-auth runs per-consumer cluster-local
|
|
|
|
|
|
pins, and `flex-auth-secrets-engine` has not been created yet, so the
|
|
|
|
|
|
2026-09-06 probe finding was the design working rather than an outage.
|
|
|
|
|
|
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 08:02:01 +02:00
|
|
|
|
Resolved 2026-09-06 by gate-house `GH-DEC-2026-005` (GH-IN-0002), settling
|
|
|
|
|
|
approval-engine's `APPROVAL-IN-0002`. Both changes it assigned to this repo are
|
|
|
|
|
|
implemented.
|
|
|
|
|
|
|
|
|
|
|
|
1. The validator is split by owning layer. `approval_claim.validate_approval_claim`
|
|
|
|
|
|
consumes approval-engine's approval-claim for the approval fact (issuer,
|
|
|
|
|
|
`valid_now`, consumption state, binding digest, freshness, `reason_code`).
|
|
|
|
|
|
`authorization.validate_decision_envelope` consumes the flex-auth
|
|
|
|
|
|
DecisionEnvelope for the decision (effect, exact CheckRequest binding,
|
|
|
|
|
|
canonical request digest, lifetime, policy package/version pin). Neither
|
|
|
|
|
|
republishes the other's data. `ActionAuthorization` is deferred and never
|
|
|
|
|
|
ratified (`FLEX-DEC-2026-006`); nothing validates it any more.
|
|
|
|
|
|
2. `AUTHORITY = "state-hub"` and the `provenance.authority` requirement are
|
|
|
|
|
|
gone. flex-auth traced the constant to their own fixture
|
|
|
|
|
|
(`examples/caring/action_authorization.json`), which contradicted their
|
|
|
|
|
|
ownership section — their bug, fixed at source. State Hub is a read model and
|
|
|
|
|
|
holds no runtime approval authority, so the check failed closed against every
|
|
|
|
|
|
correctly issued record.
|
|
|
|
|
|
|
|
|
|
|
|
Two consequences worth stating rather than burying:
|
|
|
|
|
|
|
|
|
|
|
|
- There are now **two different digests** over the same proposed action, by
|
|
|
|
|
|
contract, never compared to each other: the approval-engine native binding
|
|
|
|
|
|
digest over `{action, actor, principal, purpose, target}`, and the flex-auth
|
|
|
|
|
|
canonical CheckRequest digest. `claim.binding.pdp_digest` is preferred when
|
|
|
|
|
|
the issuer recorded one. The CheckRequest digest itself is unchanged and its
|
|
|
|
|
|
contract test is preserved verbatim — flex-auth confirmed only the envelope
|
|
|
|
|
|
went away, not the digest join.
|
|
|
|
|
|
- The distinct-approver threshold is **no longer a consumer-side check**. The
|
|
|
|
|
|
claim does not expose approver entries; approval-engine folds that requirement
|
|
|
|
|
|
into `valid_now`. We now rely on the issuer for it, which is the correct layer
|
|
|
|
|
|
but is a real reduction in what this engine verifies independently.
|
|
|
|
|
|
|
|
|
|
|
|
Still outstanding, unchanged by the ruling: step 2 needs the
|
|
|
|
|
|
`flex-auth-secrets-engine` Service DNS (not created — per-consumer cluster-local
|
|
|
|
|
|
pins are the design, so the earlier "no reachable PDP" probe was not an outage)
|
|
|
|
|
|
and the published package from `FLEX-WP-0021-T02`. The pin stays unset.
|
|
|
|
|
|
`docs/gated-actions.md` delivered the twelve-action vocabulary that unblocks
|
|
|
|
|
|
`FLEX-WP-0021-T01`. Separately tracked: production requires a KeyCape RS256 JWT,
|
|
|
|
|
|
not the static Bearer token this engine currently sends.
|
|
|
|
|
|
|
2026-09-06 08:09:42 +02:00
|
|
|
|
Closed out 2026-09-06. gate-house amended `GH-DEC-2026-005`; flex-auth published
|
|
|
|
|
|
the policy package (`FLEX-WP-0021-T01`/`T02` done, commit `f75db59`).
|
|
|
|
|
|
|
|
|
|
|
|
- `secrets-engine.catalog-lane.lifecycle` v1 exists with both manifests, a
|
|
|
|
|
|
loadable registry snapshot, 26 fixtures and five standalone check requests;
|
|
|
|
|
|
`validate -kind policy` reports 22/22 Rego tests and 26/26 fixtures. Our
|
|
|
|
|
|
vocabulary is recorded in flex-auth `docs/secrets-engine-action-vocabulary.md`.
|
|
|
|
|
|
All four constraints from `docs/gated-actions.md` are encoded rather than
|
|
|
|
|
|
assumed: `test_revoke_is_not_an_action` asserts `revoke` denies
|
|
|
|
|
|
`unknown_action`, `destroy` is fixtured as the dual-control case before
|
|
|
|
|
|
`T04` opens the path, `compromise`/`reactivate` are annotated overlay-only,
|
|
|
|
|
|
and the ungated verbs deny `unknown_action`. flex-auth reports four of the
|
|
|
|
|
|
twelve would have been inferred wrongly, `revoke` most confidently.
|
|
|
|
|
|
- **A published limit now rests on a property of this repo.** `apply` and
|
|
|
|
|
|
`apply --dry-run` are indistinguishable to a PDP; both arrive as action
|
|
|
|
|
|
`apply`. What separates them is that our PEP does not call the gate for a dry
|
|
|
|
|
|
run. flex-auth recorded that as a limit rather than implying a control they do
|
|
|
|
|
|
not have, and asked to be told if the assumption stops holding.
|
|
|
|
|
|
`tests/test_dry_run_never_gates.py` now enforces it across all five dry-run
|
|
|
|
|
|
handlers, with a paired test proving the hook is load-bearing. Changing a
|
|
|
|
|
|
dry-run path to gate is now a test failure carrying the instruction to notify
|
|
|
|
|
|
flex-auth first.
|
|
|
|
|
|
- Dual control on `destroy` checks what the claim *says* (two distinct approver
|
|
|
|
|
|
subject ids). flex-auth deliberately does not re-derive temporal validity,
|
|
|
|
|
|
signature, or supersession — those are approval-engine's to assert and **ours
|
|
|
|
|
|
to verify against the live claim**. `validate_approval_claim` is that check.
|
|
|
|
|
|
- `allow_ttl` is 15m in the package, stated rather than left to an engine
|
|
|
|
|
|
default. Per `FLEX-DEC-2026-004` that is authority to *issue* the operation,
|
|
|
|
|
|
not to keep using what it produced; bounding a delivered secret's own lifetime
|
|
|
|
|
|
is ours. Already satisfied: `normalize_wrap_ttl` caps wrap at 15m and lane
|
|
|
|
|
|
AppRoles carry 5m/15m token TTLs with a single-use Secret ID.
|
|
|
|
|
|
- Structural note carried as a revisit trigger, not a defect: because we named
|
|
|
|
|
|
exactly one calling identity, the denial ladder has no `action_not_granted`
|
|
|
|
|
|
branch — with one subject holding all twelve actions it could never fire, and
|
|
|
|
|
|
a rule that cannot fail reads as per-action control that is not there.
|
|
|
|
|
|
Registering a second identity, or splitting the CLI identity by lane or stage,
|
|
|
|
|
|
is the trigger; adding the branch then is additive and needs no request-shape
|
|
|
|
|
|
change.
|
|
|
|
|
|
- gate-house struck the G3 revisit trigger for a composed authorization object,
|
|
|
|
|
|
verified against the DecisionEnvelope schema: `lifetime` is required whenever
|
|
|
|
|
|
effect is allow, so carrying its own end — the one structural thing the bundle
|
|
|
|
|
|
did that the split does not — is already answered. Three triggers remain: a
|
|
|
|
|
|
third or fourth PEP-shaped consumer, a single signed forwardable artifact, and
|
|
|
|
|
|
§17 request-claim assent.
|
|
|
|
|
|
- On the approver threshold we stopped checking, gate-house recorded both that
|
|
|
|
|
|
it is correct on layering **and** a genuine reduction in defence in depth,
|
|
|
|
|
|
noting a decision recording only the first would be self-serving. The
|
|
|
|
|
|
compensating property is reconstructability at the issuer under §9.6 —
|
|
|
|
|
|
detection, not prevention — not a second check here.
|
|
|
|
|
|
|
|
|
|
|
|
**The pin stays unset.** Publishing is not deploying: there is still no
|
|
|
|
|
|
`flex-auth-secrets-engine` pin and therefore no address to call. `T04` stands it
|
|
|
|
|
|
up in `callerAuth.mode warn`; `T05` hands over the Service DNS and confirms the
|
|
|
|
|
|
package/version to configure. Next inbound is the `T03` replay fixture — a real
|
|
|
|
|
|
DecisionEnvelope from this package including `provenance.registry_snapshot_digest`
|
|
|
|
|
|
— to verify the digest join unchanged.
|
|
|
|
|
|
|
fix: exclude correlation fields from the flex-auth request digest
Verified the digest join against flex-auth's T03 replay fixtures and found
request_digest was hashing fields docs/canonical-request-digest.md excludes.
The material is tenant, subject, action, resource, context only: id is
correlation, policy_version lives in provenance, caring_context is hashed
separately. This engine included all three when present.
Because the join adopts the served request id, every real production request
would have carried one, so the computed digest would have matched no issued
decision and failed closed against every correct allow. Same unsatisfiable
shape as the removed AUTHORITY constant.
The old pinned constant was computed with the id inside the material, so it
was wrong and its passing proved nothing. Replaced with fixture-driven tests
over two real envelopes (vendored with provenance) plus a structural test
that correlation fields do not move the digest. Both fixtures are needed:
input_claim_digests.context appears only with a non-empty context.
Also stops computing the native claim digest. The claim's binding.action and
binding.target speak approval-engine's vocabulary while ours speaks the
catalog's, and no mapping is published; flex-auth makes no cross-check and
states the correspondence is ours via pdp_digest. A claim recording no
pdp_digest now fails closed naming the missing mapping rather than comparing
two different languages. That mapping is a prerequisite for destroy.
274 tests pass. Production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:17:38 +02:00
|
|
|
|
Verified 2026-09-06 against flex-auth's `T03` replay fixtures
|
|
|
|
|
|
(`FLEX-WP-0021-T03`, commit `9e10d1c`), vendored to
|
|
|
|
|
|
`tests/fixtures/flex-auth-replay/`. Running them found a defect of exactly the
|
|
|
|
|
|
class we had just been warning about.
|
|
|
|
|
|
|
|
|
|
|
|
- **`request_digest` was hashing fields the contract excludes.** The material is
|
|
|
|
|
|
`tenant`, `subject`, `action`, `resource`, `context` only; `id` is correlation,
|
|
|
|
|
|
`policy_version` lives in provenance, and `caring_context` is hashed
|
|
|
|
|
|
separately. This engine included all three when present. Because the join
|
|
|
|
|
|
adopts the served request id, every real production request would have carried
|
|
|
|
|
|
one, so the computed digest would have matched no issued decision and failed
|
|
|
|
|
|
closed against every correct allow — the same unsatisfiable-rule shape as the
|
|
|
|
|
|
removed `AUTHORITY` constant and as flex-auth's own destroy rule in `68ad039`.
|
|
|
|
|
|
Fixed by `digest_material`; both fixtures now reproduce their published
|
|
|
|
|
|
digests exactly.
|
|
|
|
|
|
- The previously pinned digest constant was computed *with* the id inside the
|
|
|
|
|
|
material, so it was wrong and its passing was not evidence. It is replaced by
|
|
|
|
|
|
fixture-driven contract tests over two real envelopes plus a structural test
|
|
|
|
|
|
that correlation fields do not move the digest. Both fixtures are required:
|
|
|
|
|
|
`provenance.input_claim_digests.context` appears only with a non-empty context.
|
|
|
|
|
|
- **The native claim digest is unusable and is no longer computed.** The claim's
|
|
|
|
|
|
`binding.action`/`binding.target` speak approval-engine's vocabulary while ours
|
|
|
|
|
|
speaks the catalog's, and no mapping is published; flex-auth makes no
|
|
|
|
|
|
cross-check and states the correspondence is ours via `pdp_digest`. A claim
|
|
|
|
|
|
recording no `pdp_digest` now fails closed naming the missing mapping rather
|
|
|
|
|
|
than comparing two vocabularies. **A published mapping is a prerequisite for
|
|
|
|
|
|
making `destroy` reachable under this task.** flex-auth offered to co-author it
|
|
|
|
|
|
with approval-engine.
|
|
|
|
|
|
- Narrowing accepted from approval-engine: `entries` is UNIQUE on
|
|
|
|
|
|
`(approval_id, subject_id)`, so approver distinctness is a storage invariant,
|
|
|
|
|
|
not a count this engine was previously protecting. What we stopped verifying is
|
|
|
|
|
|
that approval-engine applies its own invariant correctly — still a real
|
|
|
|
|
|
reduction in defence in depth, but narrower than "the threshold is now
|
|
|
|
|
|
unverified". They also added `threshold` objects to issuance and use events
|
|
|
|
|
|
(`87e55e2`) so the evaluation is reconstructable under §9.6.
|
|
|
|
|
|
- flex-auth corrected the `destroy` dual-control rule (`68ad039`) before
|
|
|
|
|
|
generating these fixtures: it had required `context.approval.status ==
|
|
|
|
|
|
'approved'` and an approver list, neither of which exists in the claim schema.
|
|
|
|
|
|
Our acknowledgement of the earlier rule crossed that fix. The corrected rule
|
|
|
|
|
|
requires only kind/issuer/`valid_now`, and our claim validation is unchanged.
|
|
|
|
|
|
- One global pin confirmed correct: `secrets-engine.catalog-lane.lifecycle` v1 is
|
|
|
|
|
|
one package covering all twelve actions. Do not build per-action pin
|
|
|
|
|
|
configuration for a distinction that does not exist.
|
|
|
|
|
|
|
2026-09-06 14:56:21 +02:00
|
|
|
|
Brought into practice 2026-09-06. The chain is complete in code and proven end
|
|
|
|
|
|
to end; what remains is deployment only.
|
|
|
|
|
|
|
|
|
|
|
|
- Step 2 is implemented (`decision_check.check_decision`, POST `/v1/check`) and
|
|
|
|
|
|
`approval_consume.authorize_action` coordinates steps 1 and 2. Both build the
|
|
|
|
|
|
same CheckRequest through one `_expected_request`, since two descriptions of
|
|
|
|
|
|
the proposed action cannot produce corresponding digests.
|
|
|
|
|
|
- `apply_unreachable_engine_stance` now takes `authorized=`. The published map
|
|
|
|
|
|
defines `fail_closed` as no protected side effect *without* a durable
|
|
|
|
|
|
access-engine decision record, so holding a validated one means the engine was
|
|
|
|
|
|
reachable and the residue does not apply. This is not a stance bypass: both
|
|
|
|
|
|
steps must have succeeded, CAS consume still precedes OpenBao, and with
|
|
|
|
|
|
nothing configured `authorize_action` returns `None` and production fails
|
|
|
|
|
|
closed exactly as before. Verified by CLI, not only by unit test.
|
|
|
|
|
|
- `tests/test_integration_authorization.py` drives the real gate through
|
|
|
|
|
|
claim -> check -> consume -> OpenBao against a live throwaway OpenBao, and
|
|
|
|
|
|
asserts PIP-before-PDP ordering, consume as the last step before the backend,
|
|
|
|
|
|
and that an unreachable PDP, a denied decision, an invalid claim, a missing
|
|
|
|
|
|
`pdp_digest`, a consume conflict, and an action mismatch each stop before the
|
|
|
|
|
|
backend. The stub serves transport and sequencing only; wire contracts stay
|
|
|
|
|
|
pinned to flex-auth's real fixtures.
|
|
|
|
|
|
- The end-to-end run found a third instance of the cross-vocabulary defect: a
|
|
|
|
|
|
leftover comparison of the claim's `binding.action` against ours. Real claims
|
|
|
|
|
|
say `secrets.kv.destroy` where we say `destroy`, so it would have failed
|
|
|
|
|
|
against every one of them. Removed; the tie is `pdp_digest`. Worth noting the
|
|
|
|
|
|
pattern: unit tests with self-consistent fakes hid all three, and each was
|
|
|
|
|
|
found only by a real artifact or a real chain.
|
|
|
|
|
|
|
feat: bind the destroy gate to approval_binding_digest and pdp_path
The vocabulary mapping this path was waiting on is not coming: gate-house
rejected it in GH-DEC-2026-008, because a translation can be confidently
wrong and fails open by accepting a claim approved for a different action.
The stronger option arrived instead, and both halves are enforced here.
flex-auth published binding.approval_binding_digest (FLEX-DEC-2026-007) to
fix the circularity this repo reported: a pdp_digest recorded at issue time
can never equal the request_digest of the request that carries the claim in
its hashed context, so with GH-DEC-2026-008 requiring that equality, destroy
would have failed closed forever on a check no correct record could pass.
- authorization.approval_binding_digest implements the published exclusion
rule, including Go's context,omitempty behaviour when stripping empties
the context; digest_material drops an empty context for the same reason.
- validate_decision_envelope recomputes the field rather than trusting it,
refuses a claim-bearing request whose decision records none, and compares
the claim's digest from step 1 against it -- never against request_digest,
which still covers the claim so it stays a sound replay identity.
- validate_approval_claim requires binding.pdp_path true before using
pdp_digest at all. Path intent is never inferred from a digest that
happens to be present; pre-schema-v3 approvals carry pdp_path false
regardless of any digest they hold.
Replay fixtures re-vendored from dd3ce4c. The destroy pins moved a second
and final time; approval_binding_digest did not, which is the point. The
fixture now demonstrates the property instead of asserting it: we rederive
fa07becf... from its own request through our canonical implementation,
proving we hash the same material flex-auth does rather than pinning a
constant we cannot reproduce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715726@bnt-lap001
Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-06 20:39:59 +02:00
|
|
|
|
Destroy gate closed 2026-09-06 (inbound `FLEX-DEC-2026-007`, `GH-DEC-2026-008`,
|
|
|
|
|
|
approval-engine schema v3). The vocabulary mapping this task was waiting on is
|
|
|
|
|
|
not coming, and it should not have been waited on: gate-house rejected it
|
|
|
|
|
|
outright, for the reason flex-auth and approval-engine both gave independently —
|
|
|
|
|
|
a translation can be confidently wrong and fails **open** by accepting a claim
|
|
|
|
|
|
approved for a different action. The stronger option arrived instead, and both
|
|
|
|
|
|
halves of it are now enforced here.
|
|
|
|
|
|
|
|
|
|
|
|
- **The circularity we reported is fixed at source.** A `pdp_digest` recorded at
|
|
|
|
|
|
approval-issue time can never equal the `request_digest` of a request that
|
|
|
|
|
|
carries the claim in its hashed context. With `GH-DEC-2026-008` requiring that
|
|
|
|
|
|
equality on the PDP path, destroy would have been permanently un-allowable,
|
|
|
|
|
|
failing closed forever on a check no correct record could pass. flex-auth
|
|
|
|
|
|
published `binding.approval_binding_digest` (commit `dd3ce4c`): the same
|
|
|
|
|
|
canonical digest with `context.approval` removed, stable across attaching the
|
|
|
|
|
|
claim. The comparison is against that field, never `request_digest`.
|
|
|
|
|
|
- **`request_digest` deliberately still covers the claim**, so the two digests
|
|
|
|
|
|
disagree on a claim-bearing request and that disagreement is load-bearing: two
|
|
|
|
|
|
requests differing only in which approval was presented must not share a
|
|
|
|
|
|
replay identity, or an allow obtained with a valid claim could be replayed
|
|
|
|
|
|
against a request carrying none. `approval_binding_digest` is not a replay
|
|
|
|
|
|
identity and is not used as one.
|
|
|
|
|
|
- `authorization.approval_binding_digest` implements the published exclusion
|
|
|
|
|
|
rule, including flex-auth's `json:"context,omitempty"` behaviour of dropping a
|
|
|
|
|
|
context that strips empty. `digest_material` now drops an empty context for the
|
|
|
|
|
|
same reason. `validate_decision_envelope` recomputes the field when present
|
|
|
|
|
|
rather than trusting it, refuses a claim-bearing request whose decision records
|
|
|
|
|
|
none, and takes the claim's digest from step 1 as `expected_approval_binding_digest`.
|
|
|
|
|
|
- **`binding.pdp_path` is required true** before `pdp_digest` is used at all.
|
|
|
|
|
|
It is approval-engine's declaration that the approval was requested against a
|
|
|
|
|
|
bound CheckRequest; their `create()` refuses `pdp_path` true without a digest,
|
|
|
|
|
|
so the declaration guarantees the digest. The converse is not inferred: a
|
|
|
|
|
|
digest present for some other reason is no declaration, and pre-v3 approvals
|
|
|
|
|
|
carry `pdp_path` false regardless of any digest they hold.
|
|
|
|
|
|
- The cost is accepted rather than engineered around: an approval requested
|
|
|
|
|
|
without a bound CheckRequest is not usable on this path and will not become
|
|
|
|
|
|
usable later. If a real destroy workflow cannot bind at issue, that is the
|
|
|
|
|
|
falsifier gate-house wrote into the reversal and it gets raised, not
|
|
|
|
|
|
worked around.
|
|
|
|
|
|
|
|
|
|
|
|
The replay fixtures are re-vendored from `dd3ce4c`, and the pins moved a second
|
|
|
|
|
|
and final time (`request_digest` `sha256:c749ee2d…`, context input-claim digest
|
|
|
|
|
|
`sha256:8b73d29e…`; `approval_binding_digest` `sha256:fa07becf…`, which did
|
|
|
|
|
|
**not** move — that is the point). The fixture now demonstrates the property
|
|
|
|
|
|
instead of asserting it: `test_pdp_digest_equals_the_published_approval_binding_digest`
|
|
|
|
|
|
rederives `fa07becf…` from the fixture's own request through our canonical
|
|
|
|
|
|
implementation, which is a hermetic proof that this engine hashes the same
|
|
|
|
|
|
material flex-auth does. A pin alone would only have asserted the constant.
|
|
|
|
|
|
`decision_rotate.json` is unchanged and carries no `approval_binding_digest`;
|
|
|
|
|
|
a test pins that omission so the field is not silently duplicated onto ordinary
|
|
|
|
|
|
decisions.
|
|
|
|
|
|
|
|
|
|
|
|
Our own CheckRequest stays claim-free (`context` is `{"purpose": …}`), so the
|
|
|
|
|
|
identity holds transitively today; the code is already correct for the day we
|
|
|
|
|
|
carry a claim in context.
|
|
|
|
|
|
|
|
|
|
|
|
Still open and still ours: the T04 consumer-shape question flex-auth has asked
|
|
|
|
|
|
twice — operator CLI reaching an in-cluster pin, a workload we do not have yet,
|
|
|
|
|
|
or no pin at all. It is a deployment-shape answer, not engine work, and it is
|
|
|
|
|
|
the reply owed on that thread.
|
|
|
|
|
|
|
2026-09-06 14:56:21 +02:00
|
|
|
|
Remaining to go live is configuration and deployment, not engine work:
|
|
|
|
|
|
`SECRETS_ENGINE_PDP_URL`/`_PDP_TOKEN_FILE` (awaiting the `flex-auth-secrets-engine`
|
|
|
|
|
|
pin, `FLEX-WP-0021-T04`/`T05`), `SECRETS_ENGINE_APPROVAL_URL`/`_TOKEN_FILE`
|
|
|
|
|
|
(awaiting `APPROVAL-WP-0002-T03`), the policy pin (published but not to be set
|
|
|
|
|
|
until T05), a KeyCape RS256 credential in place of the static Bearer token, and
|
feat: bind the destroy gate to approval_binding_digest and pdp_path
The vocabulary mapping this path was waiting on is not coming: gate-house
rejected it in GH-DEC-2026-008, because a translation can be confidently
wrong and fails open by accepting a claim approved for a different action.
The stronger option arrived instead, and both halves are enforced here.
flex-auth published binding.approval_binding_digest (FLEX-DEC-2026-007) to
fix the circularity this repo reported: a pdp_digest recorded at issue time
can never equal the request_digest of the request that carries the claim in
its hashed context, so with GH-DEC-2026-008 requiring that equality, destroy
would have failed closed forever on a check no correct record could pass.
- authorization.approval_binding_digest implements the published exclusion
rule, including Go's context,omitempty behaviour when stripping empties
the context; digest_material drops an empty context for the same reason.
- validate_decision_envelope recomputes the field rather than trusting it,
refuses a claim-bearing request whose decision records none, and compares
the claim's digest from step 1 against it -- never against request_digest,
which still covers the claim so it stays a sound replay identity.
- validate_approval_claim requires binding.pdp_path true before using
pdp_digest at all. Path intent is never inferred from a digest that
happens to be present; pre-schema-v3 approvals carry pdp_path false
regardless of any digest they hold.
Replay fixtures re-vendored from dd3ce4c. The destroy pins moved a second
and final time; approval_binding_digest did not, which is the point. The
fixture now demonstrates the property instead of asserting it: we rederive
fa07becf... from its own request through our canonical implementation,
proving we hash the same material flex-auth does rather than pinning a
constant we cannot reproduce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tNMAYcSQmZWUE4wqP4ij
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715726@bnt-lap001
Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-06 20:39:59 +02:00
|
|
|
|
`approval.authorization_id` on each lane. Destroy's additional requirement is
|
|
|
|
|
|
now met in code: the `pdp_path` + `approval_binding_digest` identity replaced the
|
|
|
|
|
|
vocabulary mapping that was never going to exist.
|
2026-09-06 14:56:21 +02:00
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Define and enforce the decision contract needed by production commands. A
|
|
|
|
|
|
resolved approval must bind at least:
|
|
|
|
|
|
|
|
|
|
|
|
- canonical decision UUID and non-superseded status;
|
|
|
|
|
|
- catalog id and stage;
|
|
|
|
|
|
- action (`apply`, `provision`, `verify`, `exec`, `handoff`, `suspend`,
|
|
|
|
|
|
`deactivate`, `destroy`, or later `rotate`);
|
|
|
|
|
|
- exact field set or policy/auth targets where applicable;
|
|
|
|
|
|
- requesting actor/purpose and expiry or bounded execution window;
|
|
|
|
|
|
- required approval count and distinct approvers for `dual-control`.
|
|
|
|
|
|
|
|
|
|
|
|
Coordinate the external object shape with State Hub/flex-auth rather than
|
|
|
|
|
|
inventing authorization ownership in this repo. Keep local fixtures for
|
|
|
|
|
|
throwaway demos and non-production tests only; require an explicit unsafe-demo
|
|
|
|
|
|
switch if they remain reachable at all.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- Every privileged production handler passes an action-specific request into
|
|
|
|
|
|
one shared authorization function.
|
|
|
|
|
|
- Tests reject wrong lane, stage, action, field set, actor/purpose, expiry,
|
|
|
|
|
|
superseded status, insufficient approvals, duplicate approvers, and local
|
|
|
|
|
|
production fixtures before any OpenBao call.
|
|
|
|
|
|
- `approval.model: dual-control` has enforced semantics rather than being a
|
|
|
|
|
|
label.
|
|
|
|
|
|
- Catalogs use resolvable canonical decision UUIDs for live actions; legacy CCR
|
|
|
|
|
|
references remain provenance, not executable authorization.
|
|
|
|
|
|
- State Hub outage fails production actions closed while dry-run/catalog
|
|
|
|
|
|
inspection remains usable.
|
|
|
|
|
|
|
|
|
|
|
|
## Make delivery sessions short-lived and explicitly closed
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T05
|
2026-08-29 14:36:38 +02:00
|
|
|
|
status: done
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "36bcd64d-8b27-542d-b8da-ede1a4d3c22f"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-29 14:36:38 +02:00
|
|
|
|
Completed 2026-08-29. Delivery AppRole sessions self-revoke in `finally` on
|
|
|
|
|
|
exec and both verification kinds. Bootstrap is a named provider with no
|
|
|
|
|
|
implicit fallback from the KeyCape scaffold. Wiring that scaffold as OpenBao
|
|
|
|
|
|
JWT login is `SECRETS-WP-0008-T06` / hardening H1, blocked on the
|
|
|
|
|
|
platform-owned role — not remaining T05 engine work.
|
|
|
|
|
|
|
2026-08-23 12:05:58 +02:00
|
|
|
|
Progress 2026-08-23. AppRole login material now travels through strict temporary
|
|
|
|
|
|
JSON input rather than argv. Exec and both verification kinds use a scoped
|
|
|
|
|
|
session that self-revokes in `finally`, drops the in-memory token even when
|
|
|
|
|
|
cleanup fails, and exposes only an accessor fingerprint plus cleanup booleans as
|
|
|
|
|
|
evidence. Tests cover success and exception cleanup, idempotence, failed revoke
|
|
|
|
|
|
visibility, and value/credential exclusion. Provider review requests were sent
|
|
|
|
|
|
to railiance-platform (`8f910aff-3a94-43c6-8805-eb9276e46fc0`) and key-cape
|
|
|
|
|
|
(`0627ca55-115e-43dc-b2f6-3195be3bc90d`); steady-state engine authentication
|
|
|
|
|
|
remains external-contract work.
|
|
|
|
|
|
|
2026-08-23 14:15:42 +02:00
|
|
|
|
KeyCape has accepted the `secrets-engine-openbao` service contract. An explicit
|
|
|
|
|
|
consumer scaffold now performs HTTPS client-credentials exchange, strict
|
|
|
|
|
|
out-of-repo client-secret input, exact claim/assurance/lifetime preflight,
|
|
|
|
|
|
three-minute renewal classification, and rejection of ID/refresh tokens. It has
|
|
|
|
|
|
no bootstrap/operator/AppRole fallback and is deliberately not wired into the
|
|
|
|
|
|
CLI until railiance-platform materializes the exact OpenBao JWT mount and role.
|
|
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Refactor AppRole login into a scoped session object that retains only the
|
|
|
|
|
|
minimum non-secret lifecycle handle needed to revoke the issued token in a
|
|
|
|
|
|
`finally` path. Apply it consistently to exec, KV verification, auth-capability
|
|
|
|
|
|
verification, and any future file/wrapped delivery.
|
|
|
|
|
|
|
|
|
|
|
|
Define the steady-state OpenBao authentication interface separately from the
|
|
|
|
|
|
delivery AppRole. Bootstrap token files remain an explicit break-glass/setup
|
|
|
|
|
|
provider; add a bounded OIDC/service-auth provider only with railiance-platform
|
|
|
|
|
|
and identity-owner review.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- Success, child non-zero exit, setup failure, verification failure, SIGINT, and
|
|
|
|
|
|
SIGTERM tests all attempt token revocation and clean temporary material.
|
|
|
|
|
|
- Secret IDs remain single-use where configured; tokens retain TTL/use limits
|
|
|
|
|
|
as backstops rather than their only cleanup.
|
|
|
|
|
|
- Evidence records session establishment/revocation outcome using non-secret
|
|
|
|
|
|
handles or request ids only.
|
|
|
|
|
|
- A failed revocation is visible and actionable without printing token or
|
|
|
|
|
|
accessor material.
|
|
|
|
|
|
- Bootstrap-file authentication is clearly separated from the reviewed
|
|
|
|
|
|
steady-state provider and cannot become an implicit fallback.
|
|
|
|
|
|
|
|
|
|
|
|
## Strengthen verification, readiness, and evidence
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T06
|
2026-08-29 14:36:38 +02:00
|
|
|
|
status: done
|
2026-08-23 11:27:10 +02:00
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "431bc91b-439e-595a-8a8a-7d4c82e68322"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-29 14:36:38 +02:00
|
|
|
|
Completed 2026-08-29. Acceptance is met: multi-field readiness, real unrelated
|
|
|
|
|
|
identity for KV denial, privileged attempt/terminal evidence, and `audit`.
|
|
|
|
|
|
Residuals belong elsewhere: exact-action authorization is T04 /
|
|
|
|
|
|
`SECRETS-WP-0008-T02`; OpenBao audit request-id correlation is hardening H5;
|
|
|
|
|
|
unrelated-identity *selection* for production remains an owner/ops concern,
|
|
|
|
|
|
not an engine mint.
|
|
|
|
|
|
|
2026-08-23 12:05:58 +02:00
|
|
|
|
Progress 2026-08-23. KV verification now checks every declared field by default,
|
|
|
|
|
|
with one path-level negative probe, and route readiness reads once and requires
|
|
|
|
|
|
all declared fields. Missing readiness evidence contains field names only. Tests
|
2026-08-23 12:33:38 +02:00
|
|
|
|
prove a missing sibling makes readiness false. Evidence delivery attempts now
|
|
|
|
|
|
receive append-only local success/failure receipts, and `audit` produces an
|
|
|
|
|
|
allowlisted per-lane summary of actions, results, decisions, session cleanup,
|
|
|
|
|
|
and delivery outcomes without echoing arbitrary detail. The real
|
|
|
|
|
|
unrelated-identity input is now enforced as a strict mode-0600 out-of-repo token
|
|
|
|
|
|
file; absence fails closed, and throwaway OpenBao integration deliberately adds
|
|
|
|
|
|
an overlapping unrelated read policy and proves the check fails. Production
|
|
|
|
|
|
identity selection/ownership, audit request correlation, queued/replayed State
|
2026-08-23 12:58:12 +02:00
|
|
|
|
Hub delivery, and exact-action authorization remain outstanding. All live
|
|
|
|
|
|
privileged handlers now share one attempt/terminal evidence guard: approval
|
|
|
|
|
|
rejection, backend/input exceptions, interruption, verification failure, and
|
|
|
|
|
|
success are recorded without exception prose. Tests prove decision and backend
|
|
|
|
|
|
failures stop before inappropriate backend work and exclude fake secret text.
|
2026-08-23 12:33:38 +02:00
|
|
|
|
|
2026-08-23 14:15:42 +02:00
|
|
|
|
State Hub evidence posts now carry stable idempotency, source-agent, and repo
|
|
|
|
|
|
headers. HTTP 202 edge-relay receipts are classified as `queued` and retain only
|
|
|
|
|
|
their non-secret outbox id in the companion evidence record and lane audit.
|
|
|
|
|
|
Outbox replay remains owned by State Hub/operator tooling.
|
|
|
|
|
|
|
|
|
|
|
|
The complete repository suite is re-run at each implementation slice, including
|
|
|
|
|
|
throwaway OpenBao integration coverage.
|
2026-08-23 12:05:58 +02:00
|
|
|
|
|
2026-08-23 11:27:10 +02:00
|
|
|
|
Make verification and routing truthful for multi-field and high-risk lanes:
|
|
|
|
|
|
|
|
|
|
|
|
- verify every declared field unless the exact approved subset is explicit;
|
|
|
|
|
|
- make route readiness require every required field, not only the first;
|
|
|
|
|
|
- replace the garbage-token negative check with a reviewed real unrelated
|
|
|
|
|
|
identity/token or a capabilities-based denial proof that can detect policy
|
|
|
|
|
|
overlap;
|
|
|
|
|
|
- record attempt, success/failure, cleanup, and authorization outcome for every
|
|
|
|
|
|
privileged command;
|
|
|
|
|
|
- correlate engine evidence with OpenBao audit request ids where available;
|
|
|
|
|
|
- provide a non-secret lane audit summary over local/State Hub evidence.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance:
|
|
|
|
|
|
|
|
|
|
|
|
- Removing any required sibling field makes readiness false and identifies only
|
|
|
|
|
|
the missing field name, never values.
|
|
|
|
|
|
- Negative verification fails when a deliberately overlapping unrelated policy
|
|
|
|
|
|
is introduced in throwaway OpenBao.
|
|
|
|
|
|
- Evidence tests cover backend exceptions, child failures, State Hub delivery
|
|
|
|
|
|
failures, and cleanup failures without leaking fake secret patterns.
|
|
|
|
|
|
- An audit/report command summarizes decisions, attempts, outcomes, and cleanup
|
|
|
|
|
|
for a lane without becoming an authority or replacing OpenBao audit logs.
|
|
|
|
|
|
- The full test suite and throwaway OpenBao integration suite pass.
|
|
|
|
|
|
|
|
|
|
|
|
## Resume native production lane adoption
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: SECRETS-WP-0007-T07
|
|
|
|
|
|
status: wait
|
|
|
|
|
|
priority: high
|
2026-08-25 17:51:13 +02:00
|
|
|
|
state_hub_task_id: "a0a1dd92-d703-5a95-b488-d895f34d5cf7"
|
2026-08-23 11:27:10 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-29 14:36:38 +02:00
|
|
|
|
Dependency: T01–T03, T05, T06 are done. T04 still waits on the durable
|
|
|
|
|
|
ActionAuthorization serving path. Also requires explicit per-lane approvals
|
|
|
|
|
|
and scoped production authority from railiance-platform.
|
2026-08-23 11:27:10 +02:00
|
|
|
|
|
|
|
|
|
|
Resume `SECRETS-WP-0006-T05` lane by lane. Apply only the exact native policy and
|
|
|
|
|
|
bounded AppRole, verify every field plus unrelated-consumer denial, confirm the
|
|
|
|
|
|
existing workload delivery remains healthy, exercise only an approved
|
|
|
|
|
|
value-safe child command, and record rollback/session-cleanup evidence.
|
|
|
|
|
|
|
|
|
|
|
|
After a lane passes, complete `SECRETS-WP-0006-T06` with ops-warden. Retire its
|
|
|
|
|
|
interim proxy only after both parties agree that native readiness evidence is
|
|
|
|
|
|
complete. Do not batch-retire all five lanes.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance per lane:
|
|
|
|
|
|
|
|
|
|
|
|
- Canonical action-bound approval is resolved and linked.
|
|
|
|
|
|
- Policy/AppRole server readback matches the reviewed exact-path plan.
|
|
|
|
|
|
- All declared fields pass presence/access checks; a real unrelated identity is
|
|
|
|
|
|
denied; no value enters evidence.
|
|
|
|
|
|
- Issued delivery sessions are explicitly revoked and cleanup is evidenced.
|
|
|
|
|
|
- Existing ESO/OIDC delivery and application health remain good according to
|
|
|
|
|
|
their owners.
|
|
|
|
|
|
- Rollback is rehearsed or capabilities-safe verified before proxy retirement.
|
|
|
|
|
|
- ops-warden routing reports native readiness and removes only that lane's
|
|
|
|
|
|
interim proxy.
|
|
|
|
|
|
|
|
|
|
|
|
## Dependencies and coordination
|
|
|
|
|
|
|
|
|
|
|
|
- `SECRETS-WP-0006` owns the five-lane catalog admission and live cutover.
|
|
|
|
|
|
- railiance-platform owns production OpenBao authority, custody, and backend
|
|
|
|
|
|
audit.
|
|
|
|
|
|
- State Hub/flex-auth own the canonical decision/authorization contract.
|
|
|
|
|
|
- key-cape/user-engine own human/service identity and claims.
|
|
|
|
|
|
- Workload owners own ESO/Kubernetes delivery and provider/application health.
|
|
|
|
|
|
- ops-warden owns credential routing and interim proxy disposition.
|
|
|
|
|
|
|
|
|
|
|
|
Any external contract change must be recorded as non-secret evidence and must
|
|
|
|
|
|
not transfer custody or authorization ownership into this repository.
|
|
|
|
|
|
|
|
|
|
|
|
## Exit criteria
|
|
|
|
|
|
|
|
|
|
|
|
- Current unsafe multi-field provisioning and misleading revoke behavior are
|
|
|
|
|
|
contained and replaced.
|
|
|
|
|
|
- Raw values cannot enter subprocess argv or evidence through provisioning.
|
|
|
|
|
|
- Production approvals are canonical, exact-action, expiring, and genuinely
|
|
|
|
|
|
dual-control-capable.
|
|
|
|
|
|
- Delivery tokens are explicitly revoked on every exit path.
|
|
|
|
|
|
- Readiness and verification cover all declared fields and meaningful unrelated
|
|
|
|
|
|
identities.
|
|
|
|
|
|
- Lifecycle operations distinguish reversible access suspension from irreversible
|
|
|
|
|
|
custody destruction.
|
|
|
|
|
|
- At least one of the five admitted production lanes completes the hardened
|
|
|
|
|
|
native verification/cutover path; remaining lanes have explicit external
|
|
|
|
|
|
blockers rather than engine-safety blockers.
|
|
|
|
|
|
- Service-mode design remains deferred until these guarantees are demonstrated.
|