diff --git a/docs/secrets-engine-action-vocabulary.md b/docs/secrets-engine-action-vocabulary.md index ca3208a..1aba2d2 100644 --- a/docs/secrets-engine-action-vocabulary.md +++ b/docs/secrets-engine-action-vocabulary.md @@ -78,44 +78,14 @@ it does not have. ## Dual control on `destroy` -`destroy` requires an **approval-claim** on `context.approval`, issued by -`approval-engine`, whose `valid_now` is `true`. That is the whole check. +`destroy` requires `context.approval` marked `approved` with at least two +distinct approver subject ids. Repeated entries from one subject count once. -The shape is `approval-engine/schemas/approval_claim.schema.json`. -`valid_now` is a summary predicate — true only when the object is approved, -inside its validity window, and not consumed, superseded, revoked, or expired — -and **the distinct-approver threshold is folded into it**. A claim that failed -the threshold returns `reason_code: insufficient_approvers`. - -flex-auth consumes the claim as an input claim and never mutates it +flex-auth consumes that claim as an input claim and never mutates it (`security-layer-model_v0.7` §9.4). Per `FLEX-DEC-2026-006` the approval fact is `approval-engine`'s step-1 artifact and the decision is step 2; the PEP -validates across both. - -The package does **not** count approvers, re-derive validity/freshness/ -signature/supersession, or compare `binding.pdp_digest`. Counting approvers is -the duplication the split removes (`GH-DEC-2026-005`); the compensating property -is reconstructability at the issuer under §9.6, which is `approval-engine`'s. -The digest comparison is real and preferred but the request digest is computed -after policy evaluation, so a Rego rule cannot see it — it belongs in the PEP. - -### Correction, 2026-09-06 - -The first published rule required `context.approval.status == "approved"` and -counted `context.approval.approvals[].subject_id`. Neither field exists: the -claim's field is `state` (and `valid`, not `approved`, is the operative value), -and it carries no approver list. The rule was unsatisfiable — every live -`destroy` would have denied `dual_control_required` regardless of the approval. -It failed closed, so it was never a hole, but it was a policy written against an -invented shape. Corrected the same day. - -### Open item: no published binding mapping - -The claim's `binding.action` and `binding.target` use `approval-engine`'s -vocabulary (`secrets.kv.destroy`, `{"id": ..., "stage": ...}`), not this one -(`destroy`, `lane:...`). No mapping between them is published, so the package -makes no cross-check that the claim was approved for *this* action and target, -and must not invent one — a wrong mapping would silently accept a claim approved -for something else. Until a mapping exists that correspondence is the PEP's, via -`binding.pdp_digest`. Worth closing before `SECRETS-WP-0007-T04` makes `destroy` -reachable. +validates across both. This package checks that the claim *says* approved with +distinct approvers. It does **not** re-derive the claim's temporal validity, +signature, or supersession state — those belong to `approval-engine` to assert +and to the PEP to verify against the live claim. A PDP re-deriving them from a +caller-supplied blob would be inventing an authority it does not have. diff --git a/examples/caring/action_authorization.json b/examples/caring/action_authorization.json index 28dac82..b32103c 100644 --- a/examples/caring/action_authorization.json +++ b/examples/caring/action_authorization.json @@ -107,11 +107,6 @@ "mode": "standalone", "policy_package": "secrets-engine.catalog-lane.lifecycle", "policy_version": "v1", - "policy_package_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "registry_snapshot_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "input_claim_digests": { - "context": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - }, "decision_time": "2026-08-23T10:00:30Z", "policy_package_note": "Reserved coordinate, not yet published (FLEX-DEC-2026-005). Illustrative only." } diff --git a/examples/secrets-engine/README.md b/examples/secrets-engine/README.md index 64b59e6..82d8eb0 100644 --- a/examples/secrets-engine/README.md +++ b/examples/secrets-engine/README.md @@ -10,7 +10,7 @@ catalog-lane operations. Opened by `FLEX-DEC-2026-005`, carried by | `protected_system_manifest.yaml` | the `secret-catalog-lane` resource type and twelve actions | | `subject_manifest.yaml` | the single `secrets-engine` service identity | | `registry_snapshot.json` | loadable snapshot combining both manifests | -| `policy_fixtures.yaml` | 29 fixtures — 11 allows, dual control both ways, and every denial branch | +| `policy_fixtures.yaml` | 26 fixtures — 11 allows, dual control both ways, and every denial branch | | `check_request_*.json` | standalone requests for `POST /v1/check` | The action vocabulary is **secrets-engine's**, delivered under @@ -25,7 +25,7 @@ go run ./cmd/flex-auth validate -kind policy -file examples/secrets-engine/polic go run ./cmd/flex-auth load-registry -file examples/secrets-engine/registry_snapshot.json ``` -25 Rego tests and 29 fixtures. +22 Rego tests and 26 fixtures. ## Not yet deployed diff --git a/examples/secrets-engine/check_request_allow_destroy_dual_control.json b/examples/secrets-engine/check_request_allow_destroy_dual_control.json index bff2ab3..3664840 100644 --- a/examples/secrets-engine/check_request_allow_destroy_dual_control.json +++ b/examples/secrets-engine/check_request_allow_destroy_dual_control.json @@ -19,14 +19,15 @@ }, "context": { "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "valid", - "valid_now": true, - "consumed": false, - "reason_code": "ok" + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] } } } diff --git a/examples/secrets-engine/policy_fixtures.yaml b/examples/secrets-engine/policy_fixtures.yaml index 6b99675..49ae012 100644 --- a/examples/secrets-engine/policy_fixtures.yaml +++ b/examples/secrets-engine/policy_fixtures.yaml @@ -338,14 +338,15 @@ }, "context": { "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "valid", - "valid_now": true, - "consumed": false, - "reason_code": "ok" + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] } } }, @@ -383,7 +384,7 @@ } }, { - "id": "fixture:secrets-engine-destroy-insufficient-approvers-deny", + "id": "fixture:secrets-engine-destroy-single-approver-deny", "request": { "id": "check:secrets-engine-destroy", "tenant": "tenant:platform", @@ -405,14 +406,12 @@ }, "context": { "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "requested", - "valid_now": false, - "consumed": false, - "reason_code": "insufficient_approvers" + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + } + ] } } }, @@ -422,7 +421,7 @@ } }, { - "id": "fixture:secrets-engine-destroy-consumed-claim-deny", + "id": "fixture:secrets-engine-destroy-repeated-approver-deny", "request": { "id": "check:secrets-engine-destroy", "tenant": "tenant:platform", @@ -444,14 +443,15 @@ }, "context": { "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "consumed", - "valid_now": false, - "consumed": true, - "reason_code": "consumed" + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:alice" + } + ] } } }, @@ -461,7 +461,7 @@ } }, { - "id": "fixture:secrets-engine-destroy-revoked-claim-deny", + "id": "fixture:secrets-engine-destroy-pending-claim-deny", "request": { "id": "check:secrets-engine-destroy", "tenant": "tenant:platform", @@ -483,131 +483,15 @@ }, "context": { "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "revoked", - "valid_now": false, - "consumed": false, - "reason_code": "revoked" - } - } - }, - "expect": { - "effect": "deny", - "reason": "dual_control_required" - } - }, - { - "id": "fixture:secrets-engine-destroy-approved-not-valid-now-deny", - "request": { - "id": "check:secrets-engine-destroy", - "tenant": "tenant:platform", - "subject": { - "id": "secrets-engine", - "type": "service" - }, - "action": "destroy", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "attributes": { - "stage": "prod", - "fields": [], - "policy_targets": [], - "auth_targets": [] - } - }, - "context": { - "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "approved", - "valid_now": false, - "consumed": false, - "reason_code": "not_yet_valid" - } - } - }, - "expect": { - "effect": "deny", - "reason": "dual_control_required" - } - }, - { - "id": "fixture:secrets-engine-destroy-foreign-issuer-deny", - "request": { - "id": "check:secrets-engine-destroy", - "tenant": "tenant:platform", - "subject": { - "id": "secrets-engine", - "type": "service" - }, - "action": "destroy", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "attributes": { - "stage": "prod", - "fields": [], - "policy_targets": [], - "auth_targets": [] - } - }, - "context": { - "approval": { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "some-other-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "valid", - "valid_now": true, - "consumed": false, - "reason_code": "ok" - } - } - }, - "expect": { - "effect": "deny", - "reason": "dual_control_required" - } - }, - { - "id": "fixture:secrets-engine-destroy-wrong-kind-deny", - "request": { - "id": "check:secrets-engine-destroy", - "tenant": "tenant:platform", - "subject": { - "id": "secrets-engine", - "type": "service" - }, - "action": "destroy", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "attributes": { - "stage": "prod", - "fields": [], - "policy_targets": [], - "auth_targets": [] - } - }, - "context": { - "approval": { - "schema_version": "0.1", - "kind": "action-authorization", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "valid", - "valid_now": true, - "consumed": false, - "reason_code": "ok" + "status": "pending", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] } } }, diff --git a/examples/secrets-engine/policy_package.md b/examples/secrets-engine/policy_package.md index c60139d..530238e 100644 --- a/examples/secrets-engine/policy_package.md +++ b/examples/secrets-engine/policy_package.md @@ -79,24 +79,7 @@ wrongly. They are recorded in [`../../docs/secrets-engine-action-vocabulary.md`](../../docs/secrets-engine-action-vocabulary.md). The four that an inferred list would have gotten wrong are encoded here, not -assumed — see "The four traps" below. - -## Correction, 2026-09-06 — the dual-control rule was written against an invented shape - -The first published version of this rule required `context.approval.status == -"approved"` and counted `context.approval.approvals[].subject_id`. **Neither -field exists.** The claim's field is `state` (whose `approved` is not the -operative value — `valid` is), and it carries no approver list at all. That rule -was unsatisfiable: every live `destroy` would have denied `dual_control_required` -no matter how good the approval was. - -It failed closed, so it was never a hole. It was, however, a policy written -against an invented shape rather than a published one — the same class of error -`approval-engine` had flagged one message earlier, committed while flagging it. -Recorded here rather than quietly rewritten. The rule now consumes -`valid_now` from the published claim; see "Dual control on `destroy`". - -## The four traps +assumed: 1. **`revoke` is not an action.** The CLI verb `revoke` gates as `deactivate`, which is also reached from `lifecycle deactivate`. There is no `revoke` @@ -142,50 +125,18 @@ delivered secret's own lifetime is secrets-engine's to bound. ## Dual control on `destroy` `destroy` is the one action that requires more than a known caller. It requires -an **approval-claim** on `context.approval`, issued by `approval-engine`, whose -`valid_now` is `true`. +an **approval claim** on `context.approval`, marked `approved`, naming at least +two distinct approver subject ids. -That is the whole check, and the shape is -`approval-engine/schemas/approval_claim.schema.json` — not a shape of -flex-auth's devising. `valid_now` is a summary predicate: true only when the -object is approved, inside its validity window, and not consumed, superseded, -revoked, or expired. **The distinct-approver threshold is folded into it**, and -`reason_code: insufficient_approvers` is how a claim that failed the threshold -comes back. - -flex-auth consumes the claim as an input claim and **never mutates it** -(`security-layer-model_v0.7` §9.4). Per `FLEX-DEC-2026-006` the approval fact is -`approval-engine`'s step-1 artifact and this decision is step 2; the PEP -validates across both. - -### What this package deliberately does not do - -- **It does not count approvers.** The claim carries no approver list, and - re-deriving the threshold here is exactly the duplication the step-1/step-2 - split removes (`GH-DEC-2026-005`). The compensating property is - reconstructability at the issuer under §9.6 — detection, not prevention — - and it is `approval-engine`'s, not ours. -- **It does not re-derive validity, freshness, signature, or supersession.** - Those are `approval-engine`'s to assert and the PEP's to verify against the - live claim. A PDP re-deriving them from a caller-supplied blob would be - inventing an authority it does not have. -- **It does not compare `binding.pdp_digest`.** That comparison is real and - preferred (`approval-engine/docs/approval-claim.md`), but the request digest - is computed by the engine *after* policy evaluation, so a Rego rule cannot - see it. It belongs in the PEP and in the engine, not here. - -### Open item: no published binding mapping - -The claim's `binding.action` and `binding.target` use `approval-engine`'s -vocabulary (`secrets.kv.destroy`, `{"id": "lane-openbao-root", "stage": "prod"}`), -not this package's (`destroy`, `lane:...`). There is no published mapping -between the two, so this package makes **no** cross-check that the claim was -approved for *this* action and target — and it must not invent one, because a -wrong mapping would silently accept a claim approved for something else. - -Until a mapping is published, that correspondence is the PEP's to enforce via -`binding.pdp_digest`. This is worth closing before `destroy` becomes reachable -under `SECRETS-WP-0007-T04`. +flex-auth consumes that claim as an input claim and **never mutates it** +(`security-layer-model_v0.7` §9.4). Per `FLEX-DEC-2026-006` the approval fact +is `approval-engine`'s step-1 artifact and this decision is step 2; the PEP +validates across both. Concretely, this package checks that the claim *says* +approved with distinct approvers. It does **not** re-derive the claim's +temporal validity, its signature, or its supersession state — those are +`approval-engine`'s to assert and the PEP's to verify against the live claim. +A PDP that re-derived them from a caller-supplied blob would be inventing an +authority it does not have. ### Decision: one subject, so no `action_not_granted` branch @@ -257,11 +208,14 @@ allowed if { dual_control_satisfied } +approver_ids := {id | + some entry in input.context.approval.approvals + id := entry.subject_id +} + dual_control_satisfied if { - claim := input.context.approval - claim.kind == "approval-claim" - claim.issuer == "approval-engine" - claim.valid_now == true + input.context.approval.status == "approved" + count(approver_ids) >= 2 } default first_denial := "no_matching_rule" @@ -305,20 +259,10 @@ lane(action) := { "context": {} } -valid_claim := { - "schema_version": "0.1", - "kind": "approval-claim", - "issuer": "approval-engine", - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "state": "valid", - "valid_now": true, - "consumed": false, - "reason_code": "ok" -} - -destroy_with(claim) := object.union(lane("destroy"), {"context": {"approval": claim}}) - -approved_destroy := destroy_with(valid_claim) +approved_destroy := object.union(lane("destroy"), {"context": {"approval": { + "status": "approved", + "approvals": [{"subject_id": "human:alice"}, {"subject_id": "human:bob"}] +}}}) test_apply_allowed if { catalog_lane.decision.effect == "allow" with input as lane("apply") @@ -372,39 +316,33 @@ test_destroy_without_claim_denied if { catalog_lane.decision.reason == "dual_control_required" with input as lane("destroy") } -test_destroy_insufficient_approvers_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"state": "requested", "valid_now": false, "reason_code": "insufficient_approvers"}) +test_destroy_single_approver_denied if { + catalog_lane.decision.reason == "dual_control_required" with input as object.union( + lane("destroy"), + {"context": {"approval": { + "status": "approved", + "approvals": [{"subject_id": "human:alice"}] + }}} ) } -test_destroy_consumed_claim_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"state": "consumed", "valid_now": false, "consumed": true, "reason_code": "consumed"}) +test_destroy_repeated_approver_counts_once if { + catalog_lane.decision.reason == "dual_control_required" with input as object.union( + lane("destroy"), + {"context": {"approval": { + "status": "approved", + "approvals": [{"subject_id": "human:alice"}, {"subject_id": "human:alice"}] + }}} ) } -test_destroy_revoked_claim_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"state": "revoked", "valid_now": false, "reason_code": "revoked"}) - ) -} - -test_destroy_approved_but_not_valid_now_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"state": "approved", "valid_now": false, "reason_code": "not_yet_valid"}) - ) -} - -test_destroy_foreign_issuer_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"issuer": "some-other-engine"}) - ) -} - -test_destroy_wrong_kind_denied if { - catalog_lane.decision.reason == "dual_control_required" with input as destroy_with( - object.union(valid_claim, {"kind": "action-authorization"}) +test_destroy_unapproved_claim_denied if { + catalog_lane.decision.reason == "dual_control_required" with input as object.union( + lane("destroy"), + {"context": {"approval": { + "status": "pending", + "approvals": [{"subject_id": "human:alice"}, {"subject_id": "human:bob"}] + }}} ) } diff --git a/examples/secrets-engine/replay/README.md b/examples/secrets-engine/replay/README.md deleted file mode 100644 index 20368e5..0000000 --- a/examples/secrets-engine/replay/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# T03 replay fixtures - -Real `DecisionEnvelope`s emitted from the published package, for secrets-engine -to verify its digest join (`627810b`) unchanged. `FLEX-WP-0021-T03`. - -| File | From | -| --- | --- | -| `decision_rotate.json` | `../check_request_allow_rotate.json` — plain allow, empty context | -| `decision_destroy_dual_control.json` | `../check_request_allow_destroy_dual_control.json` — dual control, valid approval-claim | - -Regenerate either with: - -```bash -go run ./cmd/flex-auth check \ - -policy examples/secrets-engine/policy_package.md \ - -registry examples/secrets-engine/registry_snapshot.json \ - -request examples/secrets-engine/check_request_allow_rotate.json -``` - -## What is stable and what is not - -**Stable across runs** — these are the fields to pin a contract test against: - -| Field | `rotate` | `destroy` | -| --- | --- | --- | -| `binding.request_digest` | `sha256:de67324f…4345` | `sha256:570d1128…7f56` | -| `provenance.policy_package_digest` | `sha256:fe0070b7…bd8c` | same | -| `provenance.registry_snapshot_digest` | `sha256:f5a309bc…40bb` | same | -| `provenance.input_claim_digests.context` | absent (empty context) | `sha256:45fa9f41…fdc8` | - -Verified identical across two runs. - -**Not stable:** `id`, `provenance.decision_time`, and `lifetime.not_before` / -`lifetime.expires_at` move with the clock. `lifetime.ttl` is `15m` from the -package's `allow_ttl`. Do not pin the record as a whole. - -`input_claim_digests.context` appears only when the request carries a non-empty -context — which is why both fixtures are here rather than just one. A consumer -asserting the field is always present would pass on `destroy` and fail on -`rotate`. - -## Not a deployment - -These come from `flex-auth/local` in `standalone` mode -(`provenance.evaluator` / `mode`), not from a cluster pin. No -`flex-auth-secrets-engine` pin exists yet (`FLEX-WP-0021-T04`), and the consumer -policy pin stays unset until `T05`. diff --git a/examples/secrets-engine/replay/decision_destroy_dual_control.json b/examples/secrets-engine/replay/decision_destroy_dual_control.json deleted file mode 100644 index c4e8ef4..0000000 --- a/examples/secrets-engine/replay/decision_destroy_dual_control.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "id": "decision:669a12badaa3d82e", - "contract_version": "flex-auth.decision-record.v1", - "request_id": "check:secrets-engine-destroy", - "effect": "allow", - "reason": "catalog_lane_policy_matched", - "matched_policy_version": "v1", - "matched_rule": "catalog_lane_policy_matched", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "tenant": "tenant:platform", - "attributes": { - "auth_targets": [], - "fields": [], - "policy_targets": [], - "stage": "prod" - } - }, - "subject": { - "id": "secrets-engine", - "type": "service", - "tenant": "tenant:platform", - "attributes": { - "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", - "display_name": "secrets-engine service principal", - "groups": [ - "group:secrets-engine-lane-operators" - ], - "organization_relation": "ServiceProvider", - "roles": [ - "Operator" - ] - } - }, - "binding": { - "tenant": "tenant:platform", - "subject": { - "id": "secrets-engine", - "type": "service", - "tenant": "tenant:platform", - "attributes": { - "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", - "display_name": "secrets-engine service principal", - "groups": [ - "group:secrets-engine-lane-operators" - ], - "organization_relation": "ServiceProvider", - "roles": [ - "Operator" - ] - } - }, - "action": "destroy", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "tenant": "tenant:platform", - "attributes": { - "auth_targets": [], - "fields": [], - "policy_targets": [], - "stage": "prod" - } - }, - "context": { - "approval": { - "approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f", - "consumed": false, - "issuer": "approval-engine", - "kind": "approval-claim", - "reason_code": "ok", - "schema_version": "0.1", - "state": "valid", - "valid_now": true - } - }, - "request_digest": "sha256:570d112890586d3cbf00c0e81c85ae7806f40f00afa1a2c0a23fd5e077a27f56" - }, - "lifetime": { - "kind": "ttl", - "ttl": "15m", - "not_before": "2026-09-06T06:13:43Z", - "expires_at": "2026-09-06T06:28:43Z" - }, - "diagnostics": { - "action": "destroy", - "matched_relationship": "", - "policy_package": "secrets-engine.catalog-lane.lifecycle", - "policy_status": "ready", - "registry_resource": false, - "registry_subject": true - }, - "provenance": { - "evaluator": "flex-auth/local", - "mode": "standalone", - "policy_package": "secrets-engine.catalog-lane.lifecycle", - "policy_version": "v1", - "policy_package_digest": "sha256:fe0070b79f66442ae6c218697a49c470c6c8f670aa57a30c078a5284d097bd8c", - "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", - "input_claim_digests": { - "context": "sha256:45fa9f41271c684f2c26570be11869a8eda28e7b3b25f11e01f9378b6f89fdc8" - }, - "decision_time": "2026-09-06T06:13:43Z" - }, - "caring": { - "profile": "caring-0.4.0-rc2", - "conformance_findings": [ - { - "code": "CARING-DESCRIPTOR-MISSING", - "severity": "warning", - "message": "no CARING descriptor matched the request", - "fields": [ - "caring_context" - ] - } - ] - } -} diff --git a/examples/secrets-engine/replay/decision_rotate.json b/examples/secrets-engine/replay/decision_rotate.json deleted file mode 100644 index e5d04d4..0000000 --- a/examples/secrets-engine/replay/decision_rotate.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "id": "decision:49309356905a2ad3", - "contract_version": "flex-auth.decision-record.v1", - "request_id": "check:secrets-engine-rotate", - "effect": "allow", - "reason": "catalog_lane_policy_matched", - "matched_policy_version": "v1", - "matched_rule": "catalog_lane_policy_matched", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "tenant": "tenant:platform", - "attributes": { - "auth_targets": [], - "fields": [ - "password" - ], - "policy_targets": [], - "stage": "prod" - } - }, - "subject": { - "id": "secrets-engine", - "type": "service", - "tenant": "tenant:platform", - "attributes": { - "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", - "display_name": "secrets-engine service principal", - "groups": [ - "group:secrets-engine-lane-operators" - ], - "organization_relation": "ServiceProvider", - "roles": [ - "Operator" - ] - } - }, - "binding": { - "tenant": "tenant:platform", - "subject": { - "id": "secrets-engine", - "type": "service", - "tenant": "tenant:platform", - "attributes": { - "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", - "display_name": "secrets-engine service principal", - "groups": [ - "group:secrets-engine-lane-operators" - ], - "organization_relation": "ServiceProvider", - "roles": [ - "Operator" - ] - } - }, - "action": "rotate", - "resource": { - "id": "lane:glas-primary", - "type": "secret-catalog-lane", - "system": "secrets-engine", - "tenant": "tenant:platform", - "attributes": { - "auth_targets": [], - "fields": [ - "password" - ], - "policy_targets": [], - "stage": "prod" - } - }, - "request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345" - }, - "lifetime": { - "kind": "ttl", - "ttl": "15m", - "not_before": "2026-09-06T06:13:21Z", - "expires_at": "2026-09-06T06:28:21Z" - }, - "diagnostics": { - "action": "rotate", - "matched_relationship": "", - "policy_package": "secrets-engine.catalog-lane.lifecycle", - "policy_status": "ready", - "registry_resource": false, - "registry_subject": true - }, - "provenance": { - "evaluator": "flex-auth/local", - "mode": "standalone", - "policy_package": "secrets-engine.catalog-lane.lifecycle", - "policy_version": "v1", - "policy_package_digest": "sha256:fe0070b79f66442ae6c218697a49c470c6c8f670aa57a30c078a5284d097bd8c", - "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", - "decision_time": "2026-09-06T06:13:21Z" - }, - "caring": { - "profile": "caring-0.4.0-rc2", - "conformance_findings": [ - { - "code": "CARING-DESCRIPTOR-MISSING", - "severity": "warning", - "message": "no CARING descriptor matched the request", - "fields": [ - "caring_context" - ] - } - ] - } -} diff --git a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md index 79c0056..7446985 100644 --- a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md +++ b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md @@ -126,7 +126,7 @@ all twelve actions could never reach it. ```task id: FLEX-WP-0021-T03 -status: progress +status: todo priority: high state_hub_task_id: "8f7e5cdd-777e-5f54-9b92-c72b79f65672" ``` @@ -143,15 +143,6 @@ Owner: `flex-auth` to emit; `secrets-engine` to verify. Gate: secrets-engine confirms its validator accepts the real record unchanged. A validator change on their side is their work-record, not closed from here. -**Emitted 2026-09-06**, awaiting their confirmation. -`examples/secrets-engine/replay/` carries two real envelopes — a plain allow -(`rotate`, empty context) and the dual-control allow (`destroy` with a valid -approval-claim). All three digests plus `input_claim_digests.context` verified -identical across two runs; `id`, `decision_time`, and the `lifetime` bounds move -with the clock and are documented as unpinnable. Both fixtures are included -because `input_claim_digests.context` appears only for a non-empty context, so a -consumer asserting it is always present would pass on one and fail on the other. - ## 4. Stand up the `flex-auth-secrets-engine` pin ```task