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
693 lines
38 KiB
Markdown
693 lines
38 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: 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 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: 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` §§4–5. `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` §§6–8 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
|