ops-warden/workplans/WARDEN-WP-0032-security-zones.md
tegwick 7ce58ae638
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: adopt security zones and explicit workload refs
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:36:37 +02:00

23 KiB
Raw Blame History

id type title domain repo status owner topic_slug planning_priority depends_on_workplans created updated state_hub_workstream_id
WARDEN-WP-0032 workplan Adopt security zones as a consumer — retire the global policy.enabled infotech ops-warden finished ops-warden netkingdom P1
WARDEN-WP-0031
2026-08-19 2026-08-22 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

id: WARDEN-WP-0032-T01
status: done
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 M0M3 are per-workload and remain genuinely composable; those two are still open for ZONE-WP-0001-T02.

Done 2026-08-21. Every input listed above is now carried in ZONE-WP-0001, and T02 there is done — which is the acceptance condition for this task, since what was owed was inputs rather than a design.

What actually reached them: the 27 graded lanes with risk/status/delegation (T05), the M0M3 ladder in registry/policy/security-posture.yaml — which ZONE-WP-0001-T02 adopted as the maturity ladder rather than .repo-classification.yaml category, on the evidence that the latter grades a production SSH CA below a documentation repo — the three controls, and the compiled-registry path including the dormant trust_zone constant at scripts/build_flex_auth_registry.py:77.

Two inputs were handed over as corrections to ops-warden's own earlier claims, which is the part worth recording. organization_posture was offered as a candidate axis and net-kingdom refused it (above); and ops-warden asserted no registry carried a workload join key, which was true of its own catalog and false of the estate — rapp-*/declarations/rapp.yaml carries eight workload_identity declarations. scripts/report_workload_join.py then measured the join rather than asserting it: 1 of 27 lanes matches a declared workload. That number is ZONE-WP-0001-T03's critical path, and it came out of correcting this repo's own input rather than out of supplying a new one.

id: WARDEN-WP-0032-T02
status: done
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.

Consumer implementation landed in the working tree 2026-08-22. The config loader rejects the retired policy.enabled and global policy.fail_closed keys. scripts/build_flex_auth_registry.py now emits the normative workload_id, security_zone, security_zone_declared, security_zone_admission, security_zone_reason, and security_zone_revision attributes; the dormant trust_zone field is gone. The PEP reads compiled resource membership and applies its local per-zone failure mode. Sign and audit records retain the zone, selected failure mode, outcome, and decision id.

The legacy --no-policy proxy switch remains only as a rejected compatibility flag. It can no longer bypass the gate: unresolved credential targets take the explicit unknown profile and its configured failure mode.

Done 2026-08-22. flex-auth package v2 (commit e521e7b) supplies total stance over every v0.1 zone plus unknown, preserves native enforcement for not-applicable, and returns audit_only for advisory decisions. The required live caller check passed through the existing tunnel with command-mode caller identity and decision decision:f3f7c88f9585582a; the check used a temporary migrated copy because the operator's persistent config still carries the two retired keys. Full repo tests pass and sign/audit evidence records zone, failure mode, outcome, and decision id.

The required readiness recheck was run on 2026-08-22 after warden plan returned autonomous. It correctly returned NOT READY before obtaining or printing any token because the active operator warden.yaml still contains the two retired global keys. Operator configuration migration is therefore an explicit remaining acceptance step, followed by the value-safe live /v1/check smoke. Do not ask flex-auth to enforce caller authentication yet.

id: WARDEN-WP-0032-T03
status: done
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.

Done 2026-08-22. tenancy.yaml now declares ops-warden as an independently governed operational execution unit with its Kubernetes service-account binding, and admits it to z1-operational at M1, medium, internal. docs/evidence/security-zone-admission-2026-08-22.md records why this is the highest evidenced admission rather than an aspirational M2. The declaration passes net-kingdom's current tenancy-posture_v0.1 validator.

id: WARDEN-WP-0032-T04
status: done
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.

Done 2026-08-22. ADR-0006 is marked superseded without rewriting its decision. ADR-0009 accepts security-zones_v0.1 as a consumer, records the PDP/PEP split and unknown handling, and is indexed as its successor. SCOPE.md, wiki/WorkloadSecurityPosture.md, wiki/PolicyGatedSigning.md, configuration guidance, access guidance, and affected playbooks now describe the zone-aware control and do not instruct operators to use a global bypass.

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

id: WARDEN-WP-0032-T06
status: done
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 M0M3 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.

Second layer checked 2026-08-20 — it does not cover them. The OpenBao side was compared statically (policy file vs catalog): agent-high-risk-boundary denies 5 data paths covering 6 of the 17 high-risk lanes. Eight high-risk lanes with concrete KV paths are not denied — and four of those were already graded high before the regrade, so the divergence is pre-existing rather than something the grading introduced. It had simply never been comparable before.

