flex-auth/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md
tegwick afd9be5aa9
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Successful in 57s
fix: the address we published was a misdirection, and the channel is unauthenticated
secrets-engine probed the Service DNS name handed over in FLEX-WP-0021-T05
and found it resolves, from the workstation, to an unrelated public host.
Reproduced here: search ad.binect.de answers wildcard, so
flex-auth-secrets-engine.flex-auth.svc.cluster.local and
this-service-does-not-exist.flex-auth.svc.cluster.local both resolve to
80.158.43.29, while the trailing-dot FQDN correctly fails. A bare Service
name in a handover is not merely unreachable from there, it is a live
misdirection, and the handover was ours.

Had a deployment pointed at it, the CheckRequest body would have gone to
that host: subject, tenant, lane and resource ids, stage, field names,
purpose, plus the caller's bearer token.

Trailing-dot FQDN and "in-cluster only" now replace the bare name in the
example README, SCOPE.md, and the T05 note.

Their real question was how the response channel is authenticated, and
they declined to answer it locally because choosing a transport control
for our service is not a consumer's call. Right boundary, so the answer
is recorded here as FLEX-DEC-2026-010: it is not authenticated. Pins
serve plain HTTP, the envelope carries no signature, and a responder that
knows the package id and version can return a well-formed allow that
passes every check a consumer performs.

The part worth stating in the contract is that the digests do not help
and look like they do. Every input to request_digest,
policy_package_digest and registry_snapshot_digest is either sent by the
caller or published in this repo, so a forger reproduces all three
exactly. They establish integrity of the binding, never authenticity of
the source — and publishing more digests makes a forged envelope look
more authenticated, not less.

For secrets-engine specifically: fail-closed protects against a PDP that
is absent, not against one that lies. An unreachable PDP denies; a lying
PDP allows.

Third instance of one seam in three decisions. 008: a tenant carried
into the digest and never compared — visible, not enforced. 009: a caller
authenticated and never recorded — enforced, not visible. 010: a record
verifiable and unauthentic — checkable, but not evidence.

One nuance that changes the operator recommendation: kubectl port-forward
does authenticate the responder, transitively — no DNS name, one named
pod, API-server TLS. That is the exact reverse of the caller direction,
where it bypasses the NetworkPolicy. Independent properties pointing
opposite ways, so neither can be summarised as "the network protects it".

FLEX-WP-0024 carries signing; key custody routes through warden/OpenBao
rather than minting a key here.

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
2026-09-06 22:44:45 +02:00

15 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 finished 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.

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

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

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

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:

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)

Corrected 2026-09-06: the address originally handed over was a bare Service name, which is a live misdirection from a workstation. secrets-engine probed it and flex-auth reproduced: search ad.binect.de expands any *.svc.cluster.local name to one unrelated public host, including names for services that do not exist. The trailing dot is required and the address is in-cluster only. FLEX-WP-0024 carries the consequence — a published address a consumer can be misdirected from, reaching an unsigned decision envelope over plain HTTP.

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.

v2 production correction applied — 2026-09-06

Glas applied the existing operator production authorization to correct the reported v1 tenant fail-open. Helm revision 2 uses CI main-d98323b digest sha256:db1c4f7e621c7ea119489a321d7db0e05da09afc17be5f69d873b2b3c7f60cfc. Lint/server dry-run/rollout passed. Six published Check requests on the live service returned expected two allows/four denies, including wrong_tenant; all matched_policy_version values are v2. Other consumer Deployment specs are unchanged. Values pin updated. Caller authentication remains warn; workstation consumer access/adoption remains outstanding despite T05 closure. Glas has sent a follow-up requesting an explicit live owner work record for that gate. Do not roll back to known over-permissive v1; stop this dedicated release if v2 cannot be served.