zone-engine/workplans/ZONE-WP-0001-security-zone-model.md
codex 44cd1fc231 fix(workplans): adopt ADR-007 derived identifiers
Records absent from central carried random pre-ADR-007 identifiers minted by
the retired local hub, which C-06 refused as stale references. Deriving from
the canonical record id takes no identity from anything.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 19:31:02 +02:00

693 lines
38 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: finished
owner: zone-engine
topic_slug: netkingdom
planning_priority: P1
created: "2026-08-19"
updated: "2026-08-22"
state_hub_workstream_id: "84db5351-3813-5059-9462-4a4fc43614fe"
quality_dod: DoD-Ok
quality_dod_at: "2026-08-22"
quality_dod_by: codex
quality_dod_note: "All tasks are done; canon is published, two owner declarations resolve successfully through a third repo, both consumer suites pass, live caller identity is re-proven, and the no-runtime decision is recorded."
---
# 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: done
priority: high
state_hub_task_id: "297b714c-4aac-5d63-a56f-f3cf83ef038c"
```
**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.
**Done 2026-08-22.** Both required owners answered on the record below. Their
binding decisions have been carried into `GOAL.md` and `SCOPE.md`: zone-engine
owns identity, membership, and the exception lifecycle; net-kingdom publishes
the canon standard; flex-auth remains the only PDP; membership is compiled data
and stance is owned by the policy or enforcement point that renders the effect.
**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.
**net-kingdom answered 2026-08-19 (amended by net-kingdom, as canon owner of
`tenancy-posture_v0.1`).** Confirmed on both counts, and one part of the
question is taken off this repo.
**1. Enforcement stance is a separate standard, not a seventh axis of
`tenancy-posture`.** Recorded as that standard's Decision 5.6 (draft-9). The
reason is structural, not territorial, and it matters for how the zone model is
shaped:
- **Every one of the six axes is monotone** — higher is stronger and higher is
what a service wants. `current`/`target`/`gap`, §12's *improve* and *guard*
steps, and §6's special allowance for a permanently-low level all depend on
it. **Enforcement stance is not monotone.** `ADR-0006` is precisely the
finding that the top rung is the wrong answer for the SSH lane the tunnels
depend on. A ladder whose top is sometimes wrong is an enumeration, and
embedding one in the vector would break the six that are ladders. §8.3
refused a QoS axis on a weaker version of this test.
- **`tenancy-posture` is descriptive; stance is prescriptive.** §6 is *accuracy,
not altitude*, and it is safe only because the framework blocks nothing by
itself. Prescription enters through Decision 8.2, where a **requirer** sets a
minimum and a machine reconciles it against declarations. Fold stance in as an
axis and an accurately declared `exempt` becomes conformant *and* exempt — a
conformance rule handing out the exemption it exists to audit.
This is the same split flex-auth argued in T03, arrived at from the canon side:
**membership is data and is declared; stance is a rule and belongs to the
control's owner.** Membership behaves like a level. Stance behaves like a tier
minimum.
**Binding on the declaration format (this pre-empts part of T05).** To avoid the
parallel-framework outcome this message was sent to prevent, canon rules that
**zone membership is declared in `tenancy.yaml` under a reserved top-level
`zones:` key** — sibling to `tenancy:` and `provider:`, never inside
`tenancy.current`. §5.4 makes that file the repo's single posture declaration
surface and a second root file recreates the divergence §5.4 ended. The key is
already reserved in `canon/schemas/tenancy-posture_v0.1.schema.json` and is
deliberately unconstrained there, so the validator will not reject a combined
declaration while `security-zones_v0.1` is still being drafted. One file, one
review cadence, one validator; two standards, because they have different owners
and different conformance semantics.
**On `organization_posture` (asked in T02 and in `WARDEN-WP-0032-T01`):** it does
not belong in a per-repo declaration under either key. It is a fleet-wide,
time-varying scalar describing the *estate*, not a property of the declaring
service, and a per-repo copy of a global goes stale in as many places as there
are repos. Read it as an **input** to stance selection; do not absorb it.
**2. Reefs — `zone-engine` is right, and the reconciliation is not yours.**
*Placement is not posture* is correct and should stay. But the pairing has been
misidentified: the unreconciled pair is not zone ↔ reef, it is **reef ↔ `P` and
`V`**, and it is a defect in `tenancy-posture`, not a gap in the zone model.
`P` grades tenant data isolation *within a datastore*; a reef is a named compute
substrate with an accepted residual risk, and there is no rung for that. The
live consequence is on `V`: `reef-railiance` is single-node with a shared
control plane, which caps `V` for everything bound to it under Decision 4.6.1,
and nothing joins those facts today. That is Decision 5.5's provider-declaration
finding one layer down — a reef is a provider with nowhere to state what it
makes reachable.
Recorded as `tenancy-posture` Decisions 8.4.1 and 8.4.2 and taken as
`net-kingdom`'s `NK-WP-0027`. **`ZONE-WP-0001` is not blocked on it.** Delete the
reef question from T02's list of things this model must answer; citing
Decision 8.4.1 is sufficient.
One further canon ruling, because three repos had written the same rule in three
vocabularies — *topology is not readiness* (`railiance-master`), *placement is
not posture* (here), and §3.1's naming requirement. Decision 8.4.1 now states it
once: **the substrate a workload sits on is never, by itself, evidence for a
level on any ladder.** Cite it rather than restating it locally.
**Ownership confirmed.** `zone-engine` owns zone identity, membership and the
exception lifecycle. Canon publishes `security-zones_v0.1.md` beside
`tenancy-posture_v0.1` and the `*-engine` boundary contracts, owner-driven, as
WP-0015's maturity model was. The repo is not archived.
Carry into `GOAL.md`: the tightened flex-auth invariant, and that the declaration
surface is `tenancy.yaml`'s `zones:` key rather than a new file.
```task
id: ZONE-WP-0001-T02
status: done
priority: high
state_hub_task_id: "fa30ca6b-368e-53bf-9314-21f52bcf959b"
```
**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.
- ~~How do zones relate to **reefs**?~~ **Struck by net-kingdom 2026-08-19.**
Not this model's question. Cite `tenancy-posture` Decision 8.4.1 — substrate
location is never by itself evidence for a level — and stop there. The real
unreconciled pair is reef ↔ `P`/`V` and it is canon's, tracked as `NK-WP-0027`.
- ~~Do zones **compose with or fold in** `organization_posture`?~~ **Answered by
net-kingdom 2026-08-19:** do not fold it in. It is a fleet-wide time-varying
scalar, not a per-service property; consume it as an input to stance selection.
The *other* two axes (environment posture, `M0``M3`) remain an open question
for this task — they are per-workload and genuinely composable.
**Done 2026-08-19.** Full result: `docs/estate-partition-2026-08-19.md`.
> **Corrected the same day by operator direction, recorded in §7 of that
> document.** Policy is about the **workload**, not the lane; the software repo
> only *suggests*; the workload and its responsible party declare the scrutiny;
> and a zone **requires** a standard for admission — a workload qualifies to run
> in a zone rather than being labelled with one. This is `tenancy-posture`
> Decision 8.2's tier-minimum mechanism, which Decision 5.6 had already said
> stance would behave as. The four bands below survive as membership *inputs*,
> not as zones.
Four bands, each of the 27 lanes in exactly one — **Decided** (1, the only lane
ops-warden decides), **Fronted** (10, owner has a front door), **Covered** (12,
ops-warden proxies what it does not own), **Signposted** (4, routed, nothing
flows). Derived from declared fields, not invented.
The discriminator is `delegation.mode`, not `risk`: 10 of 11 high-risk lanes are
interim and 8 of those are `exec_capable`. A field added to answer a governance
question predicts operational danger better than the one named for it.
Three results that bind the later tasks:
- **Stance is per control per zone, never per zone alone.** The three existing
controls cut the estate differently — pre-sign gate 1 lane, fetch gate 13,
read-boundary 11, with only 8 in the overlap. They were built independently
and their scopes were never reconciled.
- **Zone is a property of the lane.** The caller (`WARDEN_AGENT_ID`) is a
*modifier*, not the zone — collapsing them would make one lane need two. And
the moment is confirmed as a separate object: nothing in the corpus represents
it, and `delegation.blocked_on` is exactly the expiry-by-intention shape T04
must not reproduce.
- **Environment posture and `M0``M3` compose but cannot be joined today** —
they are per-workload, zones are per-lane, and no registry carries a join key.
Inventing one would be fabrication under §6.
**Residue, and the most valuable output:** 14 of 27 lanes carry no `risk` value,
and `is_high_risk` is `risk == "high"`, so the agent read-boundary never fires
for them; 5 are `exec_capable`. Routed to `risk-nexus` as **`RISK-F-0003`**.
Consequence for this workplan: `risk` is not usable as a membership input until
those lanes are graded.
```task
id: ZONE-WP-0001-T03
status: done
priority: high
state_hub_task_id: "6896ee26-7abd-5c0a-ba00-076aa9d05874"
```
**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.
**Owner rulings arrived and task completed, 2026-08-22.** Re-running
`ops-warden/scripts/report_workload_join.py` finds **nine** declared rapp
workloads, but still only **one of 27 lanes** joins to a workload; 13 name an
undeclared candidate and 13 have no workload-shaped path. Net-kingdom Decisions
5.6.1 and 5.6.2 settle the residue: operational/control-plane execution units
are workloads; non-rapp workloads declare authoritative identity directly in
`tenancy.yaml`; the stable workload id is the explicit join key; and absent
identity, membership, or floor resolves to `unknown` without inference. Canon's
schema now requires `workload_identity` beside every `zones:` block, which also
settles the declaration surface originally asked of `repo-manager`.
The stance, failure-mode, maturity-derived default, and initial control profile
are defined in `docs/security-zones_v0.1.md` §§45. `RISK-N-0004`'s consumer
requirements are carried into §7's resolved view: it is workload-addressable,
authoritative, machine-readable, exposes guarantees as well as membership, and
returns `unknown` without manufacturing a zone. The unresolved `public`
DataClassification floor is handled honestly as `unknown` pending
info-tech-canon's ruling; it no longer blocks the model.
Repo-manager subsequently accepted `RMGR-ADR-004` and the v1 workload-reference
contract. Managed deployables resolve through the explicit
`(rapp_id, workload_identity.name)` pair with optional `deployable`; catalog
owners distinguish workload-applicable subjects from native `not-applicable`
actors, actions, lanes, patterns, and resources. This sharpens rather than
reopens canon's ruling: the executing unit is the workload, while the action or
resource it handles is not. All omitted or unresolved applicable references
remain `unknown`.
**Operator direction 2026-08-19 — defaults derive from maturity, not from
nothing.** An ungraded lane must not inherit the safest-for-the-tool default; it
should inherit the default its *maturity context* implies. Early or experimental
context: an absent grade is tolerable and explicitly accepted. Production
context: an absent grade is `high`, or `critical` where the context is critical.
This is the same rule `RISK-F-0003` needs and is why that finding's fix is a
model question, not a cleanup.
Three things established while checking what this can attach to:
- **The right ladder already exists: `M0``M3`** in ops-warden's
`registry/policy/security-posture.yaml`. It is ordered (`rank`), phased
(`experimental-poc``alpha-early-access` → …), and already carries a data
ceiling per rung (`max_dataclass`: `synthetic``internal` → …) plus
`promotion_gate`. It is exactly the shape this rule needs. What it lacks is a
**join** to lanes or repos — the gap T02 found.
- **`.repo-classification.yaml` `category` cannot carry it.** It is populated
estate-wide (54 `project`, 23 `tooling`, 14 `product`, 7 `research`, 4
`experimental`) and canon calls it "work mode, maturity, or organizational
purpose" — but it is unordered, and the actual values invert the property we
need: `railiance-platform`, which runs production OpenBao and owns three of
the five lanes `RISK-F-0003` exposes, is `tooling`; `ops-warden`, a production
SSH CA, is `tooling`; `net-kingdom`, a canon-documentation repo, is `product`.
Deriving a safety default from it would grade production credential
infrastructure below a documentation repo. `criticality` exists in the rapp
vocabulary (`low`/`medium`/`high`/`critical`) but is unset on every repo
checked.
- **Whose maturity — the owner's, not the holder's.** All 27 lanes live in
ops-warden's catalog, but their `owner_repo` is elsewhere. Deriving from the
repo that holds the pointer would give every lane ops-warden's classification,
which says nothing about the credential. Membership must derive from the lane's
owner or the workload it serves.
**The principle validates against the data:** all five lanes `RISK-F-0003`
exposes are owned by production-serving components, so "unclassified in a
production context → high" would have caught every one.
**One vocabulary caution for T05.** `accepted` is not a grade. A grade says how
dangerous a lane is; an acceptance says who agreed to carry that danger, and
therefore needs an owner and a date. Putting an acceptance in a descriptive
field repeats the error net-kingdom rejected when it refused stance as a
tenancy axis. Model them as two fields: grade (derived, defaultable) and
acceptance (asserted, owned, expiring).
**Resolved by the T02 correction.** Grade and acceptance are the two sides of a
Decision 8.2 join: the zone asserts a floor, the workload asserts its posture,
and the join is mechanical. Not two fields to invent — one join to implement.
**And the join is now the critical path.** ops-warden's catalog contains no
workloads, so there is no subject to attach an admission standard to. `M0`-`M3`
already grades workloads, which is the side that exists; what is missing is
anything declaring which workload consumes which lane. T03 cannot produce a
usable stance model until that is answered, and the honest answer may be that it
belongs to whatever declares workloads — not to this repo.
**Corrected 2026-08-20 — the workload side exists, and most of the join with
it.** T02 concluded no registry carries a join key. That was too strong; it was
derived from ops-warden's catalog alone, which is the one place a workload
declaration would *not* live.
`rapp-*/declarations/rapp.yaml` is the workload declaration surface. **Eight**
rapps declare `workload_identity` (`name`, `principal`, `service_account`,
`tenant`) together with `data_classification`, `criticality`
(`low`/`medium`/`high`/`critical`), `readiness_state`, and `bound_reefs`.
And the chain to maturity is already complete: ops-warden's
`registry/policy/security-posture.yaml` carries `dataclass_floor`, mapping
`synthetic→M0, internal→M1, confidential→M2, restricted→M3`. So
**workload → `data_classification` → minimum maturity** is a shared vocabulary
that spans two repos today, with nobody having built it for this purpose. That
is the operator's maturity-derived default, already half-implemented.
`criticality` is the other half — it is exactly the "critical if the context is
critical" axis, declared per workload, and untouched by any control so far.
Three defects in the join, which are now T03's actual work rather than a
blocking unknown:
1. **The lane→workload key is derivable but fragile.** `path_template` encodes
`platform/workloads/<domain>/<workload>/<bundle>` for most lanes — but the
convention is inconsistent: `platform/workloads/rapp-qonto/keycape-client`
has one level fewer, so parsing yields `keycape-client` as a "workload" when
it is a bundle. And the names do not agree: that path says `rapp-qonto` while
the rapp declares `workload_identity.name: qonto`. **Recommendation: add an
explicit `workload:` field to catalog entries.** Inferring identity by
parsing a path string is precisely the implicit coupling that breaks quietly.
2. **Vocabulary mismatch.** rapps declare `public`, which `dataclass_floor` does
not map. `rapp-policy-nexus` is `public` today, so the floor cannot grade it.
One of the two vocabularies must move; canon owns `DataClassification`.
3. **Coverage is partial.** Eight workloads are declared; ops-warden's catalog
yields ~17 distinct path identities. The unmatched remainder is the
interesting set — it is where a lane exists for something that is not a
declared workload at all.
This reopens the ownership question favourably: zone-engine does **not** need to
build a workload registry. It needs to consume `rapp` declarations and ops-warden's
`dataclass_floor`, and to ask ops-warden for one explicit field.
### Measured 2026-08-20 — the join covers one lane in twenty-seven
The correction above was itself too optimistic. Saying "the workload side exists,
and most of the join with it" was an inference from structure; computing it gives
a different answer. `ops-warden/scripts/report_workload_join.py`:
```
declared workloads (rapp-*): 8
lanes matched to a workload: 1
lanes unmatched: 13
lanes with no usable path: 13
```
The single match is `issue-core-ingestion-api-key` → workload `issue-core`,
`confidential`, `criticality: high`, floor `M2`.
`rapp-qonto-keycape-client` does **not** match, and demonstrates the predicted
failure exactly: the path offers `keycache-client` and `rapp-qonto`, while the
rapp declares `workload_identity.name: qonto`. Neither candidate is the declared
name.
**So the gap is not a missing key. It is missing declarations.** Thirteen lanes
name something plausible — `whynot-design`, `llm-connect`, `forgejo-admin`,
`audit-core`, `email-connect`, `company-email` — that no rapp declares as a
workload. Those are real running things holding real credentials, and the
workload registry does not know they exist. Thirteen more lanes have no usable
path at all: SSH, policy checks, tunnels, broker grants and pattern lanes, which
are not KV addresses and never will be.
**Consequence for the model, and it is a hard one.** A zone whose subject is the
workload cannot govern a credential estate in which 26 of 27 lanes have no
identifiable workload. T03 cannot proceed to stance modelling on this basis.
Either:
- **the declaration surface must grow** to cover what actually holds credentials
— which is `rapp`/`repo-manager` work, not zone-engine's, and is a far larger
ask than "add one field"; or
- **zones bind to something other than a workload for the lanes that have none**
— which reopens the subject question that the operator's direction settled, and
should not be done quietly to make the model fit.
The second is the tempting one and is how a model quietly becomes wrong. Take the
first to `repo-manager` and `net-kingdom` before modelling further.
The three defects listed above stand, but they are second-order next to this: an
explicit `workload:` field on catalog entries is still worth having, and it does
not help until there are workloads to point it at.
**Build stage is a positional fact, not a concession.** The organization is in
`build`; the default stance is permissive and zones tighten by *admission*,
rather than by raising a global floor. That is what makes this model compatible
with deep refactoring instead of hostile to it, and it is the same reasoning
that produced `ADR-0006`.
**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: done
priority: high
state_hub_task_id: "8808346a-6293-59be-9336-67e1c896f198"
```
**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.
**Done 2026-08-22.** The lifecycle and runtime decision are recorded in
`docs/exception-lifecycle-2026-08-22.md`. A grant is a reviewed,
machine-readable relaxation for named workloads, one zone, and one control;
only the control owner's designated authority may grant it, within that
control's declared maximum duration. The enforcement point applies it only for
`not_before <= now < not_after`, falls back to the base rule on any invalid or
unevaluable fact, stamps the active exception into its decision/verdict, and
caps any durable credential or session so no granted authority outlives the
exception. Expiry auto-restores the base rule; renewal is a new grant.
**No runtime is warranted.** flex-auth policy evaluates time in Rego; CLI and
PEP controls evaluate it on invocation. A central service could neither reload
flex-auth's digest-pinned artifacts nor improve deadline enforcement without
adding a synchronous availability and authority boundary. Git review plus
enforcement-time evaluation is the cheaper and stronger result.
**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: done
priority: medium
state_hub_task_id: "911fbcf1-d330-53e6-8874-cff320b5fc3a"
```
**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.
**Done 2026-08-22.** `docs/security-zones_v0.1.md` §§68 defines the `zones:`
shape, the workload-addressable resolved view, and membership-change evidence.
Membership is attached to an authoritative `workload_identity` in the same
service entry and compiled to the governed workload resource as `workload_id`,
`security_zone`, `security_zone_admission`, and
`security_zone_revision`. Caller identity remains subject context. A control
that governs the caller must name that workload role explicitly.
The dormant hardcoded `trust_zone: platform` is not reused; it must be retired
before `security_zone` is emitted. Membership changes require a reviewed source
change, a new content/source revision, and a compiler diff against the preceding
snapshot. Temporary relaxation is always an exception, never a membership edit.
**Carrier file settled by net-kingdom 2026-08-19 (see T01).** The declaration
rides `tenancy.yaml` under a reserved top-level `zones:` key, not a new root
file. The key is already permitted and deliberately unconstrained in
`net-kingdom/canon/schemas/tenancy-posture_v0.1.schema.json`, so this task
defines the shape inside `zones:` and nothing else. What remains open here is
unchanged: the `trust_zone` collision, and whether membership rides the subject
or the resource.
**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: done
priority: medium
state_hub_task_id: "1957437b-223f-5f63-961a-c9256690746f"
```
**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.
**Done 2026-08-22.** The owner draft is
`docs/security-zones_v0.1.md`. It incorporates net-kingdom Decisions 5.6.1 and
5.6.2, the T03 control profile, the T04 exception lifecycle, and the T05
declaration/compiler contract. Commit `b7095bb` was offered to net-kingdom for
owner-driven publication; net-kingdom published it at canon revision `337484a`
on 2026-08-22.
```task
id: ZONE-WP-0001-T07
status: done
priority: medium
state_hub_task_id: "9a794ffe-c056-5da8-9d56-6826f5ed37ce"
```
**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.
**Done 2026-08-22.** ops-warden `7ce58ae`/`6604ace` and flex-auth
`e521e7b`/`00a7bc4` declare evidenced `z1-operational` membership. The
zone-engine compiler reads both as `satisfied`, making zone-engine the third
repo in the adoption proof. ops-warden retired `policy.enabled`, global
`policy.fail_closed`, and the dormant `trust_zone` compiler source; flex-auth
package v2 owns a total stance mapping and ops-warden owns the total PEP failure
mapping. The live authenticated caller check returned HTTP 200 and
`decision:f3f7c88f9585582a`. Full evidence is
`docs/evidence/security-zone-adoption-2026-08-22.md`.
Repo-manager adoption/proof is linked as `RMGR-WP-0010-T07` to this task
(`ZONE-WP-0001-T07`, State Hub task
`c7ea24df-8aa9-4ead-ba3a-033aa5201c21`). Its accepted v1 tuple and explicit
`unknown`/`not-applicable` distinction are now consumed by the draft.
## 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` — draft-9 Decisions 5.6,
8.4.1, 8.4.2 are net-kingdom's answer to T01
- `net-kingdom` `NK-WP-0027` — the reef ↔ `P`/`V` reconciliation, taken off this repo
- `repo-manager/docs/RailianceAppDeploymentGuide.md` — reefs