fix(secrets-engine): v2 adds the tenant rule v1 never had
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 41s

secrets-engine.catalog-lane.lifecycle v1 contained no reference to
input.tenant — not in well_formed, not in the denial ladder, not in a
test. A rotate on lane:glas-primary under tenant:coulomb returned allow
against the deployed package (decision:066e629bbf0c0924).

Found while answering glas-harness's tenant-alignment request, which had
asked for wrong-tenant denial evidence. There was none to return.

Three covers failed the same way: every one of the 29 fixtures carried
tenant:platform, so the suite could not report on the field; T02's own
gate named "wrong-tenant deny" and was recorded done unmet; and the
engine hashes tenant into request_digest but never compares it. Four
other published packages carry the branch — this one was the outlier.

v2 adds wrong_tenant above wrong_system, three Rego tests and three
fixtures (28/28, 32/32). The absent-tenant test caught a second defect
in the first draft: a bare input.tenant != comparison is undefined on a
missing key, so the branch dropped and the ladder reported the wrong
rung. request_tenant := object.get(input, "tenant", "") fixes it.

v2 supersedes rather than amends v1 because the defect failed open: a
consumer pinned to _VERSION=v1 would keep receiving allows with no
signal the rule beneath the version string had changed. The earlier
dual-control correction stayed at v1 because it denied everything.

Replay envelopes regenerated at v2; both request_digest values are
byte-identical, so secrets-engine's digest join needs no re-pinning.

The sweep this prompted found tenant-engine unscoped on tenant as well —
deployed, and verified allowing tenant:coulomb. Not the same fix: its
request tenant names the target rather than the caller, so a constant
would break it. Recorded and carried by FLEX-WP-0022 rather than patched
unilaterally.

FLEX-WP-0021 closes at T05; the pin still serves v1 until a redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715613@bnt-lap001
Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
This commit is contained in:
tegwick 2026-09-06 20:38:45 +02:00
parent a81697a589
commit d98323b2bb
12 changed files with 690 additions and 28 deletions

View file

@ -7,6 +7,7 @@ to verify its digest join (`627810b`) unchanged. `FLEX-WP-0021-T03`.
| --- | --- |
| `decision_rotate.json` | `../check_request_allow_rotate.json` — plain allow, empty context |
| `decision_destroy_dual_control.json` | `../check_request_allow_destroy_dual_control.json` — dual control, valid approval-claim |
| `decision_wrong_tenant_deny.json` | `../check_request_deny_wrong_tenant.json` — foreign tenant, denied `wrong_tenant` |
Regenerate either with:
@ -24,12 +25,26 @@ go run ./cmd/flex-auth check \
| Field | `rotate` | `destroy` |
| --- | --- | --- |
| `binding.request_digest` | `sha256:de67324f…4345` | `sha256:c749ee2…091a` |
| `provenance.policy_package_digest` | `sha256:fe0070b7…bd8c` | same |
| `provenance.policy_package_digest` | `sha256:bd11c5fe…c643` | same |
| `provenance.registry_snapshot_digest` | `sha256:f5a309bc…40bb` | same |
| `provenance.input_claim_digests.context` | absent (empty context) | `sha256:8b73d29…2800` |
Verified identical across two runs.
**Regenerated at `v2`, 2026-09-06.** The package gained the tenant rule it had
been missing, so `provenance.policy_version` is now `v2` and
`policy_package_digest` moved from `sha256:fe0070b7…bd8c` to
`sha256:bd11c5fe…c643`. **Both `request_digest` values are unchanged** — the
canonical digest covers tenant/subject/action/resource/context and not the
package, so a consumer that pinned the digest join has nothing to re-pin. Only
the two provenance fields moved, and they moved because the policy did.
`decision_wrong_tenant_deny.json` is the denial evidence for the tenant
question: the same `rotate` request as `decision_rotate.json` with
`tenant: tenant:coulomb` substituted. Its `request_digest` differs from the
allow's, which is the replay identity working — the tenant is hashed material,
so the two requests are not the same request. A deny carries no `lifetime`.
| `binding.approval_binding_digest` | absent (no claim) | `sha256:fa07bec…cd56` |
That last row is the value an approval's `pdp_digest` must equal — never

View file

@ -1,10 +1,10 @@
{
"id": "decision:44a40c339a020772",
"id": "decision:4e327202e2aee41c",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-destroy",
"effect": "allow",
"reason": "catalog_lane_policy_matched",
"matched_policy_version": "v1",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
@ -105,8 +105,8 @@
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-06T12:51:16Z",
"expires_at": "2026-09-06T13:06:16Z"
"not_before": "2026-09-06T18:35:19Z",
"expires_at": "2026-09-06T18:50:19Z"
},
"diagnostics": {
"action": "destroy",
@ -120,13 +120,13 @@
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v1",
"policy_package_digest": "sha256:fe0070b79f66442ae6c218697a49c470c6c8f670aa57a30c078a5284d097bd8c",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"input_claim_digests": {
"context": "sha256:8b73d29ecef286d42e03d2420531d6c45219f325a7ae004c1ecfc781203a2800"
},
"decision_time": "2026-09-06T12:51:16Z"
"decision_time": "2026-09-06T18:35:19Z"
},
"caring": {
"profile": "caring-0.4.0-rc2",

View file

@ -1,10 +1,10 @@
{
"id": "decision:49309356905a2ad3",
"id": "decision:414734bb30381ff7",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-rotate",
"effect": "allow",
"reason": "catalog_lane_policy_matched",
"matched_policy_version": "v1",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
@ -74,8 +74,8 @@
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-06T06:13:21Z",
"expires_at": "2026-09-06T06:28:21Z"
"not_before": "2026-09-06T18:35:18Z",
"expires_at": "2026-09-06T18:50:18Z"
},
"diagnostics": {
"action": "rotate",
@ -89,10 +89,10 @@
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v1",
"policy_package_digest": "sha256:fe0070b79f66442ae6c218697a49c470c6c8f670aa57a30c078a5284d097bd8c",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-06T06:13:21Z"
"decision_time": "2026-09-06T18:35:18Z"
},
"caring": {
"profile": "caring-0.4.0-rc2",

View file

@ -0,0 +1,104 @@
{
"id": "decision:7d56b7fc274ddfd6",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-wrong-tenant",
"effect": "deny",
"reason": "wrong_tenant",
"matched_policy_version": "v2",
"matched_rule": "wrong_tenant",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:coulomb",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"binding": {
"tenant": "tenant:coulomb",
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"action": "rotate",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:coulomb",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"request_digest": "sha256:c9c6e6f8713266e9e95ae1443a395a3a1f965ba95469dea747645f0437bb0d20"
},
"diagnostics": {
"action": "rotate",
"matched_relationship": "",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_status": "ready",
"registry_resource": false,
"registry_subject": true
},
"provenance": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-06T18:35:20Z"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"conformance_findings": [
{
"code": "CARING-DESCRIPTOR-MISSING",
"severity": "warning",
"message": "no CARING descriptor matched the request",
"fields": [
"caring_context"
]
}
]
}
}