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>
319 lines
15 KiB
Markdown
319 lines
15 KiB
Markdown
---
|
||
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 T02–T03,
|
||
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
|