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
767 lines
42 KiB
Markdown
767 lines
42 KiB
Markdown
---
|
||
id: SECRETS-WP-0007
|
||
type: workplan
|
||
title: "Production-safe provisioning, authorization, and lifecycle hardening"
|
||
domain: infotech
|
||
repo: secrets-engine
|
||
status: active
|
||
owner: codex
|
||
topic_slug: custodian
|
||
created: "2026-08-23"
|
||
updated: "2026-09-06"
|
||
state_hub_workstream_id: "68a39be1-bd9c-5133-ad64-e7bca892aaf3"
|
||
---
|
||
|
||
# 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
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "5ddba36d-bc86-5670-baf1-50c872e6577e"
|
||
```
|
||
|
||
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.
|
||
|
||
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
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "ddf3ed54-9cbc-5e18-b15a-5da14cda5d9b"
|
||
```
|
||
|
||
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.
|
||
|
||
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
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "bb6073e1-c952-5357-b707-5574cef50f2f"
|
||
```
|
||
|
||
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.
|
||
|
||
Residual from this task, now implemented: `secrets-engine session revoke`
|
||
`--accessor-file` or `--lease-id-file` (hardening H4a).
|
||
|
||
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
|
||
self-revocation is implemented in T05; a general known-accessor operator
|
||
command is `session revoke --accessor-file` (H4a).
|
||
|
||
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
|
||
status: wait
|
||
priority: high
|
||
state_hub_task_id: "4b58edec-c705-55e5-9ece-362e1ff13079"
|
||
```
|
||
|
||
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.
|
||
|
||
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`.
|
||
|
||
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
|
||
authenticated approval storage are still outstanding. flex-auth corrected its
|
||
example digest and added a complete binding regression assertion in `d402408`.
|
||
|
||
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/`.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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
|
||
`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.
|
||
|
||
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
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "36bcd64d-8b27-542d-b8da-ede1a4d3c22f"
|
||
```
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "431bc91b-439e-595a-8a8a-7d4c82e68322"
|
||
```
|
||
|
||
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.
|
||
|
||
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
|
||
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
|
||
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.
|
||
|
||
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.
|
||
|
||
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
|
||
state_hub_task_id: "a0a1dd92-d703-5a95-b488-d895f34d5cf7"
|
||
```
|
||
|
||
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.
|
||
|
||
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.
|