diff --git a/SCOPE.md b/SCOPE.md index e6bb823..7698b44 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -135,8 +135,15 @@ call itself. `ADR-0009` retires `policy.enabled` and the global `policy.fail_clo outright and replaces them with a total per-zone stance map in the consumer PEP (`ops-warden/pep-stance.yaml`): open for `z0`-`z2` and `unknown`, closed for `z3-critical`. That map is the reference shape for `security-layer-model_v0.7` -§6.4 obligation 3, and it is the estate's only published one — §13.1's register -has one row, which the standard records as itself the finding. +§6.4 obligation 3. It is no longer the only published one: `secrets-engine` +published `pep-stance.yaml` (total over catalog stage plus `unknown`, runtime-read +and test-pinned), so §13.1's register now has **two rows** rather than the one the +standard recorded as itself the finding. flex-auth reviewed the pair in +`docs/stance-register-review.md` — the first exercise of the aggregate-divergence +capability it claimed on 2026-08-29 — and found that the two maps take opposite +stances on `unknown` (`fail_open` vs `fail_closed`) and scope on different axes +(security-zone vs catalog-stage), which makes the rows conformant but not +comparable. flex-auth reviewed the zone model as the PDP on 2026-08-19 and holds one boundary: zone **membership** compiles into diff --git a/docs/stance-register-review.md b/docs/stance-register-review.md new file mode 100644 index 0000000..5e6ec31 --- /dev/null +++ b/docs/stance-register-review.md @@ -0,0 +1,101 @@ +# Stance-register review — the register has a second row + +Status: published +Date: 2026-09-06 +Standard: `security-layer-model_v0.7` §6.4 obligation 3, §13.1 +Reviewer: flex-auth (Engine / PDP) + +## Why flex-auth is writing this + +In the 2026-08-29 alignment review flex-auth told gate-house it is "the +repository positioned to notice when the aggregate of consumer stances diverges +from what the policy packages say," and then recorded that the claim was sound +but unexercised: §13.1's register existed with **one row**, and one row cannot +diverge from anything. + +It now has two. This is that claim being exercised for the first time, and +flex-auth is reporting a divergence it found rather than waiting to be asked. + +## The two published maps + +| | `ops-warden` | `secrets-engine` | +| --- | --- | --- | +| File | `pep-stance.yaml` | `pep-stance.yaml` | +| Rule of record | `ADR-0009` | `INTENT.md` / `layer.yaml` | +| Protected action | SSH certificate issuance | OpenBao metadata/KV lifecycle and delivery | +| Scope axis | `security-zone` | `catalog-stage` | +| Pinned to code by test | yes (`PolicyConfig.failure_modes`) | yes (`SHIPPED_STANCE`) | + +Both are §6.4-conformant. Both are total over their axis, both publish rather +than bury the map, both carry no implicit default, and both are asserted equal +to the shipped value by a test — which is the property that makes a published +map worth relying on at all. + +## Finding 1 — the two maps take opposite stances on `unknown` + +| | `unknown` | +| --- | --- | +| `ops-warden` | `fail_open` — explicit, versioned build profile, `ADR-0009` | +| `secrets-engine` | `fail_closed` | + +Both are defensible and neither is a defect. `ops-warden`'s `unknown` is a +build-profile artifact on a path whose z3 rows are closed; `secrets-engine` +treats an unmapped stage as production-shaped. But the estate's only two +published maps disagree about the case that, by construction, is the one nobody +planned for. + +That is the aggregate divergence flex-auth said it would notice. It is reported +as an observation for gate-house's register, **not** as a request that either +repository change. A PDP does not get to set a consumer's stance — §9.3's +two-owner split is flex-auth's own finding and it cuts this way too. + +What is worth deciding, and it is gate-house's to decide, is whether §6.4 should +say anything about `unknown` specifically. Today it requires only that the map +be total; totality is satisfied by either answer. + +## Finding 2 — the rows are not comparable, and the register cannot say so + +`ops-warden` scopes by `security-zone`; `secrets-engine` scopes by +`catalog-stage`, explicitly "the equivalent scope until security-zone +membership arrives as a claim on the decision." §6.4's "per zone or equivalent" +permits both. + +The consequence is that the register cannot answer the question an inventory +exists to answer. "What is the estate's stance for a `z2-protected` workload?" +has no answer for `secrets-engine`, because that repository has no zone axis — +and the mapping between a catalog stage and a zone is not published anywhere. + +This is a property of the register rather than a fault in either map. It is +worth recording before a third row arrives, because the cost of two +incommensurable axes is small and the cost of five is not. + +flex-auth holds a relevant boundary here, unchanged from 2026-08-19: zone +**membership** compiles into the registry snapshot flex-auth already consumes, +while per-zone **stance** belongs to the consumer. If zone membership becomes a +claim on the decision, `secrets-engine`'s axis can converge onto zones without +either side inventing a mapping. + +## Finding 3 — `secrets-engine`'s map cites a shelved artifact + +`pep-stance.yaml` defines `fail_closed` as "no protected side effect without a +durable access-engine / `ActionAuthorization` record." + +`ActionAuthorization` was shelved on the PEP consumption path by +`GH-DEC-2026-005`, which flex-auth accepted in `FLEX-DEC-2026-006` — arguing +against its own proposal. The step-1 artifact is `approval-engine`'s +approval-claim; the step-2 artifact is flex-auth's `DecisionEnvelope`. + +The stance itself is unaffected: `fail_closed` still means no protected side +effect without a durable record, and the records exist. Only the name of the +artifact is stale. Because this file is read at runtime and pinned by test, the +stale name is more durable than a comment would be, which is why it is worth a +line rather than being left. + +## What flex-auth is not claiming + +- No stance is wrong. flex-auth cannot express fail-open at all, and does not + get to grade a consumer's residue. +- No change is requested of `ops-warden`. Its `unknown: fail_open` is + documented, versioned, and reasoned. +- This is not a §13.1 register. gate-house owns the register; this is one + reviewer's reading of the two rows now in it. diff --git a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md index 79c0056..2828a50 100644 --- a/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md +++ b/workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md @@ -178,6 +178,29 @@ Owner: `flex-auth`; deployment approval required. 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