diff --git a/docs/secrets-engine-action-vocabulary.md b/docs/secrets-engine-action-vocabulary.md new file mode 100644 index 0000000..1aba2d2 --- /dev/null +++ b/docs/secrets-engine-action-vocabulary.md @@ -0,0 +1,91 @@ +# secrets-engine action vocabulary + +Status: published +Date: 2026-09-06 +Workplan: `FLEX-WP-0021-T01` +Source: secrets-engine `docs/gated-actions.md`, read out of their `cli.py` +Package: `secrets-engine.catalog-lane.lifecycle` v1 +(`examples/secrets-engine/policy_package.md`) + +`FLEX-WP-0021-T01` required secrets-engine to **name** this list rather than +letting flex-auth derive it, and made an inferred action a blocker rather than +a default. That was not ceremony: four of the twelve would have been inferred +wrongly, and one action that looks obvious does not exist at all. + +The example vocabulary `secrets-engine.lifecycle/v1` this replaces is **not** +this list and was never published (`FLEX-DEC-2026-005`). + +## Request shape + +| Field | Value | +| --- | --- | +| `resource.type` | `secret-catalog-lane` | +| `resource.system` | `secrets-engine` | +| `resource.id` | the catalog id | +| `resource.attributes.stage` | deployment stage | +| `resource.attributes.fields` | sorted; populated for `provision`, `rotate`, `verify`, `exec`; empty list otherwise | +| `resource.attributes.policy_targets` / `auth_targets` | sorted | +| `context.approval` | approval claim; required for `destroy` only | + +Empty lists are sent rather than omitted or guessed. + +## The twelve actions + +| Action | Reaches OpenBao | Notes | +| --- | --- | --- | +| `apply` | yes | `apply --dry-run` does not reach the gate | +| `provision` | yes | carries `fields` | +| `rotate` | yes | carries `fields` | +| `verify` | yes | carries `fields` | +| `handoff` | yes | | +| `wrap` | yes | | +| `exec` | yes | carries `fields` | +| `deactivate` | yes | also the gate for the CLI verb `revoke` | +| `suspend` | yes | | +| `destroy` | yes | dual control; not reachable live yet | +| `compromise` | no | local delivery overlay only | +| `reactivate` | no | local delivery overlay only | + +## Four things an inferred list gets wrong + +**1. `revoke` is not an action.** The CLI verb `revoke` gates as `deactivate`, +which is also reached from `lifecycle deactivate`. There is no `revoke` value +and one must not be added. The package asserts this +(`test_revoke_is_not_an_action` → `unknown_action`). + +**2. `destroy` is defined but not reachable live.** Its handler raises before +the gate, so only `--dry-run` renders. It stays in the vocabulary as the +dual-control case; no live `destroy` Check arrives until +`SECRETS-WP-0007-T04` lands. Keeping it in the package now means the rule is +reviewed and fixtured *before* the path opens rather than in the same change +that opens it. + +**3. `compromise` and `reactivate` touch no OpenBao object.** They mutate local +delivery-overlay state only. They are gated because they change delivery +posture, not because they write to the backend — a distinction worth recording, +because a reviewer looking for backend writes would otherwise read their +presence as a mistake. + +**4. Seven CLI surfaces never reach the gate** and must not appear in the +package: `plan`, `apply --dry-run`, `route`, `audit`, `catalog`, +`decision inspect`, and `evidence`. + +`apply --dry-run` is the one flex-auth cannot enforce. `apply` is an action and +`apply --dry-run` is not, but both would arrive as `apply` — the distinction is +invisible to the PDP. What keeps them apart is that the PEP does not call the +gate for a dry run. flex-auth records the limit rather than implying a control +it does not have. + +## Dual control on `destroy` + +`destroy` requires `context.approval` marked `approved` with at least two +distinct approver subject ids. Repeated entries from one subject count once. + +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. 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/secrets-engine/README.md b/examples/secrets-engine/README.md new file mode 100644 index 0000000..82d8eb0 --- /dev/null +++ b/examples/secrets-engine/README.md @@ -0,0 +1,36 @@ +# secrets-engine example + +Policy package, manifests, and fixtures for `secrets-engine`'s gated +catalog-lane operations. Opened by `FLEX-DEC-2026-005`, carried by +`FLEX-WP-0021`. + +| File | What it is | +| --- | --- | +| `policy_package.md` | `secrets-engine.catalog-lane.lifecycle` v1, `allow_ttl: 15m` | +| `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` | 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 +`FLEX-WP-0021-T01` and recorded in +[`../../docs/secrets-engine-action-vocabulary.md`](../../docs/secrets-engine-action-vocabulary.md). +Read that before changing any action string here. + +## Verify + +```bash +go run ./cmd/flex-auth validate -kind policy -file examples/secrets-engine/policy_package.md +go run ./cmd/flex-auth load-registry -file examples/secrets-engine/registry_snapshot.json +``` + +22 Rego tests and 26 fixtures. + +## Not yet deployed + +There is no `flex-auth-secrets-engine` pin yet (`FLEX-WP-0021-T04`), so +secrets-engine has no address to call. Their policy pin +(`SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION`) stays **unset and +fail-closed** until `FLEX-WP-0021-T05` hands them the published package and the +Service DNS. Do not configure it from this directory. diff --git a/examples/secrets-engine/check_request_allow_destroy_dual_control.json b/examples/secrets-engine/check_request_allow_destroy_dual_control.json new file mode 100644 index 0000000..3664840 --- /dev/null +++ b/examples/secrets-engine/check_request_allow_destroy_dual_control.json @@ -0,0 +1,33 @@ +{ + "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": { + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] + } + } +} diff --git a/examples/secrets-engine/check_request_allow_rotate.json b/examples/secrets-engine/check_request_allow_rotate.json new file mode 100644 index 0000000..7b859f0 --- /dev/null +++ b/examples/secrets-engine/check_request_allow_rotate.json @@ -0,0 +1,23 @@ +{ + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} +} diff --git a/examples/secrets-engine/check_request_deny_destroy_without_claim.json b/examples/secrets-engine/check_request_deny_destroy_without_claim.json new file mode 100644 index 0000000..8963aae --- /dev/null +++ b/examples/secrets-engine/check_request_deny_destroy_without_claim.json @@ -0,0 +1,21 @@ +{ + "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": {} +} diff --git a/examples/secrets-engine/check_request_deny_revoke_not_an_action.json b/examples/secrets-engine/check_request_deny_revoke_not_an_action.json new file mode 100644 index 0000000..c7068e0 --- /dev/null +++ b/examples/secrets-engine/check_request_deny_revoke_not_an_action.json @@ -0,0 +1,21 @@ +{ + "id": "check:secrets-engine-revoke", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "revoke", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} +} diff --git a/examples/secrets-engine/check_request_deny_unknown_subject.json b/examples/secrets-engine/check_request_deny_unknown_subject.json new file mode 100644 index 0000000..9050753 --- /dev/null +++ b/examples/secrets-engine/check_request_deny_unknown_subject.json @@ -0,0 +1,23 @@ +{ + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "some-other-service", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} +} diff --git a/examples/secrets-engine/policy_fixtures.yaml b/examples/secrets-engine/policy_fixtures.yaml new file mode 100644 index 0000000..49ae012 --- /dev/null +++ b/examples/secrets-engine/policy_fixtures.yaml @@ -0,0 +1,791 @@ +[ + { + "id": "fixture:secrets-engine-apply-allow", + "request": { + "id": "check:secrets-engine-apply", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "apply", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-provision-allow", + "request": { + "id": "check:secrets-engine-provision", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "provision", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-rotate-allow", + "request": { + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-verify-allow", + "request": { + "id": "check:secrets-engine-verify", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "verify", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-handoff-allow", + "request": { + "id": "check:secrets-engine-handoff", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "handoff", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-wrap-allow", + "request": { + "id": "check:secrets-engine-wrap", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "wrap", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-exec-allow", + "request": { + "id": "check:secrets-engine-exec", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "exec", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-deactivate-allow", + "request": { + "id": "check:secrets-engine-deactivate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "deactivate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-suspend-allow", + "request": { + "id": "check:secrets-engine-suspend", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "suspend", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-compromise-allow", + "request": { + "id": "check:secrets-engine-compromise", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "compromise", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-reactivate-allow", + "request": { + "id": "check:secrets-engine-reactivate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "reactivate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-destroy-dual-control-allow", + "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": { + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] + } + } + }, + "expect": { + "effect": "allow", + "reason": "catalog_lane_policy_matched" + } + }, + { + "id": "fixture:secrets-engine-destroy-no-claim-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": {} + }, + "expect": { + "effect": "deny", + "reason": "dual_control_required" + } + }, + { + "id": "fixture:secrets-engine-destroy-single-approver-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": { + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + } + ] + } + } + }, + "expect": { + "effect": "deny", + "reason": "dual_control_required" + } + }, + { + "id": "fixture:secrets-engine-destroy-repeated-approver-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": { + "status": "approved", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:alice" + } + ] + } + } + }, + "expect": { + "effect": "deny", + "reason": "dual_control_required" + } + }, + { + "id": "fixture:secrets-engine-destroy-pending-claim-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": { + "status": "pending", + "approvals": [ + { + "subject_id": "human:alice" + }, + { + "subject_id": "human:bob" + } + ] + } + } + }, + "expect": { + "effect": "deny", + "reason": "dual_control_required" + } + }, + { + "id": "fixture:secrets-engine-revoke-not-an-action-deny", + "request": { + "id": "check:secrets-engine-revoke", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "revoke", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-ungated-plan-deny", + "request": { + "id": "check:secrets-engine-plan", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "plan", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-ungated-route-deny", + "request": { + "id": "check:secrets-engine-route", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "route", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-ungated-audit-deny", + "request": { + "id": "check:secrets-engine-audit", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "audit", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-ungated-catalog-deny", + "request": { + "id": "check:secrets-engine-catalog", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "catalog", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-ungated-evidence-deny", + "request": { + "id": "check:secrets-engine-evidence", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "evidence", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_action" + } + }, + { + "id": "fixture:secrets-engine-unknown-subject-deny", + "request": { + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "some-other-service", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "unknown_subject" + } + }, + { + "id": "fixture:secrets-engine-wrong-subject-type-deny", + "request": { + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "human" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "wrong_subject_type" + } + }, + { + "id": "fixture:secrets-engine-wrong-system-deny", + "request": { + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "some-other-system", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "wrong_system" + } + }, + { + "id": "fixture:secrets-engine-wrong-resource-type-deny", + "request": { + "id": "check:secrets-engine-rotate", + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service" + }, + "action": "rotate", + "resource": { + "id": "lane:glas-primary", + "type": "secret", + "system": "secrets-engine", + "attributes": { + "stage": "prod", + "fields": [ + "password" + ], + "policy_targets": [], + "auth_targets": [] + } + }, + "context": {} + }, + "expect": { + "effect": "deny", + "reason": "wrong_resource_type" + } + } +] diff --git a/examples/secrets-engine/policy_package.md b/examples/secrets-engine/policy_package.md new file mode 100644 index 0000000..530238e --- /dev/null +++ b/examples/secrets-engine/policy_package.md @@ -0,0 +1,386 @@ +--- +id: secrets-engine.catalog-lane.lifecycle +name: secrets-engine catalog-lane lifecycle authorization +namespace: secrets-engine:secret-catalog-lane +version: v1 +status: ready +package: flexauth.secrets_engine.catalog_lane +allow_ttl: 15m +actions: + - apply + - provision + - rotate + - verify + - handoff + - wrap + - exec + - deactivate + - suspend + - destroy + - compromise + - reactivate +owner: team:platform-security +fixtures: + - policy_fixtures.yaml +caring: + profile: caring-0.4.0-rc2 + enforce: false + canonical_roles: + - Operator + organization_relations: + - ServiceProvider + scopes: + - level: Platform + id: platform:secrets-engine + tenant: tenant:platform + planes: + - Secret + - Policy + - Audit + capabilities: + - Create + - EditAny + - Execute + - Archive + - Restore + - View + - Audit + exposure_modes: + - Metadata + conditions: + - DualApprovalRequired + restrictions: + - PrivilegeEscalationBlocked +activation: + mode: local +metadata: + source: examples/secrets-engine/policy_package.md + flex_auth_contract: protected-system-v0 + action_vocabulary: docs/secrets-engine-action-vocabulary.md +--- + +# secrets-engine catalog-lane lifecycle authorization + +This package authorizes `secrets-engine`'s gated CLI operations over a secret +catalog lane. `secrets-engine` keeps custody of the secret material, the +OpenBao objects, and the local delivery overlay; flex-auth decides whether a +specific lifecycle operation is allowed now. + +Opened by `FLEX-DEC-2026-005` and carried by `FLEX-WP-0021`. + +## The vocabulary is theirs, not ours + +The twelve action values below were **delivered by secrets-engine** +(`FLEX-WP-0021-T01`, their `docs/gated-actions.md`), read out of `cli.py` +rather than derived from the `secrets-engine.lifecycle/v1` example vocabulary +that this package replaces. `FLEX-WP-0021-T01` made an inferred action a +blocker rather than a default, and four of the twelve would have been inferred +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: + +1. **`revoke` is not an action.** The CLI verb `revoke` gates as `deactivate`, + which is also reached from `lifecycle deactivate`. There is no `revoke` + value and one must not be added — `test_revoke_is_not_an_action` asserts it + denies `unknown_action`. +2. **`destroy` is defined but not reachable live.** Its handler raises before + the gate, so only `--dry-run` renders today. It stays in the vocabulary as + the dual-control case; no live `destroy` Check arrives until + `SECRETS-WP-0007-T04` lands. +3. **`compromise` and `reactivate` touch no OpenBao object.** They mutate local + overlay state only. They are gated because they change delivery posture, not + because they write to the backend. +4. **Seven CLI surfaces never reach the gate** and must not appear here: + `plan`, `apply --dry-run`, `route`, `audit`, `catalog`, `decision inspect`, + and `evidence`. `test_ungated_surfaces_are_not_actions` asserts they deny. + +On the fourth: `apply` *is* an action and `apply --dry-run` is not, but that +distinction is invisible to flex-auth — both would arrive as `apply`. The PEP +does not call the gate for a dry run, and that is the only thing keeping them +apart. flex-auth cannot enforce it and does not pretend to. + +## Request shape + +`resource.type` is `secret-catalog-lane`, `resource.system` is +`secrets-engine`, and the catalog id is `resource.id` +(`FLEX-DEC-2026-005`; secrets-engine implemented this in their commit +`627810b`). `resource.attributes` carries `stage` plus sorted `fields`, +`policy_targets`, and `auth_targets`. `fields` is populated for `provision`, +`rotate`, `verify`, and `exec`; the rest send an empty list rather than a +guess. + +## Allow lifetime + +`allow_ttl: 15m`, stated explicitly rather than inherited from the engine +default. These decisions authorize live secret operations, so the window in +which one may be relied on is a property worth writing down in the package +where a reviewer sees it, not one to leave implicit. + +Per `FLEX-DEC-2026-004`, that lifetime is authority to *issue* the operation, +not authority to keep using anything the operation produced. A wrapped or +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`, marked `approved`, naming at least +two distinct approver subject ids. + +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 + +**Decision (`FLEX-WP-0021-T02`, 2026-09-06): the denial ladder stops at +`unknown_subject` plus `dual_control_required`.** + +secrets-engine named exactly one calling identity. With one subject holding all +twelve actions, an `action_not_granted` branch could never fire. Following the +precedent set in `tenant-engine.write-api.mutate` (`FLEX-WP-0010-T02`), a rule +that cannot fail is worse than no rule: it reads to a later reviewer as though +per-action grants were separately controlled when they are not. + +`dual_control_required` is a real branch — it fires whenever `destroy` arrives +without a satisfying claim, and the fixtures exercise both sides. + +**Revisit when** a second calling identity is registered against +`system: "secrets-engine"`, or when secrets-engine splits its CLI identity by +lane or stage. Adding the branch then is additive to this package: no consumer +change and no request-shape change. + +## Rules + +```rego +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +valid_actions := { + "apply", + "provision", + "rotate", + "verify", + "handoff", + "wrap", + "exec", + "deactivate", + "suspend", + "destroy", + "compromise", + "reactivate", +} + +dual_control_actions := {"destroy"} + +known_subjects := {"secrets-engine"} + +decision := {"effect": "allow", "reason": "catalog_lane_policy_matched"} if { + allowed +} else := {"effect": "deny", "reason": first_denial} if { + true +} + +well_formed if { + input.resource.system == "secrets-engine" + input.resource.type == "secret-catalog-lane" + input.action in valid_actions + input.subject.type == "service" + input.subject.id in known_subjects +} + +allowed if { + well_formed + not input.action in dual_control_actions +} + +allowed if { + well_formed + input.action in dual_control_actions + dual_control_satisfied +} + +approver_ids := {id | + some entry in input.context.approval.approvals + id := entry.subject_id +} + +dual_control_satisfied if { + input.context.approval.status == "approved" + count(approver_ids) >= 2 +} + +default first_denial := "no_matching_rule" + +first_denial := "wrong_system" if { + input.resource.system != "secrets-engine" +} else := "wrong_resource_type" if { + input.resource.type != "secret-catalog-lane" +} else := "unknown_action" if { + not input.action in valid_actions +} else := "wrong_subject_type" if { + input.subject.type != "service" +} else := "unknown_subject" if { + not input.subject.id in known_subjects +} else := "dual_control_required" if { + input.action in dual_control_actions +} +``` + +## Tests + +```rego test +package flexauth.secrets_engine.catalog_lane_test + +import future.keywords.every +import future.keywords.if +import future.keywords.in +import data.flexauth.secrets_engine.catalog_lane + +lane(action) := { + "id": "check:secrets-engine", + "tenant": "tenant:platform", + "subject": {"id": "secrets-engine", "type": "service"}, + "action": action, + "resource": { + "id": "lane:glas-primary", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": {"stage": "prod", "fields": [], "policy_targets": [], "auth_targets": []} + }, + "context": {} +} + +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") +} + +test_provision_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("provision") +} + +test_rotate_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("rotate") +} + +test_verify_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("verify") +} + +test_handoff_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("handoff") +} + +test_wrap_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("wrap") +} + +test_exec_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("exec") +} + +test_deactivate_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("deactivate") +} + +test_suspend_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("suspend") +} + +test_compromise_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("compromise") +} + +test_reactivate_allowed if { + catalog_lane.decision.effect == "allow" with input as lane("reactivate") +} + +test_destroy_with_dual_control_allowed if { + catalog_lane.decision.effect == "allow" with input as approved_destroy +} + +test_destroy_without_claim_denied if { + catalog_lane.decision.reason == "dual_control_required" with input as lane("destroy") +} + +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_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_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"}] + }}} + ) +} + +test_revoke_is_not_an_action if { + catalog_lane.decision.reason == "unknown_action" with input as lane("revoke") +} + +test_ungated_surfaces_are_not_actions if { + every surface in ["plan", "route", "audit", "catalog", "decision", "evidence", "inspect"] { + catalog_lane.decision.reason == "unknown_action" with input as lane(surface) + } +} + +test_unknown_subject_denied if { + catalog_lane.decision.reason == "unknown_subject" with input as object.union( + lane("rotate"), + {"subject": {"id": "some-other-service", "type": "service"}} + ) +} + +test_wrong_subject_type_denied if { + catalog_lane.decision.reason == "wrong_subject_type" with input as object.union( + lane("rotate"), + {"subject": {"id": "secrets-engine", "type": "human"}} + ) +} + +test_wrong_system_denied if { + catalog_lane.decision.reason == "wrong_system" with input as object.union( + lane("rotate"), + {"resource": {"id": "lane:glas-primary", "type": "secret-catalog-lane", "system": "some-other-system"}} + ) +} + +test_wrong_resource_type_denied if { + catalog_lane.decision.reason == "wrong_resource_type" with input as object.union( + lane("rotate"), + {"resource": {"id": "lane:glas-primary", "type": "secret", "system": "secrets-engine"}} + ) +} +``` diff --git a/examples/secrets-engine/protected_system_manifest.yaml b/examples/secrets-engine/protected_system_manifest.yaml new file mode 100644 index 0000000..464e59b --- /dev/null +++ b/examples/secrets-engine/protected_system_manifest.yaml @@ -0,0 +1,105 @@ +id: secrets-engine +name: Secrets Engine +resource_types: + - name: secret-catalog-lane + scope_level: Resource + planes: + - Secret + - Policy + - Audit + metadata: + description: >- + A secret catalog lane: one catalog entry's delivery path at a given + stage. The catalog id is resource.id; stage plus sorted fields, + policy_targets, and auth_targets are resource.attributes. + flex-auth never sees secret material, only the lane's coordinates. +actions: + - name: apply + capabilities: [EditAny, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + note: >- + apply --dry-run never reaches the gate, but the distinction is + invisible here; both would arrive as "apply". + - name: provision + capabilities: [Create, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: rotate + capabilities: [EditAny, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: verify + capabilities: [View, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: handoff + capabilities: [EditAny, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: wrap + capabilities: [Execute, Audit] + planes: [Secret, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: exec + capabilities: [Execute, Audit] + planes: [Secret, Execution, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: deactivate + capabilities: [Archive, Audit] + planes: [Secret, Policy, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + note: >- + Reached from both the CLI verb "revoke" and "lifecycle deactivate". + There is no "revoke" action value. + - name: suspend + capabilities: [Archive, Audit] + planes: [Secret, Policy, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + - name: destroy + capabilities: [DeleteAny, Audit] + planes: [Secret, Policy, Audit] + exposure_modes: [Metadata] + metadata: + required_context: + - approval + note: >- + Dual control. Defined but not reachable live: the handler raises + before the gate, so only --dry-run renders until SECRETS-WP-0007-T04. + - name: compromise + capabilities: [EditAny, Audit] + planes: [Policy, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + note: Mutates local delivery overlay state only; touches no OpenBao object. + - name: reactivate + capabilities: [Restore, Audit] + planes: [Policy, Audit] + exposure_modes: [Metadata] + metadata: + required_context: [] + note: Mutates local delivery overlay state only; touches no OpenBao object. +caring_profiles: + - caring-0.4.0-rc2 +metadata: + flex_auth_contract: protected-system-v0 + action_vocabulary_source: secrets-engine docs/gated-actions.md (FLEX-WP-0021-T01) diff --git a/examples/secrets-engine/registry_snapshot.json b/examples/secrets-engine/registry_snapshot.json new file mode 100644 index 0000000..bcb34b6 --- /dev/null +++ b/examples/secrets-engine/registry_snapshot.json @@ -0,0 +1,282 @@ +{ + "systems": [ + { + "id": "secrets-engine", + "name": "Secrets Engine", + "resource_types": [ + { + "name": "secret-catalog-lane", + "scope_level": "Resource", + "planes": [ + "Secret", + "Policy", + "Audit" + ], + "metadata": { + "description": "A secret catalog lane: one catalog entry's delivery path at a given stage. The catalog id is resource.id; stage plus sorted fields, policy_targets, and auth_targets are resource.attributes. flex-auth never sees secret material, only the lane's coordinates." + } + } + ], + "actions": [ + { + "name": "apply", + "capabilities": [ + "EditAny", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [], + "note": "apply --dry-run never reaches the gate, but the distinction is invisible here; both would arrive as \"apply\"." + } + }, + { + "name": "provision", + "capabilities": [ + "Create", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "rotate", + "capabilities": [ + "EditAny", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "verify", + "capabilities": [ + "View", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "handoff", + "capabilities": [ + "EditAny", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "wrap", + "capabilities": [ + "Execute", + "Audit" + ], + "planes": [ + "Secret", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "exec", + "capabilities": [ + "Execute", + "Audit" + ], + "planes": [ + "Secret", + "Execution", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "deactivate", + "capabilities": [ + "Archive", + "Audit" + ], + "planes": [ + "Secret", + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [], + "note": "Reached from both the CLI verb \"revoke\" and \"lifecycle deactivate\". There is no \"revoke\" action value." + } + }, + { + "name": "suspend", + "capabilities": [ + "Archive", + "Audit" + ], + "planes": [ + "Secret", + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "destroy", + "capabilities": [ + "DeleteAny", + "Audit" + ], + "planes": [ + "Secret", + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [ + "approval" + ], + "note": "Dual control. Defined but not reachable live: the handler raises before the gate, so only --dry-run renders until SECRETS-WP-0007-T04." + } + }, + { + "name": "compromise", + "capabilities": [ + "EditAny", + "Audit" + ], + "planes": [ + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [], + "note": "Mutates local delivery overlay state only; touches no OpenBao object." + } + }, + { + "name": "reactivate", + "capabilities": [ + "Restore", + "Audit" + ], + "planes": [ + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [], + "note": "Mutates local delivery overlay state only; touches no OpenBao object." + } + } + ], + "caring_profiles": [ + "caring-0.4.0-rc2" + ], + "metadata": { + "flex_auth_contract": "protected-system-v0", + "action_vocabulary_source": "secrets-engine docs/gated-actions.md (FLEX-WP-0021-T01)" + } + } + ], + "resource_manifests": [], + "tenants": [ + { + "id": "tenant:platform", + "name": "Platform Tenant" + } + ], + "subjects": [ + { + "id": "secrets-engine", + "type": "Service", + "display_name": "secrets-engine service principal", + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ], + "groups": [ + "group:secrets-engine-lane-operators" + ], + "tenant": "tenant:platform", + "metadata": { + "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." + } + } + ], + "groups": [ + { + "id": "group:secrets-engine-lane-operators", + "display_name": "secrets-engine Catalog Lane Operators", + "members": [ + "secrets-engine" + ], + "tenant": "tenant:platform" + } + ], + "relationships": [] +} diff --git a/examples/secrets-engine/subject_manifest.yaml b/examples/secrets-engine/subject_manifest.yaml new file mode 100644 index 0000000..6d5b5e6 --- /dev/null +++ b/examples/secrets-engine/subject_manifest.yaml @@ -0,0 +1,27 @@ +id: subjects:secrets-engine-operators +tenants: + - id: tenant:platform + name: Platform Tenant +subjects: + - id: secrets-engine + type: Service + display_name: secrets-engine service principal + organization_relation: ServiceProvider + roles: + - Operator + groups: + - group:secrets-engine-lane-operators + tenant: tenant:platform + metadata: + 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. +groups: + - id: group:secrets-engine-lane-operators + display_name: secrets-engine Catalog Lane Operators + members: + - secrets-engine + tenant: tenant:platform 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 73e2a22..7446985 100644 --- a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md +++ b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md @@ -4,7 +4,7 @@ type: workplan title: "secrets-engine consumer policy package and cluster-local pin" domain: infotech repo: flex-auth -status: proposed +status: active owner: claude topic_slug: netkingdom planning_priority: P1 @@ -60,7 +60,7 @@ weaken, or supply a fallback for the consumer's package pin. ```task id: FLEX-WP-0021-T01 -status: todo +status: done priority: high state_hub_task_id: "3c191808-6fe5-5a9b-9a68-a4ce4a672dbd" ``` @@ -78,11 +78,19 @@ Owner: `flex-auth` to request and document; `secrets-engine` owns the answer. Gate: every action in the vocabulary is named by secrets-engine, not inferred here. An inferred action is a blocker, not a default. +**Done 2026-09-06.** secrets-engine delivered twelve actions in their +`docs/gated-actions.md`, read out of `cli.py`: `apply`, `provision`, `rotate`, +`verify`, `handoff`, `wrap`, `exec`, `deactivate`, `suspend`, `destroy`, +`compromise`, `reactivate`. Recorded in +`docs/secrets-engine-action-vocabulary.md`. The gate earned its keep: four +would have been inferred wrongly, and `revoke` — the obvious thirteenth — does +not exist as an action at all. + ## 2. Publish `secrets-engine.catalog-lane.lifecycle` v1 ```task id: FLEX-WP-0021-T02 -status: todo +status: done priority: high state_hub_task_id: "38770813-dfcc-5711-bba3-0db50d3af135" ``` @@ -103,6 +111,17 @@ Owner: `flex-auth`. Gate: `flex-auth validate` passes, the fixture tests pass, and the package digest is stable across two runs. +**Done 2026-09-06.** `examples/secrets-engine/` carries the package +(`allow_ttl: 15m` stated explicitly), both manifests, a loadable +`registry_snapshot.json`, 26 fixtures, five standalone check requests, and a +README. `validate -kind policy` reports valid with 22/22 Rego tests and 26/26 +fixtures passing. `destroy` is the dual-control case, gated on a +`context.approval` claim marked approved with two distinct approvers; flex-auth +checks what the claim says and does not re-derive its validity, signature, or +supersession (`FLEX-DEC-2026-006`). Per the `FLEX-WP-0010-T02` precedent the +denial ladder has no `action_not_granted` branch, because one subject holding +all twelve actions could never reach it. + ## 3. Confirm the digest join against a real decision record ```task