From f75db59a8ca0a72fee6fca68fc61c835ae122883 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 08:02:52 +0200 Subject: [PATCH] Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02) secrets-engine delivered the action vocabulary T01 asked for: twelve actions read out of cli.py, not the example vocabulary. The gate earned its keep -- four would have been inferred wrongly, and revoke, the obvious thirteenth, does not exist as an action at all. It gates as deactivate, which is also reached from lifecycle deactivate. docs/secrets-engine-action-vocabulary.md records the list and the four traps. examples/secrets-engine/ carries the package, both manifests, a loadable registry snapshot, 26 fixtures, five check requests, and a README. validate -kind policy is valid with 22/22 Rego tests and 26/26 fixtures. allow_ttl is 15m, stated in the package rather than inherited from the engine default: these decisions authorize live secret operations, so the reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004 it is authority to issue, not authority to keep using what the operation produced. destroy is the dual-control case, gated on a context.approval claim marked approved with two distinct approvers, repeated entries counting once. flex-auth checks what the claim says and deliberately does not re-derive its temporal validity, signature, or supersession -- those are approval-engine's to assert and the PEP's to verify against the live claim, per the split accepted in FLEX-DEC-2026-006. It stays in the package though its handler raises before the gate, so the rule is reviewed and fixtured before SECRETS-WP-0007-T04 opens the path. Following the FLEX-WP-0010-T02 precedent the denial ladder has no action_not_granted branch: one subject holding all twelve actions could never reach it, and a rule that cannot fail reads as control that is not there. Registering a second calling identity is the revisit trigger. Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their policy pin stays unset and fail-closed until T05. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB Assistant: claude-code Assistant-Model: opus Assistant-Process: 412054@bnt-lap001 Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19 --- docs/secrets-engine-action-vocabulary.md | 91 ++ examples/secrets-engine/README.md | 36 + ...ck_request_allow_destroy_dual_control.json | 33 + .../check_request_allow_rotate.json | 23 + ...ck_request_deny_destroy_without_claim.json | 21 + ...eck_request_deny_revoke_not_an_action.json | 21 + .../check_request_deny_unknown_subject.json | 23 + examples/secrets-engine/policy_fixtures.yaml | 791 ++++++++++++++++++ examples/secrets-engine/policy_package.md | 386 +++++++++ .../protected_system_manifest.yaml | 105 +++ .../secrets-engine/registry_snapshot.json | 282 +++++++ examples/secrets-engine/subject_manifest.yaml | 27 + ...021-secrets-engine-consumer-policy-gate.md | 25 +- 13 files changed, 1861 insertions(+), 3 deletions(-) create mode 100644 docs/secrets-engine-action-vocabulary.md create mode 100644 examples/secrets-engine/README.md create mode 100644 examples/secrets-engine/check_request_allow_destroy_dual_control.json create mode 100644 examples/secrets-engine/check_request_allow_rotate.json create mode 100644 examples/secrets-engine/check_request_deny_destroy_without_claim.json create mode 100644 examples/secrets-engine/check_request_deny_revoke_not_an_action.json create mode 100644 examples/secrets-engine/check_request_deny_unknown_subject.json create mode 100644 examples/secrets-engine/policy_fixtures.yaml create mode 100644 examples/secrets-engine/policy_package.md create mode 100644 examples/secrets-engine/protected_system_manifest.yaml create mode 100644 examples/secrets-engine/registry_snapshot.json create mode 100644 examples/secrets-engine/subject_manifest.yaml 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