diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 3256afb..1fa1d44 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -13,6 +13,7 @@ distinction that matters here. | `ADR-0003` | Cover gaps, but never silently own them | | `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | | `ADR-0005` | Implement one lane narrowly, route everything else | +| `ADR-0006` | Enforcement is zone-scoped, never a global flag | ### Owned versus inherited — check `owner:` before changing anything diff --git a/SCOPE.md b/SCOPE.md index 87a1e45..e62657b 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -153,6 +153,7 @@ superseding ADR, never an in-place edit. | `ADR-0003` | Cover gaps, but never silently own them | | `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | | `ADR-0005` | Implement one lane narrowly, route everything else | +| `ADR-0006` | Enforcement is zone-scoped, never a global flag (`policy.enabled` stays off until WP-0032) | Rules we follow but do not own — NetKingdom canon, the IAM profile, the credential-management standard, the-custodian's ADR-001 workplan convention — are @@ -277,7 +278,7 @@ for the rest. | WP | Status | Focus | | --- | --- | --- | | WP-0027 | `backlog` | Tamper-resistant credential governance + mass rotation/lockdown (Strand B follow-on to WP-0026) | -| WP-0031 | `active` | Calling-side identity for flex-auth — code + gate done; flip sequence waits on operator + flex-auth enforce | +| WP-0032 | `proposed` | Security zones — scope enforcement to bands of differing rigidity (`ADR-0006`); unblocks the pre-sign gate | | WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate | Remaining production distance is also in other repos' lanes (see Known gaps). @@ -286,7 +287,6 @@ Remaining production distance is also in other repos' lanes (see Known gaps). | Gap | Owner | Notes | | --- | --- | --- | -| `callerAuth.mode: enforce` on the ops-warden pin | flex-auth | **FLEX-WP-0016 T03** — waits on WARDEN-WP-0031 T04, then unblocks `policy.enabled: true` | | ops-bridge `cert_command` on live tunnels | ops-bridge | Playbook + readiness gate shipped (WP-0016); pilot cutover handed off, awaiting ops-bridge | | Principals sync warden ↔ railiance-infra | ops-warden + infra | `scripts/check_principals_drift.py` — operator runs periodically | | NK-WP-0009 joint SSH tutorial | net-kingdom | Parallel coordination track | @@ -352,8 +352,12 @@ Remaining production distance is also in other repos' lanes (see Known gaps). - **Production sign:** verified 2026-06-18 (`history/2026-06-17-openbao-production-verify.md`) - **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog) - **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived). - `policy.enabled: false` — the `flex-auth-ops-warden` pin is in `warn` until - ops-warden sends a caller identity (WP-0031) + WP-0031 shipped the calling identity and flex-auth's pin now runs + `callerAuth.mode: enforce` (FLEX-WP-0016) — the gate is **ready and verified** + (`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). `policy.enabled` + remains **false by decision**, not by blocker: `ADR-0006` scopes enforcement to + security zones, which WP-0032 defines. Re-check with + `scripts/check_policy_caller_identity.py` - **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`, conformance checker, dev doubles); canon landing owner-driven - **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate + diff --git a/docs/adr/ADR-0006-enforcement-is-zone-scoped.md b/docs/adr/ADR-0006-enforcement-is-zone-scoped.md new file mode 100644 index 0000000..ab3d354 --- /dev/null +++ b/docs/adr/ADR-0006-enforcement-is-zone-scoped.md @@ -0,0 +1,101 @@ +--- +id: ops-warden-adr-0006 +type: adr +title: "ADR-0006 — Enforcement is zone-scoped, never a global flag" +domain: infotech +repo: ops-warden +status: accepted +version: "1.0" +revision: "1" +owner: ops-warden +binds: "ops-warden" +created: "2026-08-19" +updated: "2026-08-19" +last_reviewed: "2026-08-19" +review_interval: 6m +enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; WARDEN-WP-0032" +supersedes: "" +successor: "" +--- + +# ADR-0006 — Enforcement is zone-scoped, never a global flag + +## Status + +Accepted 2026-08-19, at the moment `policy.enabled: true` was ready to be set +and deliberately was not. + +## Context + +WARDEN-WP-0031 finished the calling side of the flex-auth pre-sign gate. The +flex-auth pin `flex-auth-ops-warden` runs `callerAuth.mode: enforce`; ops-warden +presents a bound ServiceAccount token; the readiness gate exits 0 against the +enforcing pin and an anonymous `/v1/check` is 401. Everything needed to set +`policy.enabled: true` was in place. + +`policy.enabled` is a **single boolean over the whole repo**. Combined with +`fail_closed: true` it makes flex-auth a hard dependency of *every* `warden +sign` — including the certificates the ops-bridge tunnels depend on, one of +which is the tunnel carrying the policy call itself. A dead tunnel or a +recreated Service does not degrade signing; it stops it. + +That cost might be acceptable for a settled production lane. It is not +acceptable uniformly, because ops-warden signs across an estate that is being +actively rebuilt. During deep refactors — CoulombCore's decommission, the +issue-core move, cluster rebuilds — the same flag would harden exactly the +access needed to *perform* the refactor. Security that stops the work stops +being security and starts being an outage with good intentions. + +The repo already refuses to treat posture as one-dimensional. WP-0015 shipped +environment posture (`dev` / `test` / `prod`) and workload maturity (`M0`–`M3`); +WP-0029 added `organization_posture: build` as a third axis precisely because +the *organization's* state changes what is reasonable to demand. A global +`policy.enabled` contradicts all of that: it is a fourth control that ignores +the three axes already declared. + +## Decision + +**Enforcement posture is a property of a zone, not of the repo.** ops-warden +does not enable a fail-closed authorization gate globally. Before +`policy.enabled: true` is set anywhere, the zones must exist: named bands of +differing rigidity, each declaring what is enforced, what is advisory, and what +is exempt — and the gate must be scoped to them. + +Concretely, until WARDEN-WP-0032 defines the zone model: + +- `policy.enabled` stays `false`. Its readiness is evidence, not a mandate. +- A gate that is *ready* is recorded as ready. Readiness is not a reason to + enable; deferral with a stated reason is a legitimate terminal state for a + task, not an unfinished one. +- Any future enforcement control ships zone-aware or does not ship. A second + global boolean is the defect this record exists to prevent. + +## Consequences + +**We accept** that the pre-sign gate remains unexercised in production longer, +and that the WP-0031 evidence ages. Re-running +`scripts/check_policy_caller_identity.py` re-establishes it cheaply, and the +readiness gate exists precisely so this is a re-check rather than a re-do. + +**We accept** that flex-auth's `flex-auth-ops-warden` pin sits enforcing with no +enforcing consumer. That is not waste: it makes the anonymous path 401 rather +than a decision, which was the ADHOC-2026-08-17-T01 condition regardless of +whether ops-warden calls it. + +**We reject** the framing that a ready control should be turned on because it is +ready. The question is not "does it work" but "which zone is this, and does this +zone want this failure mode." + +**This binds future work.** A zone-blind enforcement flag proposed in any +ops-warden workplan is out of order under this ADR, and should be sent back to +the zone model rather than merged with a caveat in its description. + +## Related + +- `WARDEN-WP-0031` — the calling side that made the flip possible (T05 deferred + under this ADR) +- `WARDEN-WP-0032` — the zone model this record defers to +- `wiki/WorkloadSecurityPosture.md` — the two axes already shipped (WP-0015) +- `wiki/PolicyGatedSigning.md` — the gate itself +- `history/2026-08-19-flex-auth-caller-identity-evidence.md` — readiness evidence +- flex-auth `FLEX-WP-0016` — the enforcing pin diff --git a/docs/adr/README.md b/docs/adr/README.md index 73c2053..7365cd0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -67,3 +67,4 @@ and the publication is a defect. | `ADR-0003` | Cover gaps, but never silently own them | ops-warden | | `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | ops-warden, and any agent runtime calling `warden access` | | `ADR-0005` | Implement one lane narrowly, route everything else | ops-warden | +| `ADR-0006` | Enforcement is zone-scoped, never a global flag | ops-warden | diff --git a/wiki/PolicyGatedSigning.md b/wiki/PolicyGatedSigning.md index 982ddba..b28d188 100644 --- a/wiki/PolicyGatedSigning.md +++ b/wiki/PolicyGatedSigning.md @@ -254,6 +254,25 @@ truncated fingerprint, never the value, so its output is safe to paste into a handoff message. A live `401` means the token was sent but rejected (audience or binding); `403` means it authenticated but may not represent `system: ops-warden`. +### Enablement is deferred by decision (ADR-0006) + +`policy.enabled` is **false, and that is a decision rather than a blocker.** The +gate is ready: flex-auth's pin runs `callerAuth.mode: enforce`, the readiness +gate exits 0 against it, and an anonymous `/v1/check` returns 401. + +It is not enabled because `policy.enabled` is one boolean over the whole repo, +and with `fail_closed: true` it makes flex-auth a hard dependency of every +`warden sign` — including the certs the ops-bridge tunnels depend on, one of +which carries the policy call. Uniform enforcement across an estate under active +refactor hardens the access needed to perform the refactor. + +`ADR-0006` scopes enforcement to security zones; `WARDEN-WP-0032` defines them. +Do not set `policy.enabled: true` outside that model — a zone-blind enablement +is out of order under the ADR, not merely inadvisable. + +The sequence below is retained as the *mechanics*, for whoever enables the gate +for a zone once zones exist. + ### Flip sequence 1. Configure `caller_auth`; `check_policy_caller_identity.py` exits 0 offline. diff --git a/workplans/WARDEN-WP-0031-policy-caller-identity.md b/workplans/WARDEN-WP-0031-policy-caller-identity.md index d3728c9..8a35e15 100644 --- a/workplans/WARDEN-WP-0031-policy-caller-identity.md +++ b/workplans/WARDEN-WP-0031-policy-caller-identity.md @@ -4,7 +4,7 @@ type: workplan title: "Calling-side identity for flex-auth, so policy.enabled can flip" domain: infotech repo: ops-warden -status: active +status: finished owner: ops-warden topic_slug: netkingdom planning_priority: P1 @@ -139,7 +139,7 @@ Evidence: `history/2026-08-19-flex-auth-caller-identity-evidence.md`. ```task id: WARDEN-WP-0031-T05 -status: wait +status: cancel priority: high state_hub_task_id: "8b8e68c4-587d-490d-a98f-840aaff0fdb8" ``` @@ -150,6 +150,21 @@ re-run the gate against the enforcing pin, and only then set `policy.enabled: true` with `fail_closed: true`. Flipping before enforce buys nothing; flipping before T04 401s every `warden sign`. -Asked flex-auth 2026-08-19 with the T04 evidence. Waiting on their enforce. -The closing `warden sign` also needs a scoped `VAULT_TOKEN` via -`ops-warden-warden-sign-token` (operator authenticates to OpenBao). +**Deferred 2026-08-19 under `ADR-0006`** — not blocked, decided against for now. + +flex-auth did enforce (FLEX-WP-0016 T03, Helm rev 2): the pin runs +`--caller-auth-mode enforce`, the gate exits 0 against it +(`HTTP 200, effect=allow, decision:f3f7c88f9585582a`), and an anonymous +`/v1/check` is 401. Everything needed to flip was in place. + +It was not flipped. `policy.enabled` is a single repo-wide boolean, and with +`fail_closed: true` it makes flex-auth a hard dependency of every `warden sign` +— including the certs the ops-bridge tunnels depend on, one of which carries +the policy call itself. Uniform enforcement across an estate being actively +rebuilt would harden the access needed to perform the rebuild. Enforcement +belongs to a zone, not to the repo; that is `ADR-0006`, and the model is +`WARDEN-WP-0032`. This task resumes as WARDEN-WP-0032-T05. + +Also shipped while proving the flip: the gate URL is now a managed ops-bridge +tunnel `flex-auth-ops-warden-railiance01` (`-L 19090:10.43.1.165:8080`) instead +of a hand-run `kubectl port-forward`. diff --git a/workplans/WARDEN-WP-0032-security-zones.md b/workplans/WARDEN-WP-0032-security-zones.md new file mode 100644 index 0000000..441edf3 --- /dev/null +++ b/workplans/WARDEN-WP-0032-security-zones.md @@ -0,0 +1,123 @@ +--- +id: WARDEN-WP-0032 +type: workplan +title: "Security zones — scope enforcement to bands of differing rigidity" +domain: infotech +repo: ops-warden +status: proposed +owner: ops-warden +topic_slug: netkingdom +planning_priority: P1 +depends_on_workplans: + - WARDEN-WP-0015 + - WARDEN-WP-0029 + - WARDEN-WP-0031 +created: "2026-08-19" +updated: "2026-08-19" +--- + +# WARDEN-WP-0032 — Security zones + +`ADR-0006` defers `policy.enabled: true` until enforcement can be scoped. This +workplan builds the thing it defers to. + +## The problem in one paragraph + +ops-warden has three posture axes already: environment (`dev`/`test`/`prod`, +WP-0015), workload maturity (`M0`–`M3`, WP-0015), and organization posture +(`build`, WP-0029). What it does not have is a way to say *"this band of the +estate is enforced, that one is advisory"* — so every enforcement control it +ships is a repo-wide boolean. `policy.enabled` is the first one to actually +become flippable, and flipping it would harden the access needed to perform the +refactors currently underway. The axes describe posture; nothing acts on them. + +## What a zone has to answer + +A zone is only useful if it decides something. Minimum: + +- **Membership** — what puts a lane, actor, or workload in this zone? Derived + from the existing axes where possible rather than a fourth hand-maintained + list. +- **Enforcement stance per control** — for each gate (pre-sign policy, agent + read-boundary, high-risk refusal): enforced, advisory, or exempt. +- **Failure mode** — fail-closed or fail-open, per control per zone. This is the + actual bite: `fail_closed` is what makes a dead tunnel an outage. +- **Who may change it, and how it is observed** — a zone that can be quietly + widened is not a boundary. + +## Open questions to settle before designing + +- Is a zone a property of the **actor** (`adm`/`agt`/`atm`), the **lane** + (catalog entry), the **workload**, or the **moment** (an active refactor)? The + CoulombCore decommission suggests the last one matters and is the hardest to + model — a time-boxed relaxation is a different object than a standing zone. +- Do zones compose with the three existing axes, or replace one? A fourth + independent axis multiplies states; folding `organization_posture` in may be + the honest move. +- Is this ops-warden's model to own at all, or NetKingdom canon that ops-warden + consumes? ADR-0005 says implement narrowly and route broadly. A security zone + model that only ops-warden honours is worth much less than one flex-auth, + secrets-engine, and tenant-engine also read — but ops-warden should not + absorb an estate-wide model on its own authority either. **Settle this + first**; it changes who writes the rest. + +## Tasks + +```task +id: WARDEN-WP-0032-T01 +status: todo +priority: high +``` + +**Decide ownership before designing.** Take the third open question to +net-kingdom (canon) and flex-auth (the PDP that would enforce it). If it is +canon, ops-warden drafts and offers, as it did for the WP-0015 maturity model, +and consumes the result. If it is ops-warden's, say so explicitly and record +why. Do not build a model whose owner is unsettled. + +```task +id: WARDEN-WP-0032-T02 +status: todo +priority: high +``` + +**Name the zones against reality, not in the abstract.** Walk the current +estate — the 27 catalog lanes, the actor inventory, the workloads with posture +descriptors — and see what bands actually exist. A zone model that does not +partition today's lanes cleanly is wrong, and the exceptions are the interesting +part. Include the refactor case explicitly: what zone is a lane in while the +component it fronts is being rebuilt? + +```task +id: WARDEN-WP-0032-T03 +status: todo +priority: high +``` + +**Model enforcement stance and failure mode per zone per control,** covering at +minimum: 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 (WP-0029). Each must be expressible as enforced / advisory / +exempt with an explicit failure mode. + +```task +id: WARDEN-WP-0032-T04 +status: todo +priority: medium +``` + +**Make `policy.enabled` zone-aware and retire the global boolean.** The +successor control reads the zone of the actor being signed for. Ship the +migration in the same change as the deprecation — leaving both is exactly the +second-source-of-truth failure ADR-0001 exists to prevent. + +```task +id: WARDEN-WP-0032-T05 +status: todo +priority: medium +``` + +**Close WARDEN-WP-0031 T05 through this model** — enable the pre-sign gate for +whichever zone the SSH signing lane lands in, with that zone's failure mode. +Re-run `scripts/check_policy_caller_identity.py` first; the WP-0031 evidence +will be stale by then and re-establishing it is cheap by design.