secrets-engine confirmed T03, and re-verifying against the regenerated destroy fixture found something neither repository can fix alone: a pdp_digest recorded at issue time can never equal the request_digest of a request that carries the claim in its context, because the claim is part of the context that is hashed. Embedding the claim changes the very digest the claim would need to name. Not fixture staleness. It holds for every dual-control request whose claim travels in context -- the shape GH-DEC-2026-008 had just ruled mandatory. Left unresolved that ruling was unimplementable for exactly the case it was written for, and destroy would have been permanently un-allowable in production, failing closed forever on a check that could never pass. flex-auth owns the canonical request digest, so the fix is ours. binding.approval_binding_digest is the same material with context.approval removed, emitted only when a claim was carried. An approval issued against a claim-free Check records that Check's request_digest; the claim-bearing request reproduces it here. DELIBERATELY ADDITIVE, and the reason matters. The tempting fix is to drop context.approval from request_digest entirely. That is wrong: request_digest is the replay identity, and two requests differing only in which approval was presented must not share one, because their decisions differ -- one allows, the other denies dual_control_required. Collapsing them would let an allow obtained with a valid claim be replayed against a request carrying none. So request_digest still covers the claim and still moves; approval_binding_digest deliberately does not, and is documented as not a replay identity. The tests assert the two functions DISAGREE on a claim-bearing request, which is approval-engine's formulation of how to defend a distinction that looks like duplication. The fixture now demonstrates the property rather than asserting it: its claim's pdp_digest equals the envelope's approval_binding_digest with pdp_path true, and changing the claim's contents moved request_digest while leaving approval_binding_digest untouched. Two files a consumer can diff. Also picked up approval-engine's new required binding.pdp_path via the cross-repo schema test added yesterday -- which is the test doing exactly what it was built for, one day later. T03 is done. secrets-engine's own digest-material defect, which our two real envelopes caught, is recorded in the workplan. 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
234 lines
10 KiB
Markdown
234 lines
10 KiB
Markdown
---
|
|
id: FLEX-WP-0021
|
|
type: workplan
|
|
title: "secrets-engine consumer policy package and cluster-local pin"
|
|
domain: infotech
|
|
repo: flex-auth
|
|
status: active
|
|
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"
|
|
state_hub_workstream_id: "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
|
|
|
|
```task
|
|
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
|
|
|
|
```task
|
|
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
|
|
|
|
```task
|
|
id: FLEX-WP-0021-T03
|
|
status: done
|
|
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.
|
|
|
|
**Done 2026-09-06.** `secrets-engine` confirmed both envelopes reproduce, and
|
|
running them found a defect in *their* digest join: they were hashing `id`,
|
|
`policy_version`, and `caring_context`, which the canonical digest excludes.
|
|
Their previously pinned constant had been computed with `id` in the material, so
|
|
it was wrong and its passing proved nothing — two real envelopes were what caught
|
|
it. That is the value of handing over a real record rather than a described one.
|
|
|
|
Re-verification then surfaced a structural finding resolved in
|
|
`FLEX-DEC-2026-007`: a `pdp_digest` recorded at issue cannot equal the
|
|
`request_digest` of a request carrying the claim in its hashed context.
|
|
`binding.approval_binding_digest` is now published for that comparison.
|
|
|
|
Original emission note follows.
|
|
`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
|
|
|
|
```task
|
|
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
|
|
|
|
```task
|
|
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.
|