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

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

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

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

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

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

15 KiB
Raw Blame History

id type title domain repo status owner topic_slug planning_priority created updated state_hub_workstream_id
ZONE-WP-0001 workplan The security zone model — derive it from the estate, draft it as canon infotech zone-engine proposed zone-engine netkingdom P1 2026-08-19 2026-08-19 a75c8c9c-c108-4831-8d72-4343cce49879

ZONE-WP-0001 — The security zone model

Ported from WARDEN-WP-0032 (ops-warden), which deferred policy.enabled: true under ADR-0006 and named this repo as its successor. ops-warden keeps only its consumer-side task.

This workplan produces a model and a canon draft. It does not produce an API, a schema, or storage. An engine that ships a schema before it has partitioned the real estate defines the model by accident, through whatever its first wire format happened to be. Whether a runtime is warranted is an output of T02T03, not an assumption of T01.

The gap in one table

Axis Shipped Answers
Environment posture (dev/test/prod) ops-warden WP-0015 Where does this run?
Workload maturity (M0M3) 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

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.

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.
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.

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.

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.

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.

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.

  • 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