From 6a6464fcc9b52f2266836a78360e299b87070316 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 01:12:50 +0200 Subject: [PATCH] Answer ops-warden and secrets-engine; open FLEX-WP-0021 Two cross-repo questions arrived in the flex-auth inbox and both are answered as decision records rather than as prose in a message. FLEX-DEC-2026-004 answers ops-warden WARDEN-WP-0034-T05. A decision lifetime shorter than the SSH certificate TTL is meaningful, but only as authority to issue, never as authority to use an already-issued certificate. The pre-sign gate is the only consumer of the shorter lifetime: no replay past expires_at, fresh Check per sign. The lever that shortens effective access is the requested TTL as a policy input, which is already deployed as the ttl_out_of_bounds deny. ops-warden's section 9.7.2 window through certificate TTL is correct as written and correctly owned by the PEP; flex-auth does not want that residue moved to the PDP. docs/decision-input-freshness.md gains the same boundary as published contract text, so the ruling is not only in the decision log. FLEX-DEC-2026-005 answers secrets-engine. A real policy package is expected and flex-auth authors it here as it does for every consumer; the reserved coordinate is secrets-engine.catalog-lane.lifecycle v1 and it does not exist yet. Their choice not to default the pin was correct and is endorsed explicitly. POST /v1/check is deployed but has no estate-wide address by design -- per-consumer cluster-local pins with default-deny ingress -- so their 2026-09-06 probe found the design working, not an outage. FLEX-WP-0021 carries that work: obtain the real action vocabulary from secrets-engine, publish the package with fixtures, confirm the digest join against a real decision record, then stand up a flex-auth-secrets-engine pin in warn without moving the other two pins. 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 --- .repo-manager/index.json | 287 +++++++++++++++++- decisions/decisions.md | 205 +++++++++++++ docs/decision-input-freshness.md | 17 ++ ...021-secrets-engine-consumer-policy-gate.md | 165 ++++++++++ 4 files changed, 659 insertions(+), 15 deletions(-) create mode 100644 workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md diff --git a/.repo-manager/index.json b/.repo-manager/index.json index 168a277..9dfe14a 100644 --- a/.repo-manager/index.json +++ b/.repo-manager/index.json @@ -2,9 +2,9 @@ "schema": "repo_manager.index.v1", "slug": "flex-auth", "repo_root": "/home/worsch/flex-auth", - "head_sha": "5753b47ccb72f96689df9ec5eeb7be100f6817cd", - "observed_at": "2026-08-29T08:19:42.754823Z", - "source_fingerprint": "2d3773fc38408c64d1a9a4efc7335ed792f361cdf6464f04389615c9b443fa53", + "head_sha": "56940727bfb36aec705318b0128599f367256c23", + "observed_at": "2026-09-05T23:08:53.160637Z", + "source_fingerprint": "727c449ea8eb418a198b97d34244e377936060c84024b65b2af950ea71714259", "source_files": [ ".repo-classification.yaml", "INTENT.md", @@ -27,7 +27,9 @@ "workplans/FLEX-WP-0015-tenancy-posture-conformance.md", "workplans/FLEX-WP-0016-ops-warden-incluster-policy-pin.md", "workplans/FLEX-WP-0017-action-bound-authorization-contract.md", - "workplans/FLEX-WP-0018-inbound-auth-corrections.md" + "workplans/FLEX-WP-0018-inbound-auth-corrections.md", + "workplans/FLEX-WP-0019-layer-model-conformance.md", + "workplans/FLEX-WP-0020-repository-identity-migration.md" ], "work_records": [ { @@ -937,7 +939,7 @@ { "kind": "workplan", "id": "FLEX-WP-0017", - "status": "active", + "status": "finished", "title": "Action-bound authorization and durable approval contract", "source_path": "workplans/FLEX-WP-0017-action-bound-authorization-contract.md", "uuid": "d75b7256-8b3d-5797-911c-96c3199b8baa", @@ -967,7 +969,7 @@ { "kind": "task", "id": "FLEX-WP-0017-T03", - "status": "wait", + "status": "cancel", "title": "Add durable storage and authenticated approval evidence", "source_path": "workplans/FLEX-WP-0017-action-bound-authorization-contract.md", "uuid": "82d39961-8140-5a7f-9bd8-5164dd1742e5", @@ -987,7 +989,7 @@ { "kind": "task", "id": "FLEX-WP-0017-T05", - "status": "wait", + "status": "cancel", "title": "Consumer handoff and live destructive-action proof", "source_path": "workplans/FLEX-WP-0017-action-bound-authorization-contract.md", "uuid": "8c3fc0a2-0855-5ac9-afa5-d03b8b1f0bf9", @@ -1024,6 +1026,196 @@ "parent_id": "FLEX-WP-0018", "extra": {} }, + { + "kind": "workplan", + "id": "FLEX-WP-0019", + "status": "finished", + "title": "Layer model v0.7 conformance: provenance, lifetimes, deadlines, and the decision contract", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "84b9dc5a-71f2-5c70-ace6-78242b13d0f1", + "parent_id": null, + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T01", + "status": "done", + "title": "Assert the layer declaration mechanically", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "f72b1305-114e-5ba2-b84c-f6cc0b524178", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T02", + "status": "done", + "title": "Add the registry-snapshot digest to decision provenance", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "7a980074-8488-5ab1-9202-60878adb261d", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T03", + "status": "done", + "title": "Give every allow an explicit lifetime", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "9d9c0e7a-56e2-5c71-9110-cea973243c22", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T04", + "status": "done", + "title": "State revocation visibility deadlines per input class", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "d5917b24-2efb-503e-9a9d-837702cafc1c", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T05", + "status": "done", + "title": "Publish the decision-record schema as flex-auth's contract", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "f7f501d7-6862-5542-88f7-78b704524706", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0019-T06", + "status": "done", + "title": "Publish the request digest as the replay test", + "source_path": "workplans/FLEX-WP-0019-layer-model-conformance.md", + "uuid": "bc109ee3-14b0-5603-a655-0d7376c2a41c", + "parent_id": "FLEX-WP-0019", + "extra": {} + }, + { + "kind": "workplan", + "id": "FLEX-WP-0020", + "status": "proposed", + "title": "Repository identity migration from flex-auth to access-engine", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "99a661a8-b36c-5c1c-b78b-1e8930bcd0a9", + "parent_id": null, + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T01", + "status": "todo", + "title": "1. Capture immutable cleanliness and identity baseline", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "bb989019-50a7-5273-ba09-b2df2e2602a4", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T02", + "status": "todo", + "title": "2. Prepare repository metadata and work-record frontmatter", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "102e9dc7-4724-5e80-84c4-092e6ecfbb2b", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T03", + "status": "todo", + "title": "3. Decide product and runtime naming separately", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "5095ddbc-b69c-5a52-b97f-08fca9b610c3", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T04", + "status": "todo", + "title": "4. Inventory consumers and create owned handoffs", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "9bb138e1-5edb-5714-8d89-3b1b7039a7ce", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T05", + "status": "todo", + "title": "5. Renew State Hub preflight and record approval", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "86fecbb6-b008-5354-8d70-0f4ba5077a58", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T06", + "status": "wait", + "title": "6. Execute the Forgejo repository rename", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "aa7ccd59-ddd3-5af0-a866-30e88552fc09", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T07", + "status": "wait", + "title": "7. Record the State Hub identity rebind", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "155041ed-8cba-5e5e-ac59-fc03ef6dd0ce", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T08", + "status": "wait", + "title": "8. Establish and register a fresh canonical clone", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "c8afe078-41cf-5d8c-84d1-a49e4b618d34", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T09", + "status": "wait", + "title": "9. Verify identity, history, routes, builds, and deployments", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "fdb50a96-f7fa-5d4a-9efa-231750dcb2f1", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T10", + "status": "wait", + "title": "10. Exercise rollback decision points", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "3dc49eb4-1f39-5250-87c9-716a9a97790d", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0020-T11", + "status": "wait", + "title": "11. Soak, hand off residuals, and clean up the old checkout", + "source_path": "workplans/FLEX-WP-0020-repository-identity-migration.md", + "uuid": "46d8318b-d1fb-52e3-b632-52230ed1438f", + "parent_id": "FLEX-WP-0020", + "extra": {} + }, { "kind": "decision", "id": "FLEX-DEC-2026-001", @@ -1112,7 +1304,7 @@ "status": "resolved", "title": "Review of security layer model v0.6 and companion v0.1: assent, two answers, five findings", "source_path": "decisions/decisions.md", - "uuid": null, + "uuid": "def6a31b-4cd7-4474-8cfa-339b0fc9fcc6", "parent_id": null, "extra": { "record": { @@ -1141,7 +1333,72 @@ "updated": "2026-08-29T08:19:41.832549Z", "rationale": "Assent to v0.6 and companion v0.1, with two answers and five findings, none blocking. Q1: section 6.4.2 is right but collides with 9.7.1's session-bound allow, needs the request digest as its mechanical replay test, and should rule explicitly on deny-caching. Q2: the visibility deadline does land on a PDP and harder than at a PEP, but must be per input class rather than one number, and it makes flex-auth's registry-provenance gap load-bearing rather than untidy. Findings: 6.4's stance-map register does not exist in section 13 and neither document says where a map is published; the companion omits it too, which is the sufficiency gap gate-house asked for; section 17 puts the decision-record schema in Taxonomy when it is the PDP's output artifact, inverting the section 2 rule flex-auth used to decline authentication evidence; sections 17-19 are H1 outside the hierarchy; section 19 grades the document it lives in and will age.", "decided_by": "flex-auth (reviewing side)", - "decided_at": "2026-08-29T08:19:41.832549Z" + "decided_at": "2026-08-29T08:19:41.832549Z", + "state_hub_decision_id": "def6a31b-4cd7-4474-8cfa-339b0fc9fcc6" + } + } + }, + { + "kind": "decision", + "id": "FLEX-DEC-2026-004", + "status": "resolved", + "title": "Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05", + "source_path": "decisions/decisions.md", + "uuid": null, + "parent_id": null, + "extra": { + "record": { + "id": "FLEX-DEC-2026-004", + "kind": "decision", + "title": "Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05", + "status": "resolved", + "origin": "cross-repo", + "origin_ref": "WARDEN-WP-0034-T05", + "owner": "flex-auth", + "affects": [ + "flex-auth", + "ops-warden" + ], + "requested_dispositions": [ + "answer", + "decline" + ], + "decided_by": "flex-auth (access-engine / PDP)", + "rationale": "Answered, not declined. A decision lifetime shorter than the SSH certificate TTL is meaningful, but only as an authority-to-issue window, never as an authority-to-use window over an already-issued certificate. flex-auth lifetime.expires_at bounds how long that one allow may be relied on to authorise a sign; it cannot bound an artifact ops-warden issued under it, and flex-auth does not claim it does. The downstream contract that consumes the shorter lifetime is the pre-sign gate itself: no replay of an allow past expires_at, and a fresh Check per sign. The lever that actually shortens effective access is the requested TTL as a policy input, which is already deployed as the ttl_out_of_bounds deny; ops-warden section 9.7.2 window through cert TTL is correctly stated and correctly owned by the PEP.", + "created": "2026-09-05T23:08:38.877787Z", + "updated": "2026-09-05T23:08:38.877787Z" + } + } + }, + { + "kind": "decision", + "id": "FLEX-DEC-2026-005", + "status": "resolved", + "title": "secrets-engine policy package is expected but unpublished; /v1/check has no estate-wide endpoint by design", + "source_path": "decisions/decisions.md", + "uuid": null, + "parent_id": null, + "extra": { + "record": { + "id": "FLEX-DEC-2026-005", + "kind": "decision", + "title": "secrets-engine policy package is expected but unpublished; /v1/check has no estate-wide endpoint by design", + "status": "resolved", + "origin": "cross-repo", + "origin_ref": "secrets-engine SECRETS-WP-0009-T03", + "owner": "flex-auth", + "affects": [ + "flex-auth", + "secrets-engine", + "approval-engine" + ], + "requested_dispositions": [ + "answer" + ], + "decided_by": "flex-auth (access-engine / PDP)", + "rationale": "Two answers. (1) Yes, a real package is expected, and flex-auth authors it in this repo as it did for every other consumer; the reserved coordinate is secrets-engine.catalog-lane.lifecycle at version v1, and it does not exist yet. Until it is published and pinned, secrets-engine keeping SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE and _VERSION as required configuration with no fallback is the correct shape and flex-auth endorses it. (2) POST /v1/check is deployed, but there is no estate-wide PDP address and there is not meant to be one: each consumer gets its own cluster-local pin whose NetworkPolicy default-denies ingress except from that one approved workload. The 2026-09-06 probe finding no reachable PDP is the design working, not an outage. A reachable endpoint for secrets-engine is a per-consumer pin that follows its policy package.", + "created": "2026-09-05T23:08:51.144287Z", + "updated": "2026-09-05T23:08:51.144287Z" } } }, @@ -1220,17 +1477,17 @@ "events": [ { "type": "repo.command.applied", - "command": "repo.work.resolve_decision", - "operation": "resolve", - "correlation_id": "fe430c1e-83bb-4dba-b6fc-337baa1a0059", + "command": "repo.work.create_decision", + "operation": "create", + "correlation_id": "ff360be5-e7bf-4914-95c3-1ea6df9f6957", "kind": "decision", - "id": "FLEX-DEC-2026-003", - "git_sha": "5753b47ccb72f96689df9ec5eeb7be100f6817cd", + "id": "FLEX-DEC-2026-005", + "git_sha": null, "files_touched": [ "decisions/decisions.md" ], "source": "repo-manager", - "emitted_at": "2026-08-29T08:19:42.754925Z" + "emitted_at": "2026-09-05T23:08:53.160770Z" } ] } diff --git a/decisions/decisions.md b/decisions/decisions.md index a450bac..9af1f85 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -537,3 +537,208 @@ questions already carry the same content as questions rather than as a grade. conformance prerequisite under §9.7.2, and will be planned as one. - flex-auth offers the canonical request digest as the §6.4.2 replay test and offers to publish the decision-record schema as its own contract. + +## FLEX-DEC-2026-004 — Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05 + +```yaml +id: FLEX-DEC-2026-004 +kind: decision +title: 'Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05' +status: resolved +origin: cross-repo +origin_ref: WARDEN-WP-0034-T05 +owner: flex-auth +affects: +- flex-auth +- ops-warden +requested_dispositions: +- answer +- decline +decided_by: flex-auth (access-engine / PDP) +rationale: 'Answered, not declined. A decision lifetime shorter than the SSH certificate + TTL is meaningful, but only as an authority-to-issue window, never as an authority-to-use + window over an already-issued certificate. flex-auth lifetime.expires_at bounds + how long that one allow may be relied on to authorise a sign; it cannot bound an + artifact ops-warden issued under it, and flex-auth does not claim it does. The downstream + contract that consumes the shorter lifetime is the pre-sign gate itself: no replay + of an allow past expires_at, and a fresh Check per sign. The lever that actually + shortens effective access is the requested TTL as a policy input, which is already + deployed as the ttl_out_of_bounds deny; ops-warden section 9.7.2 window through + cert TTL is correctly stated and correctly owned by the PEP.' +created: '2026-09-05T23:08:38.877787Z' +updated: '2026-09-06T00:00:00.000000Z' +decided_at: '2026-09-06T00:00:00.000000Z' +``` + +## Context + +ops-warden now states its §9.7.2 revocation window explicitly: an allow can +remain effective through an already-issued SSH certificate until TTL expiry +(`adm` 48h, `agt` 24h, `atm` 8h), because there is no CRL/KRL recall channel. +It asked flex-auth, as the access engine, whether a decision lifetime shorter +than the certificate TTL is meaningful under that PEP behaviour, and if so what +downstream contract should consume the shorter lifetime. + +## Disposition + +**Answered, not declined.** The lifetime is meaningful, and the mismatch +ops-warden noticed is not a defect on either side. It is two different objects +with two different revocation stories. + +### A decision lifetime is authority to issue, not authority to use + +`lifetime.expires_at` (`docs/decision-record-contract.md`, default TTL 15m) +bounds one thing: how long that particular allow may be relied on as the +authority for the action it decided. For ops-warden the action is `sign`, and +the action completes at issuance. Once the certificate exists, the decision has +been consumed; nothing in the decision record reaches the artifact. + +flex-auth does not claim otherwise, and a PDP that did would be lying. The +estate already ruled that fail-open is not expressible by a PDP because there +is no evaluator in that path; recall of an issued credential is the same shape +of claim. There is no flex-auth in the path of an SSH session already +authenticated by a valid certificate. + +So a 15m decision lifetime against a 48h `adm` certificate is not an +inconsistency to reconcile. It says the gate's answer goes stale in 15 minutes; +it says nothing about the certificate, and was never asked to. + +### The downstream contract is the pre-sign gate itself + +That is the only consumer of the shorter lifetime, and it consumes it two ways: + +1. **No replay past `expires_at`.** An allow may not be reused to authorise a + second `warden sign`, and a cached verdict must not outlive its TTL. This is + already the published rule (`docs/decision-input-freshness.md`, "How to read + this as a consumer") and it is the whole of what the lifetime binds. +2. **Fresh Check per sign.** Because the lifetime is short and claims are not + cached PDP-side, revocation of a subject, actor, or approval claim is visible + to the *next* sign immediately — deadline 0 for the claim class. That is the + real value of the short lifetime: it bounds how long a revoked principal can + keep obtaining *new* certificates, which is the part flex-auth can bound. + +### The lever that shortens effective access is already deployed + +If the goal is to shrink the window in which a revoked principal retains live +access, the mechanism is not a shorter decision lifetime — it is a shorter +**certificate** TTL, and the requested TTL is already a policy input. The +shipped `ops-warden.ssh-certificate.sign` package denies `ttl_out_of_bounds` +before OpenBao is reached (verified 2026-06-29). Capping the requested TTL per +actor class or per zone in the policy package is a policy change flex-auth can +make and explain; asking the decision lifetime to reach past issuance is not. + +flex-auth is not proposing that change here. Certificate TTLs are ops-warden's +to set, and this record does not reopen them. + +## Consequences + +- ops-warden's §9.7.2 statement — allow effective through certificate TTL, + bounded by 48h/24h/8h, no recall channel — is **correct as written and + correctly owned by the PEP**. flex-auth does not want that residue moved to + the PDP's side of the line. +- No change to `flex-auth.decision-record.v1`. `lifetime` keeps its published + meaning; this record states the boundary it already had rather than adding + one. +- If ops-warden ever wants a certificate TTL bound to policy rather than to + actor class, that is a policy-package change under a new workplan, not a + lifetime semantics change. + +## FLEX-DEC-2026-005 — secrets-engine policy package is expected but unpublished; /v1/check has no estate-wide endpoint by design + +```yaml +id: FLEX-DEC-2026-005 +kind: decision +title: secrets-engine policy package is expected but unpublished; /v1/check has no + estate-wide endpoint by design +status: resolved +origin: cross-repo +origin_ref: secrets-engine SECRETS-WP-0009-T03 +owner: flex-auth +affects: +- flex-auth +- secrets-engine +- approval-engine +requested_dispositions: +- answer +decided_by: flex-auth (access-engine / PDP) +rationale: 'Two answers. (1) Yes, a real package is expected, and flex-auth authors + it in this repo as it did for every other consumer; the reserved coordinate is secrets-engine.catalog-lane.lifecycle + at version v1, and it does not exist yet. Until it is published and pinned, secrets-engine + keeping SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE and _VERSION as required configuration + with no fallback is the correct shape and flex-auth endorses it. (2) POST /v1/check + is deployed, but there is no estate-wide PDP address and there is not meant to be + one: each consumer gets its own cluster-local pin whose NetworkPolicy default-denies + ingress except from that one approved workload. The 2026-09-06 probe finding no + reachable PDP is the design working, not an outage. A reachable endpoint for secrets-engine + is a per-consumer pin that follows its policy package.' +created: '2026-09-05T23:08:51.144287Z' +updated: '2026-09-06T00:00:00.000000Z' +decided_at: '2026-09-06T00:00:00.000000Z' +``` + +## Context + +secrets-engine applied flex-auth's earlier PDP answers in commit `627810b` +(`resource.type: secret-catalog-lane`, `resource.system: secrets-engine`, +catalog id as `request.resource.id`, and a join that validates +`ActionAuthorization.decision.binding.request_digest` against +`flex-auth.decision-record.v1`). It then asked two questions, with Glas +real-key execution (`SECRETS-WP-0009-T03`) waiting on the answers. + +## Disposition + +### Answer 1 — yes, a package is expected; it does not exist yet + +Every shipped consumer's policy package is authored **in this repository**, not +in the consumer's, and baked into the image: `ops-warden.ssh-certificate.sign` +(v2), `tenant-engine.write-api.mutate`, `user-engine.portal.authorize`, +`railiance-platform.credential-grant.issue`, `qonto-assistant.finance-read`. +secrets-engine follows the same path. The reserved coordinate is: + +| | | +| --- | --- | +| Package | `secrets-engine.catalog-lane.lifecycle` | +| Version | `v1` | + +This is a **reservation, not a publication.** No such package exists in +`examples/` today, and until one is published and pinned, a Check naming it +evaluates nothing. `FLEX-WP-0021` carries the work. + +secrets-engine's decision not to default the pin was the right call and +flex-auth endorses it explicitly: keeping +`SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` and `_VERSION` as required +configuration with no fallback is exactly correct. Defaulting them would have +pinned production to example vocabulary nobody publishes, and the failure would +have been a silent evaluation against a package that is not there rather than a +loud missing-configuration error. + +### Answer 2 — /v1/check is deployed; there is no estate-wide address, by design + +The 2026-09-06 probe finding no reachable PDP found the design working. + +flex-auth runs as **per-consumer cluster-local pins**, not as one shared +service. Three are live (`flex-auth-ops-warden`, `flex-auth-tenant-engine`, +`flex-auth-user-engine`). Each is a three-document manifest whose third document +is a default-deny `NetworkPolicy` admitting ingress from exactly one approved +consumer workload, and each is pinned to its own image digest so one consumer's +policy can roll without moving another's (`deploy/README.md`). Caller +authentication is per-pin: on the ops-warden pin it is `enforce`, so an +anonymous `/v1/check` is 401 and a token bound to another protected system is +403 (`FLEX-WP-0016`). + +There is therefore no address for secrets-engine to reach *because none has been +created for it*. A reachable endpoint is a per-consumer pin, and a pin follows +its policy package — so both questions resolve to the same piece of work, in +that order: publish the package, then pin it. + +## Consequences + +- `FLEX-WP-0021` is opened in this repo to publish + `secrets-engine.catalog-lane.lifecycle` v1 and stand up a + `flex-auth-secrets-engine` pin. It needs secrets-engine's real action + vocabulary as input; the example `secrets-engine.lifecycle/v1` vocabulary is + explicitly not it. +- secrets-engine should keep the pin required and unset until the package is + published. Nothing in this record authorises a fallback value. +- No change to `flex-auth.decision-record.v1`. The digest join secrets-engine + built against it is correct as described. diff --git a/docs/decision-input-freshness.md b/docs/decision-input-freshness.md index 2e4d3a8..6da7ed3 100644 --- a/docs/decision-input-freshness.md +++ b/docs/decision-input-freshness.md @@ -62,6 +62,21 @@ read a different snapshot. A stale-directory signal from Topaz fails closed (`topaz_directory_stale`). +## Lifetime versus an artifact issued under the decision + +`lifetime.expires_at` bounds the **decision**, not anything a consumer creates +while acting on it. It is authority to issue, not authority to use. + +Where a PEP's enforcement produces a durable credential — ops-warden's SSH +certificate is the shipped case — the credential's own TTL governs it, and no +flex-auth field reaches it. A short decision lifetime bounds how long a revoked +principal can keep obtaining *new* credentials, which is the part a PDP can +bound; it does not recall one already issued. The residue between the two is the +consumer's §9.7.2 window to state, and it is correctly stated at the PEP. + +The lever that shortens the artifact's own life is the requested TTL as a policy +input, not the decision lifetime. See `FLEX-DEC-2026-004`. + ## How to read this as a consumer 1. Do not cache a verdict past `lifetime.expires_at`. @@ -70,3 +85,5 @@ A stale-directory signal from Topaz fails closed (`topaz_directory_stale`). to a fresh Check. 3. Approval revocation is a claim-class problem: stop presenting the claim, and do not replay a digest that included it. +4. Do not read `lifetime` as a bound on a credential you issued under the + allow. Bound that credential with its own TTL. diff --git a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md new file mode 100644 index 0000000..43c0d0f --- /dev/null +++ b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md @@ -0,0 +1,165 @@ +--- +id: FLEX-WP-0021 +type: workplan +title: "secrets-engine consumer policy package and cluster-local pin" +domain: infotech +repo: flex-auth +status: proposed +owner: claude +topic_slug: netkingdom +planning_priority: P1 +planning_order: 210 +depends_on_workplans: + - FLEX-WP-0012 + - FLEX-WP-0016 +related_workplans: + - SECRETS-WP-0009 +created: "2026-09-06" +updated: "2026-09-06" +--- + +# FLEX-WP-0021 — secrets-engine consumer policy package and cluster-local pin + +Opened by `FLEX-DEC-2026-005`, answering secrets-engine's two questions of +2026-09-05. Both resolve to one piece of work in one order: **publish the +package, then pin it.** + +## Why this exists + +secrets-engine has already built its side. Commit `627810b` applies flex-auth's +PDP answers — `resource.type: secret-catalog-lane`, `resource.system: +secrets-engine`, catalog id as `request.resource.id` — and validates +`ActionAuthorization.decision.binding.request_digest` against +`flex-auth.decision-record.v1` with `status: approved` inside validity and +distinct approvers. + +Two things it cannot supply itself: + +1. `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` are required + configuration with **no fallback**, deliberately. `secrets-engine.lifecycle/v1` + is example vocabulary, not a published package. Until flex-auth publishes a + real one, that configuration cannot be set to anything true. +2. There is no address for secrets-engine to call. flex-auth runs as + per-consumer cluster-local pins with default-deny ingress admitting one + approved workload each (`deploy/README.md`); no pin exists for + secrets-engine. Its 2026-09-06 probe finding no reachable PDP found the + design working, not an outage. + +Glas real-key execution (`SECRETS-WP-0009-T03`) is the workload waiting. + +## Boundary + +flex-auth authors the policy package; secrets-engine owns the action vocabulary +it encodes and the enforcement around the verdict. flex-auth never mutates the +approval object — that is `approval-engine`'s (`security-layer-model_v0.7` §9.4) +— and validates approvals only as input claims. This plan does not default, +weaken, or supply a fallback for the consumer's package pin. + +## 1. Obtain the real action vocabulary from secrets-engine + +```task +id: FLEX-WP-0021-T01 +status: todo +priority: high +``` + +Owner: `flex-auth` to request and document; `secrets-engine` owns the answer. + +- Ask secrets-engine for the authoritative action list over + `secret-catalog-lane` — the operations it will actually gate, not the + `secrets-engine.lifecycle/v1` example set. +- Record it in `docs/secrets-engine-action-vocabulary.md`, following + `docs/tenant-engine-action-vocabulary.md`. +- Record the subject classes that may appear, and which of them are service + accounts versus humans behind an approval. + +Gate: every action in the vocabulary is named by secrets-engine, not inferred +here. An inferred action is a blocker, not a default. + +## 2. Publish `secrets-engine.catalog-lane.lifecycle` v1 + +```task +id: FLEX-WP-0021-T02 +status: todo +priority: high +``` + +Owner: `flex-auth`. + +- Write `examples/secrets-engine/policy_package.md` with + `id: secrets-engine.catalog-lane.lifecycle`, `version: v1`, and an explicit + `allow_ttl`. An omitted TTL takes the 15m engine default; `none`/`0s` denies + with `allow_lifetime_unstated` rather than granting standing access. +- Add `protected_system_manifest.yaml`, `subject_manifest.yaml`, + `resource_manifest.yaml`, and `registry_snapshot.json` for the namespace. +- Add allow and deny fixtures in `policy_fixtures.yaml` plus + `check_request_*.json`, covering at minimum: an allow per vocabulary action, + wrong-tenant deny, unknown-subject deny, and an approval-claim-absent deny. +- Add `examples/secrets-engine/README.md`. + +Gate: `flex-auth validate` passes, the fixture tests pass, and the package +digest is stable across two runs. + +## 3. Confirm the digest join against a real decision record + +```task +id: FLEX-WP-0021-T03 +status: todo +priority: high +``` + +Owner: `flex-auth` to emit; `secrets-engine` to verify. + +- Emit a real `DecisionEnvelope` from the package above and hand it to + secrets-engine as the replay fixture for the join it built in `627810b`. +- Verify `binding.request_digest` reproduces as SHA-256 over canonical JSON of + tenant/subject/action/resource/context per `docs/canonical-request-digest.md`. +- Verify `provenance.registry_snapshot_digest` is present — §9.7.2 promotes it + to a conformance prerequisite and `FLEX-WP-0019` closed that gap. + +Gate: secrets-engine confirms its validator accepts the real record unchanged. +A validator change on their side is their work-record, not closed from here. + +## 4. Stand up the `flex-auth-secrets-engine` pin + +```task +id: FLEX-WP-0021-T04 +status: wait +priority: high +``` + +Owner: `flex-auth`; deployment approval required. + +- Add `deploy/flex-auth-secrets-engine.yaml` as a three-document manifest — + `Deployment`, `Service`, default-deny `NetworkPolicy` admitting ingress from + the secrets-engine workload only — following the two existing pins. +- Promote through the Railiance overlay (`railiance/app.toml`, + `charts/flex-auth`, `values/`), not by editing the flattened emergency files. +- Build in CI and deploy **by immutable digest**, never by tag. Do not roll the + other pins: they are pinned to their own digests precisely so one policy + change does not move another consumer. +- Start `callerAuth.mode` in `warn`. Flip to `enforce` only after secrets-engine + adopts a calling identity and its warn logs are clean, as `FLEX-WP-0016` did + for ops-warden. + +Gate: the pin answers `POST /v1/check` for the secrets-engine workload and +default-denies every other source; the other two pins are unchanged. + +## 5. Hand the pin coordinates back and close + +```task +id: FLEX-WP-0021-T05 +status: wait +priority: medium +``` + +Owner: `flex-auth`. + +- Reply to secrets-engine with the Service DNS, the package id and version to + set in `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION`, and the + `callerAuth` mode currently in force. +- Update `SCOPE.md` to list secrets-engine among the shipped consumers, and log + a progress event. + +Gate: secrets-engine can set its required configuration to published values and +reach a pin; nothing about the fallback-free shape of that configuration changed.