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
301 lines
14 KiB
Markdown
301 lines
14 KiB
Markdown
---
|
|
id: FLEX-WP-0021
|
|
type: workplan
|
|
title: "secrets-engine consumer policy package and cluster-local pin"
|
|
domain: infotech
|
|
repo: flex-auth
|
|
status: finished
|
|
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.
|
|
|
|
**Reopened and re-closed 2026-09-06 at v2 — the stated gate had not been met.**
|
|
This task's gate named "wrong-tenant deny" among the required fixtures and the
|
|
package shipped without one, because it had no tenant rule at all: a `rotate`
|
|
under `tenant: tenant:coulomb` returned `allow` against the deployed v1
|
|
(`decision:066e629bbf0c0924`). Found while answering `glas-harness`'s
|
|
tenant-alignment request, which had asked for exactly that evidence. v2 adds
|
|
`wrong_tenant` above `wrong_system`, three Rego tests and three fixtures
|
|
(28/28 and 32/32 passing), and supersedes v1 rather than amending it because
|
|
the defect failed **open** — see `FLEX-DEC-2026-008`. The `T03` replay digests
|
|
are unchanged at v2; only `policy_version` and `policy_package_digest` moved.
|
|
The sweep this prompted found the same shape in `tenant-engine`
|
|
(`FLEX-WP-0022`).
|
|
|
|
## 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: done
|
|
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: done
|
|
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.
|
|
|
|
**Done 2026-09-06, with one coordinate that is not flex-auth's to supply.**
|
|
Handed back to `secrets-engine` and `glas-harness`:
|
|
|
|
```text
|
|
Service: http://flex-auth-secrets-engine.flex-auth.svc.cluster.local:8080
|
|
Package: secrets-engine.catalog-lane.lifecycle
|
|
Version: v2 <- not v1; v1 is deployed and superseded
|
|
callerAuth.mode: warn (not enforced caller authentication)
|
|
```
|
|
|
|
`SCOPE.md` lists secrets-engine among the shipped consumers and
|
|
`examples/secrets-engine/README.md` carries the coordinates at source.
|
|
|
|
Two things are stated rather than closed, because closing them is not ours:
|
|
|
|
1. **The pin serves v1 until a redeploy lands.** The over-permissive package is
|
|
live now. Deployment approval is the user's; `FLEX-DEC-2026-008` records the
|
|
defect and does not grant it.
|
|
2. **There is still no verified access path for a workstation CLI.** Ingress
|
|
admits namespace `secrets-engine` with pod label
|
|
`app.kubernetes.io/name=secrets-engine`; a CLI on an operator's machine is
|
|
not that, and Service DNS is not workstation connectivity. `T04`'s three
|
|
shapes remain undecided, and `glas-harness` named the same gap independently.
|
|
`secrets-engine` cannot complete adoption on coordinates alone.
|
|
|
|
The fallback-free, fail-closed shape of
|
|
`SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` is unchanged.
|
|
|
|
## Production deployment — 2026-09-06
|
|
|
|
User authorized production deployment in the Glas session. Installed dedicated
|
|
Helm release `flex-auth-secrets-engine`, namespace `flex-auth`, revision 1,
|
|
using values/secrets-engine.yaml. CI tag main-dd3ce4c resolves to immutable
|
|
digest sha256:89086c02c74a931068423e70937d03df7850fa0db9c63e70be56b3558f1756af.
|
|
Chart lint and server dry-run passed; deployment available 1/1. Existing three
|
|
consumer deployment specifications were compared before/after and unchanged.
|
|
|
|
Five published requests returned two allows and three expected denies via
|
|
local port-forward. Network probes: namespace secrets-engine with pod label
|
|
app.kubernetes.io/name=secrets-engine succeeded after initial propagation
|
|
retry; wrong pod label and wrong namespace remained denied across retries.
|
|
All six temporary probes and the port-forward were removed. Created the
|
|
secrets-engine namespace for the intended consumer identity; no workload or
|
|
credential was installed there.
|
|
|
|
Service: http://flex-auth-secrets-engine.flex-auth.svc.cluster.local:8080.
|
|
Policy: secrets-engine.catalog-lane.lifecycle / v1. Caller authentication is
|
|
warn, per this task's rollout plan; this is not enforced caller authentication.
|
|
Network ingress admits only the specified namespace/pod selector. T05 remains
|
|
waiting on consumer configuration/adoption and the owner handoff. Approval
|
|
service, KeyCape clients and real credential-lane activation are not supplied
|
|
by this deployment. First-install rollback is removal of this dedicated Helm
|
|
release, leaving the three existing consumers untouched.
|