secrets-engine/docs/pdp-access-path.md
tegwick 5a2099f400
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Document exact native signer and CLI admission requirements
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 10:48:40 +02:00

8.8 KiB

Reaching the access-engine pin

The supported way for this engine to reach flex-auth-secrets-engine, and the reason the address is enforced in code rather than left to configuration.

Never the Service DNS name

SECRETS_ENGINE_PDP_URL=http://flex-auth-secrets-engine.flex-auth.svc.cluster.local:8080

is refused by decision_check.require_supported_pdp_address. From a workstation that name does not fail — it resolves, to the wrong host:

$ getent hosts flex-auth-secrets-engine.flex-auth.svc.cluster.local
80.158.43.29   ...svc.cluster.local.ad.binect.de
$ getent hosts this-service-does-not-exist.flex-auth.svc.cluster.local
80.158.43.29   (identical — proves suffix expansion, not a record)

resolv.conf carries search ad.binect.de, a wildcard zone, so every *.svc.cluster.local name answers with one unrelated public address. Sending there ships the CheckRequest body and our bearer token to a third party.

As of source ff1a13d (2026-10-16), this consumer requires the producer's detached Ed25519 signature using independently admitted local keys and exact signed evaluator/mode/policy pin. The complete CLI wheel-plus-helper bundle and strict task handoff are implemented in source. Native signer custody, key/pin admission, installed placement and actual consumer receipts remain unaccepted; source enforcement does not establish those facts. See the scoped native preparation.

Historical 2026-10-12 finding: prior consumer source did not verify producer signatures, so a forger could reproduce public request digests. The literal loopback address restriction limited the destination and did not prove responder identity. That source defect is repaired; FLEX-DEC-2026-010's historical warning that an absent PDP and a lying responder require different controls remains relevant. The independently admitted native trust and rollout obligations in SECRETS-WP-0014 T02/T03 remain open.

The Check client disables automatic redirects, environment/system proxies and the process-global urllib opener. A 3xx response fails closed without contacting the redirect destination. URLs must be HTTP(S) bare origins with a literal loopback IP and optional nonzero port: no hostname (including localhost), userinfo, path prefix, query, fragment or whitespace/control characters. Existing localhost configuration must use 127.0.0.1 or [::1]. Invalid configuration is rejected before reading the bearer file. Responses are limited to 512 KiB; oversized, invalid UTF-8 and non-object JSON bodies fail closed. These rules also apply when the approval flow invokes the PDP client.

Regression tests use disposable local HTTP servers and synthetic credentials; they do not demonstrate identity or signature verification on the native pin.

Historical transport procedure and current admission gate

The following transport example records the earlier supported access shape; it is incomplete for current native use without independently admitted signing trust, the reviewed CLI bundle and exact owner configuration. It grants no token mint, installation or live action. Use literal loopback kubectl port-forward to the admitted named pod after verifying the cluster identity and pod identity. That path rides the Kubernetes API server's TLS with the operator's cluster credentials; the client cannot infer that binding from its local URL. Note this is the exact reverse of the caller direction, where port-forward bypasses the NetworkPolicy (FLEX-WP-0023); the two properties point opposite ways and summarising either as "the network protects it" gets one backwards.

# 1. Target the pod by name, not the Service.
POD=$(kubectl -n flex-auth get pods \
        -l app.kubernetes.io/name=flex-auth-secrets-engine \
        -o jsonpath='{.items[0].metadata.name}')
kubectl -n flex-auth port-forward "pod/$POD" 18080:8080 --address 127.0.0.1 &

# 2. Fresh bounded caller token per session. Audience MUST be flex-auth;
#    caller auth is `enforce` as of Helm revision 3.
umask 077
kubectl -n secrets-engine create token secrets-engine \
        --audience flex-auth --duration 10m > "$PDP_TOKEN"
chmod 600 "$PDP_TOKEN"          # outside any Git worktree

