zone-engine/workplans/ZONE-WP-0001-security-zone-model.md
tegwick 9de50d3a48 ZONE-WP-0001: flex-auth amendments as the consuming PDP
Reviewed as the only policy decision point. Four task amendments:

T01 - the 'only PDP' invariant guards latency, not authority. Compiled
data that determines an outcome still decides. Tightened wording:
zone-engine owns membership, flex-auth policy owns stance.

T03 - split membership from stance rather than rejecting option (c)
wholesale. Stance for the pre-sign gate belongs in the policy package
because registry content is absent from decision provenance. Also:
fail-open is a PEP property and no PDP can express it.

T05 - no registry schema change needed; metadata/attributes already
flatten into rego input. But trust_zone is a live name collision -
a hardcoded 'platform' constant no policy reads.

T04 - flex-auth loads registry and policy once at process start with
no reload path, so an inert compiled exception expires only by human
redeploy. Enforced expiry requires a not_after evaluated in rego.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:59:47 +02:00

319 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: ZONE-WP-0001
type: workplan
title: "The security zone model — derive it from the estate, draft it as canon"
domain: infotech
repo: zone-engine
status: proposed
owner: zone-engine
topic_slug: netkingdom
planning_priority: P1
created: "2026-08-19"
updated: "2026-08-19"
state_hub_workstream_id: "a75c8c9c-c108-4831-8d72-4343cce49879"
---
# ZONE-WP-0001 — The security zone model
Ported from `WARDEN-WP-0032` (ops-warden), which deferred `policy.enabled: true`
under `ADR-0006` and named this repo as its successor. ops-warden keeps only its
consumer-side task.
**This workplan produces a model and a canon draft. It does not produce an API,
a schema, or storage.** An engine that ships a schema before it has partitioned
the real estate defines the model by accident, through whatever its first wire
format happened to be. Whether a runtime is warranted is an *output* of T02T03,
not an assumption of T01.
## The gap in one table
| Axis | Shipped | Answers |
| --- | --- | --- |
| Environment posture (`dev`/`test`/`prod`) | ops-warden WP-0015 | Where does this run? |
| Workload maturity (`M0``M3`) | ops-warden WP-0015 | How ready is this workload? |
| `organization_posture: build` | ops-warden WP-0029 | What state is the org in? |
| **Enforcement stance** | **missing** | **Is this control enforced here, and what happens when it fails?** |
The first three describe. Nothing decides. That is why `policy.enabled` is a
repo-wide boolean, and why turning it on would have hardened the access needed
to perform the refactors currently underway.
## Do not reinvent these
**`net-kingdom/canon/standards/tenancy-posture_v0.1.md`** — the structural model
to follow: orthogonal axes, graduated levels, a per-repo declaration
(`tenancy.yaml`) with current/implemented/target vectors, evidence, and a review
date; §6 *conformance is accuracy, not altitude*; §14 adoption stance
*structure, not tooling*. That last one is why this workplan is shaped as it is.
**Reefs** (`repo-manager/docs/RailianceAppDeploymentGuide.md`) — `reef-railiance`
carries a single-node/shared-control-plane residual risk requiring explicit
acceptance. Substrate placement with risk attached is *adjacent* to a zone and
must be reconciled with it, not duplicated. repo-manager's own rule — *topology
is not readiness* — restates here as **placement is not posture**.
**flex-auth** — the PDP, `service_class: latency-critical`, already consuming a
compiled registry snapshot built by
`ops-warden/scripts/build_flex_auth_registry.py`. Zone membership reaches the
decision point by **compilation**. A synchronous zone lookup in the decision
path would be a regression dressed as an architecture.
> **flex-auth reviewed this workplan on 2026-08-19** as the PDP that would have
> to live with the result, and amended T01, T03, T04 and T05 in place. The three
> substantive changes: the "only PDP" invariant needs to bind *authority* and not
> only latency (T01); membership compiles to the registry but **stance belongs in
> the policy package**, and fail-open is not expressible by a PDP at all (T03);
> and flex-auth has **no reload path**, which decides how a time-boxed exception
> can possibly expire (T04). Membership itself needs **no registry schema
> change** — but do not land it on the existing `trust_zone` field (T05).
**`warden plan`** (ops-warden WP-0029) — already returns
`autonomous`/`founder_required`/`unroutable` from properties of the thing, with
the `reasons` that produced the verdict. Zone-aware enforcement extends that
machinery; it does not grow a second vocabulary beside it.
## Tasks
```task
id: ZONE-WP-0001-T01
status: todo
priority: high
state_hub_task_id: "ea74316e-b711-4888-9b38-553ebc9ad856"
```
**Confirm ownership on the record, and say what publishes where.** This repo was
seeded on a recommendation, not yet on agreement. Take to `net-kingdom` (canon)
and `flex-auth` (the enforcing PDP): zone-engine owns the model and the
exception lifecycle; canon publishes the standard, in the family of
`tenancy-posture_v0.1` and the `*-engine` boundary contracts; flex-auth stays
the only PDP and consumes compiled membership.
If either disagrees, the model moves and this repo is archived rather than kept
for its own sake. Record the outcome either way.
**flex-auth answered 2026-08-19 (amended by flex-auth).** Confirmed: zone-engine
owns zone identity, membership and exception lifecycle; flex-auth stays the only
PDP. But the invariant as written in `GOAL.md` guards the wrong property. *"Never
a synchronous lookup in a decision path"* is a **latency** guarantee. It does not
guarantee flex-auth remains the only decision point, because compiled data that
determines an outcome is still deciding — it just decided earlier. Under the
invariant as written, zone-engine could compile `enforced: false` for a lane and
flip `warden sign` from deny to allow without a flex-auth policy change, and
still be literally compliant.
The invariant flex-auth confirms is the tighter one:
> zone-engine is the authority over zone **identity and membership**. The
> **effect** of a zone on any decision flex-auth renders is expressed in a
> flex-auth policy package — versioned, fixtured, tested, and stamped into the
> decision record. Membership is data and is compiled. Stance is a rule and is
> policy.
Rationale is in T03. Please carry the tightened wording into `GOAL.md`; the
current wording is not wrong, it is just not load-bearing.
```task
id: ZONE-WP-0001-T02
status: todo
priority: high
state_hub_task_id: "f1016c71-ccb0-46a7-9143-e81c7ac34cad"
```
**Derive the zones from the real estate.** Walk ops-warden's 27 catalog lanes,
the `adm`/`agt`/`atm` actor inventory, and the workloads carrying posture
descriptors. The model's test is that it partitions what exists without a
residue of unexplained exceptions — and the exceptions found are the most
informative output, not noise to tidy away.
Answer explicitly:
- Is a zone a property of the **actor**, the **lane**, the **workload**, or the
**moment**? The CoulombCore decommission and the issue-core move show the last
is real and hardest to model. A time-boxed relaxation is a *different object*
from a standing zone; conflating them yields a permanent hole with a
temporary-sounding name.
- Do zones **compose with or fold in** the three existing axes? A fourth
independent axis multiplies states; absorbing `organization_posture` may be
the honest move.
- How do zones relate to **reefs**? State it in the model rather than leaving
readers to guess.
```task
id: ZONE-WP-0001-T03
status: todo
priority: high
state_hub_task_id: "7f47b750-1ae4-467d-b3c4-064f4ec07dbd"
```
**Model stance and failure mode, per control per zone.** Enforced / advisory /
exempt, *and* fail-closed or fail-open. The failure mode is where the bite is:
`fail_closed` is what turns a dead tunnel into an outage, and is precisely why
the ops-warden flip was deferred.
Controls in scope at minimum: the flex-auth pre-sign gate (`policy.enabled`,
`fail_closed`), the agent read-boundary on `risk: high` lanes (ops-warden
`ADR-0004`), and the `warden plan` escalation verdicts.
**Amended by flex-auth 2026-08-19 — split membership from stance.** ops-warden's
option (c) ("zones as policy-package data") was rejected wholesale because the
model must govern two controls flex-auth does not evaluate. flex-auth agrees with
the premise and disagrees with the conclusion: (c) was rejected at the wrong
granularity. *Where a stance is evaluated belongs with the control's enforcement
point*, and there are three enforcement points here, not one:
| Control | Enforcement point | Where its per-zone stance belongs |
| --- | --- | --- |
| flex-auth pre-sign gate | flex-auth (PDP) + `warden sign` (PEP) | **flex-auth policy package** |
| Agent read-boundary (`ADR-0004`) | ops-warden CLI | ops-warden — flex-auth never sees it |
| `warden plan` escalation verdicts | ops-warden CLI | ops-warden — flex-auth never sees it |
So (b) is right for two of three controls and wrong for the one that is
flex-auth's. Concretely, for the pre-sign gate:
- **Membership → compiled registry.** Which zone an actor/lane is in is data,
varies per resource, changes as the estate changes, and is already produced by
`build_flex_auth_registry.py`. Compile it.
- **Stance → policy package.** "In zone X the gate is advisory" is a rule about
outcomes. It belongs in the artifact that already carries fixtures, rego tests,
`Validate()`, a version, and a status.
The concrete defect in compiling stance: every flex-auth decision envelope stamps
`matched_policy_version`, `policy_package` and `policy_status`, and the decision
id is derived from policy metadata plus request
(`internal/decision/engine.go:358`). Registry content is **not** in that
provenance. If zone stance lives in the registry, a stance change alters the
verdict while the decision record still reports the same policy version — two
decisions, same version, same request, different effect, and nothing in the audit
trail explains why. That breaks flex-auth's reproducibility contract for exactly
the control this workplan exists to make safe.
**Second correction: flex-auth cannot express fail-open.** A PDP returns an
effect. "Fail-open" describes what the caller does when flex-auth is
*unreachable* — that is a property of ops-warden's client (`fail_closed` in
`warden.yaml`), and no zone data compiled into flex-auth can influence it. Model
the failure-mode axis as **PEP-side**, owned by each consumer's declaration, not
as something the PDP carries. `enforced`/`advisory`/`exempt` *is* expressible by
flex-auth (as effect plus an advisory annotation in the envelope); fail-open is
not, and a model that implies otherwise will be built and then not work.
```task
id: ZONE-WP-0001-T04
status: todo
priority: high
state_hub_task_id: "1c542ca5-2b82-4778-a3e4-c76e932423c9"
```
**Design the time-boxed exception — this is the load-bearing task.** A standing
classification needs no engine; canon plus a declaration file covers it. An
exception with an expiry is *state*, and state wants an owner. Whether this repo
earns a runtime is decided here.
Must specify: who may grant, against which zone and control, for how long, with
what recorded justification; how expiry is **enforced rather than intended**;
how an active exception is visible to someone who did not grant it; and what
happens at expiry — auto-revert, or block until renewed.
If T04 concludes the lifecycle can live as reviewed declarations in git, say so
plainly and drop the runtime. That is a valid and cheaper outcome.
**Hard constraint from flex-auth 2026-08-19 — the PDP has no reload path.**
`cmd/flex-auth/main.go:447` calls `registry.LoadFile` once at process start.
There is no watcher, no SIGHUP, no reload endpoint. Both the registry snapshot
*and* the policy package are baked into the container image and the production
pins are digest-pinned (`values/ops-warden.yaml`,
`sha256:138aa347…`). Changing either means: rebuild image → new digest → helm
upgrade → pod restart.
This is the input T04 most needs, and it kills one design outright:
- **An exception compiled as inert registry data cannot have an enforced
expiry.** It expires when a human rebuilds and redeploys. That is expiry by
intention — precisely what this task says it must not be.
- **An exception carrying its own `not_after` timestamp, evaluated against
decision time in rego, does have an enforced expiry.** OPA has a clock
(`time.now_ns()`); the grant costs a redeploy but the *lapse* is automatic and
needs nobody. The zone widens by deploy and narrows by itself, which is the
correct asymmetry.
That is a third argument for stance-in-policy independent of the audit one in
T03, and it also bears on whether this repo earns a runtime: if expiry is
evaluated at decision time from a compiled timestamp, the lifecycle needs a
*record and a review*, not a service. flex-auth's read is that this points at the
cheaper outcome, but it is T04's call.
```task
id: ZONE-WP-0001-T05
status: todo
priority: medium
state_hub_task_id: "f4556ca0-cb26-46ed-9351-d00270bd2309"
```
**Declaration format and conformance rule.** Follow `tenancy.yaml`: per-repo,
with evidence, `reviewed`, and `review_due`; carry over *accuracy, not
altitude*. Include how a zone assignment changes and how that change is
observed — a zone that can be quietly widened is not a boundary.
**Two carrier facts from flex-auth 2026-08-19 (no schema change needed).**
Compiled zone membership is expressible in flex-auth's registry format **today**,
with zero Go changes. Every registry entity carries `metadata`; `api.Resource`
additionally carries `labels`, `owner`, `attributes` and `trust_zone`, and
`internal/decision/engine.go:275` flattens all of them into
`input.resource.attributes` before evaluation. `subject.metadata` and
`subject.claims` are flattened the same way (`engine.go:257`). A compiler writing
`"security_zone": "..."` into a resource's `attributes` makes it readable from
rego as `input.resource.attributes.security_zone`. Nothing to build.
Two things to decide anyway, and both belong in this task:
1. **Do not reuse `trust_zone` — this collision has already happened inside the
PDP.** `api.Resource.TrustZone` is a first-class field, is surfaced into rego
input, and `ops-warden/scripts/build_flex_auth_registry.py:77` sets it to the
hardcoded constant `"platform"` on every ssh-certificate resource. No policy
package reads it. It is a dormant, plausibly-named, semantically-empty field
sitting exactly where a reader would expect zone membership to go. `SCOPE.md`
warns that "zone" is overloaded and names network segmentation as the hazard;
the actual live collision is this one. Name the new concept `security_zone`
explicitly, or deliberately repurpose `trust_zone` and say so loudly — but do
not let both exist meaning different things.
2. **Say whether membership rides the subject or the resource.** In the
ops-warden registry an actor appears as *both*: as a subject
(`agt-state-hub-bridge`) and as a per-actor resource
(`ssh-cert:actor/agt-state-hub-bridge`). If T02 concludes a zone is a property
of the actor, the compiler must be told which record carries it — or write
both and make that the contract. Leaving it implicit produces two compilers
that disagree. This is a compiler contract, not a schema change.
```task
id: ZONE-WP-0001-T06
status: todo
priority: medium
state_hub_task_id: "8a000c18-6ee7-4328-99c7-53c490435316"
```
**Draft the canon standard and offer it.** `security-zones_v0.1.md` for
`net-kingdom/canon/standards/`, drafted here, published there. Owner-driven
landing, as ops-warden's WP-0015 maturity model was.
```task
id: ZONE-WP-0001-T07
status: todo
priority: medium
state_hub_task_id: "c7ea24df-8aa9-4ead-ba3a-033aa5201c21"
```
**Prove it on two consumers.** A model only its author honours is not adopted.
The first is ops-warden: retire `policy.enabled` for a zone-aware control,
closing `WARDEN-WP-0031-T05`. Re-run
`ops-warden/scripts/check_policy_caller_identity.py` first — the 2026-08-19
evidence (`decision:f3f7c88f9585582a`) will be stale, and re-establishing it is
cheap by design. The second consumer comes from T01.
## Related
- ops-warden `ADR-0006` — enforcement is zone-scoped, never a global flag
- ops-warden `WARDEN-WP-0032` — the ops-warden-side stub this was ported from
- ops-warden `WARDEN-WP-0031` — the deferred flip, and the readiness evidence
- flex-auth `FLEX-WP-0016` — the enforcing pin with no enforcing consumer
- `net-kingdom/canon/standards/tenancy-posture_v0.1.md`
- `repo-manager/docs/RailianceAppDeploymentGuide.md` — reefs