ops-warden/workplans/WARDEN-WP-0032-security-zones.md
tegwick d0d4f9d8fc
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Make the risk grade fail safe, and gate CI on absence
The mechanism behind RISK-F-0003 was sharper than the finding described.
is_high_risk was risk == "high", but risk was never absent at the model layer:
RouteEntry.risk carried a dataclass default of "standard". An omitted grade was
not unhandled, it was actively resolved to the permissive value — fail-open by
construction, which is why nothing ever warned.

The default is now "ungraded" and is_high_risk returns true for anything outside
an explicit low-risk vocabulary (standard / low / accepted). An omitted grade and
an unrecognised grade from a newer catalog both resolve to high, so the boundary
fails safe in both directions rather than reading an unknown value as permission.

test_every_repo_catalog_lane_is_explicitly_graded is the CI gate that stops an
ungraded lane being committed, per ADR-0007: absence is not a grade.

"accepted" is in the low-risk vocabulary deliberately, ready for the
maturity-derived default — an experimental-context lane may be explicitly
accepted, which is a graded decision rather than an omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:13:35 +02:00

270 lines
13 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: WARDEN-WP-0032
type: workplan
title: "Adopt security zones as a consumer — retire the global policy.enabled"
domain: infotech
repo: ops-warden
status: proposed
owner: ops-warden
topic_slug: netkingdom
planning_priority: P1
depends_on_workplans:
- WARDEN-WP-0031
created: "2026-08-19"
updated: "2026-08-19"
state_hub_workstream_id: "f1b6cdcb-5e16-48ab-a72b-339e909e92f0"
---
# WARDEN-WP-0032 — Adopt security zones as a consumer
`ADR-0006` defers `policy.enabled: true` until enforcement can be scoped to a
zone. **The zone model itself is no longer ops-warden's work.** It moved to
`zone-engine` as `ZONE-WP-0001` on 2026-08-19, where it belongs under `ADR-0005`
— ops-warden implements one lane narrowly and routes the rest, and an
estate-wide enforcement model is not a lane it should absorb.
What stays here is the consumer side: ops-warden was the repo whose deferred
flip exposed the gap, it holds the controls the model must be able to express,
and it is the first consumer the model has to satisfy.
## What ops-warden owes zone-engine
Inputs, not designs. `ZONE-WP-0001-T02` derives the model from the real estate,
and most of that estate is ops-warden's:
- **27 routing catalog lanes** (`registry/routing/catalog.yaml`) with `risk`,
`status`, and `delegation` already on each — the likeliest membership inputs.
- **The actor inventory** (`adm` / `agt` / `atm`) and its TTL policy.
- **The three posture axes already shipped** — environment and maturity
(WP-0015), `organization_posture` (WP-0029) — including the honest answer to
whether `organization_posture` should be folded into zones rather than run
beside them.
- **Three controls the model must be able to express**: the flex-auth pre-sign
gate (`policy.enabled` + `fail_closed`), the agent read-boundary on
`risk: high` lanes (`ADR-0004`), and the `warden plan` escalation verdicts.
- **The compiled-registry path** (`scripts/build_flex_auth_registry.py`) — how
membership can reach flex-auth without a lookup in a latency-critical decision
path.
## Tasks
```task
id: WARDEN-WP-0032-T01
status: todo
priority: high
state_hub_task_id: "b6dac514-4b05-47b8-8745-6a1c67b6100e"
```
**Hand the estate inputs to `ZONE-WP-0001-T02`.** Not a design proposal — the
lanes, actors, axes, and controls above, with the exceptions ops-warden already
knows do not fit cleanly (the interim delegation lanes from WP-0030 are the
obvious candidates: a lane covered on someone else's behalf may not sit in the
same zone as one ops-warden owns outright).
Include the `organization_posture` fold-in question honestly, including the case
against keeping it.
**Answered by net-kingdom 2026-08-19** (canon owner of `tenancy-posture_v0.1`,
answering `ZONE-WP-0001-T01`): **do not fold it in, and do not put it in a
per-repo declaration under any key.** `organization_posture` is a fleet-wide,
time-varying scalar describing the *estate*, not a property of a declaring
service; a per-repo copy of a global goes stale in as many places as there are
repos. It stays what WP-0029 made it — an input to stance selection that the
zone model reads. Hand it over as an input, not as a candidate axis. Environment
posture and `M0``M3` are per-workload and remain genuinely composable; those two
are still open for `ZONE-WP-0001-T02`.
```task
id: WARDEN-WP-0032-T02
status: wait
priority: high
state_hub_task_id: "08735f17-fe7e-4a6a-9b71-f350f98c30b1"
```
**Replace `policy.enabled` with a zone-aware control.** Waits on
`ZONE-WP-0001-T03` and `T05`. Reads the zone of the actor being signed for and
that zone's failure mode. Ship deprecation and migration in the same change —
leaving both is the second-source-of-truth failure `ADR-0001` exists to prevent.
Re-run `scripts/check_policy_caller_identity.py` before enabling anything: the
WP-0031 evidence (`decision:f3f7c88f9585582a`, 2026-08-19) will be stale, and
re-establishing it is cheap precisely so this is a re-check rather than a re-do.
This closes `WARDEN-WP-0031-T05`.
**Amended by flex-auth 2026-08-19, reviewing `ZONE-WP-0001` as the PDP.** Two
constraints that change what this task can build, both argued in full at
`ZONE-WP-0001-T03` and `T04`:
1. **The zone-aware control splits across the boundary, it does not move.**
flex-auth will read compiled zone *membership* from the registry and apply the
per-zone *stance* from its policy package, returning an effect plus an
advisory annotation. What stays on the ops-warden side is the axis flex-auth
structurally cannot carry: **`fail_closed` is not expressible by a PDP.**
Fail-open describes what `warden sign` does when flex-auth is *unreachable*
no decision is rendered, so no compiled data and no policy rule can reach it.
So the replacement for `policy.enabled` is not one zone-derived boolean
either; it is (a) stance, which arrives *in the decision*, and (b) failure
mode, which stays a local ops-warden setting declared per zone. Plan for two,
or T02 will be built expecting flex-auth to answer something it cannot.
2. **The membership compiler is ops-warden's to change, and it has a name
collision to resolve first.** `scripts/build_flex_auth_registry.py:77` already
emits `"trust_zone": "platform"` as a hardcoded constant on every
ssh-certificate resource. It is a first-class field on flex-auth's `Resource`,
it is surfaced into rego input, and **no policy package reads it**. Before
compiling zone membership, either retire that constant or deliberately
repurpose it — do not add `security_zone` beside a dormant `trust_zone` and
leave the next reader to guess which is real. That is the `ADR-0001`
second-source-of-truth failure in miniature, inside a generated artifact.
Good news for T03's cost: **no flex-auth registry schema change is required.**
`metadata`, `labels` and `attributes` already flatten into the rego input, so the
compiler can emit membership today.
```task
id: WARDEN-WP-0032-T03
status: wait
priority: medium
state_hub_task_id: "fd642377-8d64-473c-b684-f8f1a842c223"
```
**Declare ops-warden's zones** in the format `ZONE-WP-0001-T05` settles,
alongside the existing posture descriptors.
**Amended by net-kingdom 2026-08-19 — the carrier file is already settled, only
its contents are open.** Canon Decision 5.6 (`tenancy-posture_v0.1` draft-9)
rules that zone membership is declared in **`tenancy.yaml` under a reserved
top-level `zones:` key**, sibling to `tenancy:` and `provider:` and never inside
`tenancy.current`. §5.4 makes that file the repo's single posture declaration
surface, and a second root file would be the second-source-of-truth failure
`ADR-0001` exists to prevent — in ops-warden's own idiom. The key is reserved and
deliberately unconstrained in `net-kingdom/canon/schemas/tenancy-posture_v0.1.schema.json`,
so a combined declaration validates today.
Note the consequence for this repo: **ops-warden does not currently have a
`tenancy.yaml`.** Declaring zones means writing one, which means declaring the
six-axis posture vector too. That is a real and probably overdue cost, not an
accident of this amendment — canon would rather ops-warden declare both
accurately than declare a zone with no posture beside it.
Note also *why* stance is not something this declaration may set: canon Decision
5.6 rules that a declarer must not be the party that sets the stance, or an
accurately declared `exempt` becomes conformant *and* exempt. ops-warden declares
which zone a lane or actor is in. The stance of the pre-sign gate in that zone is
flex-auth's policy package; the failure mode is ops-warden's own setting, per
flex-auth's T03 amendment. Per flex-auth's T05 amendment, this
declaration must also say whether a zone rides the **actor** (a flex-auth
subject) or the **lane** (a flex-auth per-actor resource) — in the compiled
registry an actor is both, and the compiler needs to be told which record
carries membership. Carry the conformance rule:
*accuracy, not altitude*. Declaring a stricter zone than can be evidenced is the
failure mode that looks like progress.
```task
id: WARDEN-WP-0032-T04
status: wait
priority: medium
state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
```
**Amend `ADR-0006`.** Once zones exist and enforcement scoping is owned by
`zone-engine`, ADR-0006 must say that ops-warden *follows* the model rather than
owning it — a superseding record, never an in-place edit. Update `SCOPE.md`,
`wiki/WorkloadSecurityPosture.md`, and `wiki/PolicyGatedSigning.md` with it.
```task
id: WARDEN-WP-0032-T05
status: done
priority: high
state_hub_task_id: "4d5d2756-c880-4e90-8fb7-1f03674a4cd9"
```
**Grade the five exposed lanes now — do not wait for the model.**
`RISK-F-0003`: `is_high_risk` is `risk == "high"` and `risk` is optional, so the
14 ungraded lanes never reach the agent read-boundary. Five are `exec_capable`
and can therefore stream a value to an agent session: `openbao-api-key`,
`whynot-design-npm-publish`, `key-cape-oidc-login`,
`issue-core-ingestion-api-key`, `reuse-surface-hub-write-token`.
The durable answer is a maturity-derived default (`ZONE-WP-0001-T03`), and it is
the right answer. It is also months away, and this is a live control gap in a
shipped ADR. Grade these five explicitly, then the remaining nine.
Grading is judgement, not backfill — each lane's grade should be justified in the
entry, and the operator should sanction the high/standard calls rather than
having them inferred. Verify separately whether OpenBao's
`agent-high-risk-boundary` policy covers these paths; `RISK-F-0003` deliberately
does not assume it does, because those paths were graded by the same omission.
**Done 2026-08-19, operator-sanctioned.** All 14 ungraded lanes graded on merit,
each with its justification in the entry: 17 `high`, 10 `standard`, **0
ungraded**. `warden access <lane> --fetch` with `WARDEN_AGENT_ID` set now exits 7
on lanes that were silently outside the control an hour earlier.
Graded on merit, not defensively. A first pass marked
`issue-core-ingestion-api-key` and `reuse-surface-hub-write-token` `high`; the
existing test `test_high_risk_lanes_classified` asserted the opposite and was
right — ordinary internal workload secrets are `standard`. Both were regraded
down. `high` means disclosure into a logged context is damaging beyond what
rotation recovers: provider keys with spend, admin PATs, tenant commercial data,
supply-chain publish rights. `inter-hub-bootstrap-ssh` is `high` **conservatively**
— ops-warden could not establish that no key material moves in the envelope, and
that is recorded in the entry so it is regraded with evidence rather than assumed
down.
The rule is now `ADR-0007`: build-stage permissiveness stops at credential
disclosure. Not yet verified: whether OpenBao's `agent-high-risk-boundary`
policy covers these paths (T06).
```task
id: WARDEN-WP-0032-T06
status: progress
priority: medium
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
```
**Make absence impossible, once the model says what absence means.** Waits on
`ZONE-WP-0001-T03`. Either invert the default (absent `risk` resolves through the
lane owner's maturity, per the operator direction) or require `risk` at catalog
load and in CI. Whichever lands, the failure mode to kill is the current one:
adding a lane without a grade silently places it outside a control, with nothing
at load or in CI noticing.
Feed back to `ZONE-WP-0001-T03` whether ops-warden can supply the join it needs —
today no lane references a workload or an environment, so the `M0``M3` ladder
has nothing to attach to from this side.
**Enforcement half done 2026-08-20.** It did not need the zone model: `ADR-0007`
already decided absence is a defect, which is enough to make the code fail safe
and to gate CI.
The real mechanism turned out to be sharper than `RISK-F-0003` described.
`is_high_risk` was `risk == "high"`, but `risk` was **not** absent at the model
layer — `RouteEntry.risk` carried a dataclass default of `"standard"`. So an
omitted grade was not unhandled; it was actively resolved to the permissive
value. Fail-open by construction, which is why nothing warned.
Now: the default is `"ungraded"`, and `is_high_risk` returns true for anything
not in an explicit low-risk vocabulary (`standard` / `low` / `accepted`). An
omitted grade **and** a grade from a newer catalog both resolve to high, so the
boundary fails safe in both directions. `is_graded` exposes the distinction, and
`test_every_repo_catalog_lane_is_explicitly_graded` is the CI gate that stops an
ungraded lane being committed. Four regression tests cover it.
`accepted` is in the low-risk vocabulary deliberately, ready for the
maturity-derived default: an experimental-context lane may be explicitly
accepted, which is a graded decision rather than an omission.
**Still open on this task:** whether the maturity-derived default replaces the
`ungraded` sentinel entirely (waits on `ZONE-WP-0001-T03`), and verifying
OpenBao's `agent-high-risk-boundary` policy covers these paths.
## Related
- `zone-engine` `ZONE-WP-0001` — the model, and where this work is led from
- `ADR-0006` — enforcement is zone-scoped, never a global flag
- `WARDEN-WP-0031` — the deferred flip and its readiness evidence
- `net-kingdom` `tenancy-posture_v0.1` draft-9 Decisions 5.6, 8.4.1, 8.4.2 — canon's
answer to `ZONE-WP-0001-T01`; enforcement stance is a sibling standard, not a
seventh axis, and the declaration surface is `tenancy.yaml`