flex-auth/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md
tegwick c3ede0b494
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Review the stance register's second row; record why T04 cannot proceed
TWO FINDINGS, BOTH FROM READING OTHER REPOSITORIES RATHER THAN OURS.

The §13.1 stance register has a second row. secrets-engine published
pep-stance.yaml -- total over catalog stage plus unknown, runtime-read
and pinned to SHIPPED_STANCE by test. SCOPE.md claimed ops-warden's was
the estate's only published map and that the register's single row was
itself the finding; that is no longer true and is corrected.

docs/stance-register-review.md is the first exercise of the
aggregate-divergence capability flex-auth claimed on 2026-08-29 and then
recorded as unexercised because one row cannot diverge from anything.
Three findings:

The two maps take opposite stances on unknown -- ops-warden fail_open by
versioned build profile under ADR-0009, secrets-engine fail_closed. Both
conformant, neither a defect, and they disagree about the one case nobody
planned for. Reported as an observation for gate-house's register, not as
a request that either repository change: a PDP does not set a consumer's
stance, and §9.3's two-owner split is our own finding.

The rows are not comparable. ops-warden scopes by security-zone,
secrets-engine by catalog-stage. §6.4 permits both, but the register
cannot then answer what the estate's stance is for a z2 workload. Worth
recording before a third row arrives.

secrets-engine's map defines fail_closed in terms of a durable
ActionAuthorization record, which GH-DEC-2026-005 shelved. The stance is
unaffected -- only the artifact name is stale -- but the file is read at
runtime and pinned by test, so the stale name outlives a comment.

SEPARATELY, T04 IS BLOCKED AND THE REASON IS STRUCTURAL. The task assumed
the ops-warden/tenant-engine/user-engine pattern, where the pin's
default-deny NetworkPolicy admits one approved consumer workload.
secrets-engine has no Kubernetes deployment at all -- it is a CLI. There
is no pod selector to write, and inventing one would repeat the error
corrected in T02. Three possible shapes recorded in the workplan and
raised with them; callerAuth, not the NetworkPolicy, becomes the real
boundary if an operator CLI is the caller, so the FLEX-WP-0016 precedent
does not transfer unexamined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
2026-09-06 09:31:40 +02:00

9.6 KiB

id type title domain repo status owner topic_slug planning_priority planning_order depends_on_workplans related_workplans created updated state_hub_workstream_id
FLEX-WP-0021 workplan secrets-engine consumer policy package and cluster-local pin infotech flex-auth active claude netkingdom P1 210
FLEX-WP-0012
FLEX-WP-0016
SECRETS-WP-0009
2026-09-06 2026-09-06 b01f655e-f71a-50ae-b110-178557f07c63

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

id: FLEX-WP-0021-T01
status: done
priority: high
state_hub_task_id: "3c191808-6fe5-5a9b-9a68-a4ce4a672dbd"

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.

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

id: FLEX-WP-0021-T02
status: done
priority: high
state_hub_task_id: "38770813-dfcc-5711-bba3-0db50d3af135"

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.

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

id: FLEX-WP-0021-T03
status: progress
priority: high
state_hub_task_id: "8f7e5cdd-777e-5f54-9b92-c72b79f65672"

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.

Emitted 2026-09-06, awaiting their confirmation. examples/secrets-engine/replay/ carries two real envelopes — a plain allow (rotate, empty context) and the dual-control allow (destroy with a valid approval-claim). All three digests plus input_claim_digests.context verified identical across two runs; id, decision_time, and the lifetime bounds move with the clock and are documented as unpinnable. Both fixtures are included because input_claim_digests.context appears only for a non-empty context, so a consumer asserting it is always present would pass on one and fail on the other.

4. Stand up the flex-auth-secrets-engine pin

id: FLEX-WP-0021-T04
status: wait
priority: high
state_hub_task_id: "f4e8709a-65dd-5172-97ae-e7c3432afb22"

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.

Blocked 2026-09-06 — the consumer is not a workload. This task was written from the ops-warden / tenant-engine / user-engine pattern, where the pin's default-deny NetworkPolicy admits ingress from exactly one approved consumer workload. secrets-engine has no Kubernetes deployment at all: it is a CLI (src/, cli.py, pyproject.toml) with no manifests, no namespace, and no pod labels. There is no selector to write, and inventing one would be the same error corrected in FLEX-WP-0021-T02 — authoring against a shape that does not exist.

Three shapes are possible and the choice is not flex-auth's alone:

  1. Operator-run CLI reaching an in-cluster pin. Needs a decided ingress path (tunnel or port-forward), and callerAuth becomes the real boundary rather than the NetworkPolicy, because the source address is an operator's machine rather than a pod.
  2. A secrets-engine workload that does not exist yet. Then this task is correct as written but waits on secrets-engine to have one.
  3. No pin at all, if the gate is only ever consulted from an operator context that already holds a decision.

Raised with secrets-engine; T04 does not proceed until the shape is decided. Note this also changes what callerAuth.mode: warn is warning about, so the FLEX-WP-0016 precedent does not transfer unexamined.

5. Hand the pin coordinates back and close

id: FLEX-WP-0021-T05
status: wait
priority: medium
state_hub_task_id: "f0828871-fd65-5d8c-adfd-28b13fedd2b0"

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.