This matters more than the ops-warden half: warden access exits 7 for all 17, but that only protects the ops-warden path. The OpenBao policy is what protects a direct bao kv get, which is the actual 2026-07-16 vector.

Routed to risk-nexus as RISK-F-0004, fix_owner: railiance-platform — the policy is theirs, and ops-warden does not amend another repo's control.

Live confirmation done 2026-08-21 — and the blocker was not real. The token was not expired: bao token lookup returned a valid platform-admin token from an OIDC login, and bao policy read agent-high-risk-boundary succeeded. No bao login was needed. Worth recording as a small instance of the lesson .claude/rules/finding-routing.md already states — re-read a blocker before trusting it. This one was a stale claim about the world, carried for a day in both this workplan and RISK-F-0009.

The verification script also did not exist. It was described here as "ready", and nothing was committed. scripts/check_agent_read_boundary.py now exists, with tests, and is the invariant RISK-F-0009 asked for rather than a one-off audit: exit 1 when any high-risk lane has no corresponding deny. It prefers the deployed policy and falls back to the file only with a loud warning, because deployment drift is the thing it exists to catch. Capabilities-only by construction — it reads the policy document and lane metadata, never a secret value, and never mints a token.

Three results, one against ourselves:

  1. Coverage confirmed at 6 of 17 against the live policy — the headline was right and is no longer inferred from a checkout.
  2. Six lanes uncovered, not eight. RISK-F-0009 counted openbao-api-key (a <domain>/<workload>/<bundle> pattern) and ops-warden-warden-sign-token (a broker grant, not KV) among the concrete uncovered paths, while its own prose said the first was a pattern. Neither can be expressed as a deny. Corrected: 6 covered, 6 uncovered, 5 with no address.
  3. The deployed policy has drifted from the file. The file denies platform/workloads/core-hub/runtime; the server does not. No ops-warden lane maps there, so our numbers are unchanged — it matters as evidence, since RISK-F-0009 named exactly this divergence as its unconfirmed risk.

Still not established, and not ops-warden's to establish: whether any agent token carries agent-high-risk-boundary, and whether any carries it together with a workload-kv-read-* policy. Confirming that means minting or inspecting tokens — a write against railiance-platform's OpenBao. Exposure stays theoretical to the same degree as before.

Platform half closed 2026-08-22. RAILIANCE-WP-0022 consumed the generated high-risk path input and deployed the completed deny policy. The platform-owned invariants report 19 high-risk lanes, 14 concrete entries, 0 uncovered. A dedicated coding-agent-railiance-platform AppRole then proved deny-wins with exactly agent-high-risk-boundary plus one workload read policy: data access was denied while metadata remained readable; its single-use test identity was self-revoked and no test tokens remained. Decision f0955252 records the operational identity. No KV value was read.

The local invariant agrees against the platform policy file: 19 high-risk lanes, 14 concrete entries covered, 5 without a concrete address, 0 uncovered. This session could not independently read the deployed policy, so live-state evidence is the platform-owned readback above rather than a claim inferred from the local file.

Done 2026-08-22. The settled model makes the two layers complementary: catalog CI continues to require an explicit risk field so omission cannot silently weaken the agent boundary, while RouteEntry.risk_for_zone implements the normative maturity-derived fallback for consumers evaluating unresolved or future inputs (z0 + satisfied + synthetic-only → standard; satisfied z3critical; everything else → high). An explicit catalog grade always wins. The platform deny policy and its live proof above close the direct-OpenBao half.

id: WARDEN-WP-0032-T07
status: done
priority: medium
state_hub_task_id: "6b8c8181-978c-4b0a-ac7b-2cc9ad57b3ba"

Adopt Repo Manager's workload-reference owner interface. Accepted with one documented amendment against repo-manager revision 890f3b0: managed deployables use the exact (rapp_id, workload_identity.name) pair and optional deployable name; an independently governed operational execution unit may use an exact local tenancy.yaml declaration. Native actions, actors, grants, patterns, and non-workload resources are explicitly not-applicable; unresolved workload-applicable references remain unknown. No path, owner name, or repository-name inference is allowed.

Every one of the 27 catalog lanes now carries an explicit workload_ref: 3 resolved (2 exact managed RAPP references and ops-warden's direct operational declaration), 17 unknown with reasons, and 7 not-applicable with reasons. scripts/report_workload_join.py resolves only those explicit references, Repo Manager validates the interface and resolves both managed tuples exactly, and regression tests reject malformed or inferred joins. This incorporates net-kingdom's draft-12 correction received 2026-08-22: local tenancy declaration is not an escape hatch for a managed running deployable.

  • 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