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>
270 lines
13 KiB
Markdown
270 lines
13 KiB
Markdown
---
|
||
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`
|