# 3. Point the engine at the forward.
export SECRETS_ENGINE_PDP_URL=http://127.0.0.1:18080
export SECRETS_ENGINE_PDP_TOKEN_FILE="$PDP_TOKEN"
export SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE=secrets-engine.catalog-lane.lifecycle
export SECRETS_ENGINE_AUTHORIZATION_POLICY_VERSION=v2

Shred the token file and stop the forward when the session ends. The token is ten minutes; it is a caller credential, not an authorization.

ServiceAccount secrets-engine/secrets-engine has automountServiceAccountToken: false and no role bindings — it exists to be a caller identity and nothing else.

Pin v2, never v1

v1 shipped and was deployed with no input.tenant rule at all; a rotate under tenant:coulomb returned allow. v2 supersedes rather than amends it so the change is visible in the version string. Current consumer configuration and signed trust must explicitly pin the admitted v2 artifact; the validator does not impose a universal hard-coded package/version default. A v1 configuration is not admitted by this proposal. See docs/tenant-alignment.md.

The digest join, and why it is not a digest comparison

Proved live on 2026-09-07, fixed the same day.

The evaluator normalizes before hashing: the request tenant is copied onto subject and resource, and a registry hit copies type, tenant and selected attributes onto the refs. So binding.request_digest is over material we did not send and cannot reproduce, and the validator's original byte-equality check against our unenriched request rejected every real allow.

flex-auth's canonical-request-digest.md already published the consumer rule, and the fix follows it rather than inventing one:

A consumer that re-hashes the original unenriched request will not match a decision that turned on registry attributes. Compare structured binding fields to the proposed action, and treat request_digest as the evaluator's statement of what it hashed. To recompute independently, hash the same normalized tuple the binding carries.

The bullets in earlier revisions described the September structured-enrichment check. Current source has since moved to the producer's submitted-request replay contract: validate_decision_envelope validates the exact submitted digest, opaque enriched digest shape, approval-free correspondence, policy pin and lifetime on an authenticated envelope. It does not independently reconstruct the full enriched tuple or native five-field claim in the generic CLI path.

The separate strict task_execution_handoff additionally recomputes the evaluated and approval-free digests, verifies exact descriptor/context/native claim joins (including the served native digest), and binds signed enforced caller provenance. That source seam is not an execute-ccr CLI command and does not admit the proposed service caller mapping or enable either task policy's runtime false gate.

The lesson worth keeping: the replay fixtures could not catch this, because _request_from() rebuilds the request from the binding, so the tests hashed the evaluator's output and compared it to the evaluator's output. Only a real request through this access path exposed it.

Mandatory responder authentication (WP14 source preparation)

Every serving authorize_action path requires SECRETS_ENGINE_PDP_TRUST_FILE and SECRETS_ENGINE_PDP_VERIFIER. These fail closed before approval/PDP credential access when a PDP path is configured. Trust must be an absolute regular JSON file, not group/world writable, admitted independently of the responder or its keys endpoint. Neither a loopback URL nor installing source admits production keys. See verifier/README.md for build, source pin/update and installation requirements.

Trust schema secrets-engine.pdp-trust.v1 requires exactly schema, evaluator, mode, policy_package, policy_version, policy_package_digest, and keys (a map of signer kid to base64url-unpadded Ed25519 public key). The producer has no separate issuer or instance identifier: issuer binding is the admitted signer plus signed provenance evaluator/mode; the exact policy coordinate and artifact digest identify the admitted pin. Deployment key custody must scope the signer to that responder; shared signing keys cannot prove a distinct pod identity.

Rotation is an explicit reviewed admission update: admit old/new keys for a bounded overlap, switch producer signer, verify the actual consumer, then remove the old kid and prove refusal. Keys are reloaded each call; no endpoint discovery, cached key fallback, unsigned exception, or automatic admission exists. These source checks do not constitute Platform key admission or native rollout acceptance.