feat: adopt security zones and explicit workload refs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
tegwick 2026-08-22 15:36:37 +02:00
parent 12c637cbf2
commit 7ce58ae638
52 changed files with 1547 additions and 658 deletions

View file

@ -17,7 +17,7 @@ access guidance aligned with NetKingdom canon.
---
## Where we are (2026-08-11)
## Where we are (2026-08-22)
ops-warden **issues short-lived SSH certificates and routes every other credential
need to the subsystem that owns it.** SSH signing is **production-verified** on
@ -48,14 +48,13 @@ the read-only conformance checker `scripts/check_secret_posture_conformance.py`
and the dev-tier contract-double library `warden.doubles` (T4). Canon landing in
net-kingdom / info-tech-canon is owner-driven (tracked via coordination messages, T5).
**Policy gate** is shipped on the caller side (WP-0007) with production registry
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
policy package (FLEX-WP-0006) and now runs the in-cluster pin
`flex-auth-ops-warden` (FLEX-WP-0007 finished; FLEX-WP-0016). `policy.enabled`
remains **false**: that pin is in `callerAuth.mode: warn` because ops-warden sent
no caller identity. WP-0031 ships the calling side (`policy.caller_auth` +
`scripts/check_policy_caller_identity.py`); the flip waits on an operator picking
a token source and flex-auth then enforcing.
**The policy gate is zone-aware.** The caller-identity path is production proven
and the flex-auth pin enforces caller authentication. WP-0032 adopted
`security-zones_v0.1`: the repo-wide `policy.enabled` and `policy.fail_closed`
settings are retired, target workload membership compiles into flex-auth resource
attributes, and ops-warden selects dependency failure behavior from the target
zone. Unknown membership is explicit and uses the versioned build profile.
Ops-warden itself declares `z1-operational` in `tenancy.yaml`.
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
@ -82,7 +81,7 @@ for warden-sign) but is used by 2 of 24 lanes. See
**INTENT alignment:** SSH issuance mission met in production. All ops-warden workplans
through WP-0029 are finished except WP-0027 (`backlog`) and WP-0030 (`proposed`).
Remaining distance is in other repos' lanes: ops-bridge running the cert_command pilot
cutover, flex-auth runtime deployment (FLEX-WP-0007, unblocks `policy.enabled: true`),
cutover, flex-auth publishing the zone-aware pre-sign stance package,
the owner-driven WP-0015 canon landing, and — newly named — the missing owner front
doors that keep ops-warden holding interim lanes (secrets-engine, tenant-engine).
@ -153,9 +152,10 @@ 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) |
| `ADR-0006` | Superseded: enforcement is zone-scoped, never a global flag |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
| `ADR-0009` | Adopt security-zones v0.1 and compile explicit workload membership; PEP failure mode is per zone |
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
credential-management standard, the-custodian's ADR-001 workplan convention — are
@ -188,7 +188,8 @@ for the rest.
- `cert_command`: `warden sign <actor> --pubkey <path>` → cert on stdout
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
- `warden status`, cleanup, scorecard, signatures log
- Opt-in flex-auth policy gate (`policy.enabled`, `policy_decision_id` in log)
- Zone-aware flex-auth policy gate (`policy_decision_id`, zone, failure mode, and
outcome in the signing audit; no repo-wide enable switch)
- Production flex-auth registry builder (`scripts/build_flex_auth_registry.py`,
`registry/flex-auth/production_registry_snapshot.json`)
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
@ -247,7 +248,7 @@ for the rest.
| --- | --- |
| WP-00010005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
| WP-0006 | Credential routing, security map, inventory patterns, OpenBao checklist |
| WP-0007 | Opt-in flex-auth policy gate (`policy.enabled`) |
| WP-0007 | Original opt-in flex-auth policy gate (global switch retired by WP-0032) |
| WP-0008 | Production sign verification, stewardship closeout, archive hygiene |
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
| WP-0010 | Access routing charter + pointer catalog |
@ -280,7 +281,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-0032 | `proposed` | Adopt security zones as a consumer — retire the global `policy.enabled` (model owned by `zone-engine`) |
| WP-0032 | `finished` | Security zones adopted — global switch retired, explicit workload references compiled, and owner policy live |
| 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).
@ -356,10 +357,10 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
- **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived).
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 `zone-engine` defines (`ZONE-WP-0001`). Re-check with
`scripts/check_policy_caller_identity.py`
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). WP-0032 and
`ADR-0009` retired the global switch: the compiled target workload selects the
zone, flex-auth owns stance, and ops-warden applies the zone's PEP failure mode.
Re-check caller identity 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 +

View file

@ -28,7 +28,7 @@
| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md |
| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| workplan | WARDEN-WP-0032 | proposed | — | workplans/WARDEN-WP-0032-security-zones.md |
| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md |
| workplan | WARDEN-WP-0033 | active | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
| task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md |
@ -117,11 +117,12 @@
| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T02 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T03 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T04 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T02 | progress | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T06 | progress | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |

View file

@ -4,18 +4,18 @@ type: adr
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
domain: infotech
repo: ops-warden
status: accepted
status: superseded
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-19"
updated: "2026-08-19"
updated: "2026-08-22"
last_reviewed: "2026-08-19"
review_interval: 6m
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
supersedes: ""
successor: ""
successor: "ops-warden-adr-0009"
---
# ADR-0006 — Enforcement is zone-scoped, never a global flag

View file

@ -0,0 +1,101 @@
---
id: ops-warden-adr-0009
type: adr
title: "ADR-0009 — Adopt security-zones v0.1 as a consumer"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-22"
updated: "2026-08-22"
last_reviewed: "2026-08-22"
review_interval: 3m
enforced_by: "tenancy.yaml; registry/routing/catalog.yaml workload_ref; scripts/build_flex_auth_registry.py; src/warden/policy.py; src/warden/config.py"
supersedes: "ops-warden-adr-0006"
successor: ""
---
# ADR-0009 — Adopt security-zones v0.1 as a consumer
## Status
Accepted 2026-08-22 after zone-engine completed `ZONE-WP-0001-T03/T05` and
published the declaration, compilation, stance, and failure-mode contract in
canon revision `337484a`; zone-engine's reference compiler is revision
`9b6ada7`.
## Context
ADR-0006 rejected a repo-wide `policy.enabled` switch because one boolean plus
one `fail_closed` value made flex-auth a uniform dependency of every signing
path, including continuity paths needed to repair that dependency. It deferred
the replacement to zone-engine rather than designing an estate model here.
The owning model now exists. A zone is an evidenced workload-admission fact;
control stance remains with the control owner, and dependency failure behavior
remains with the PEP. Membership resolves only through an authoritative
workload identity. Missing identity, membership, admission evidence, or a
required floor is `unknown`, never an inferred permissive zone.
## Decision
Ops-warden adopts `security-zones_v0.1` and accepts its initial build-stage rows
for the controls ops-warden owns:
- the pre-sign PEP fails open for `z0-experimental`, `z1-operational`,
`z2-protected`, `z2-continuity`, and build-profile `unknown`; it fails closed
for `z3-critical`;
- the agent high-risk read boundary remains enforced and fail-closed in every
zone and for `unknown`;
- `warden plan` never derives `autonomous` authority from unknown zone evidence.
The implementation follows four rules:
1. `policy.enabled` and the global `policy.fail_closed` setting are retired and
rejected by configuration loading. The PEP chooses failure behavior from a
total per-zone map.
2. The existing compiled flex-auth registry is the resource-membership carrier.
Actor resources receive `workload_id`, `security_zone`,
`security_zone_admission`, and `security_zone_revision`. The dormant
`trust_zone: platform` constant is removed; it is not repurposed.
3. Workload joins are explicit. Managed deployables use Repo Manager's exact
`(rapp_id, workload_identity.name, deployable?)` tuple. Independent
operational workloads use their owner-reviewed `tenancy.yaml`. Catalog
owners distinguish `not-applicable` from applicable-but-`unknown`; no path or
repository-name inference is allowed.
4. A fail-open signing result is metadata, not silence. Signature and unified
audit records carry the selected zone, failure mode, outcome, and decision id
when one exists.
Ops-warden itself declares `z1-operational`. That is an accuracy decision: the
workload has M1 evidence and does not yet have the SLO history, on-call rotation,
or exercised recovery evidence needed for z2 admission.
## Consequences
The global flip and its failure cycle no longer exist. An unknown target remains
observable and follows the versioned build profile without manufacturing
membership. A future organization-posture graduation changes the versioned
control profile, not each workload declaration.
The flex-auth policy package still owns pre-sign stance. Ops-warden can compile
and send the membership attributes, handle `allow`/`audit_only`/deny, and apply
the correct PEP failure mode; it does not write flex-auth's Rego rows.
Catalog coverage is intentionally honest at adoption: exact references resolve
where authoritative declarations exist, applicable lanes without one report
`unknown` with a reason, and generic actions/patterns are explicitly
`not-applicable`. Resolution coverage improves by adding owner declarations,
never by adding heuristics here.
## Related
- `security-zones_v0.1` (net-kingdom canon revision `337484a`; zone-engine
compiler revision `9b6ada7`)
- Repo Manager `helixforge.workloads.ops-warden-reference.v1` revision `890f3b0`
- NetKingdom tenancy-posture Decisions 5.6.1/5.6.2
- `WARDEN-WP-0032`
- `ADR-0004`, `ADR-0007`, and `ADR-0008`

View file

@ -67,6 +67,7 @@ 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 |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (**superseded by ADR-0009**) | ops-warden |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden |
| `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden |
| `ADR-0009` | Adopt security-zones v0.1; compile explicit membership and select PEP failure mode per zone | ops-warden |

View file

@ -0,0 +1,34 @@
# Ops-warden security-zone admission evidence — 2026-08-22
This record supports the `z1-operational` membership declared in
`tenancy.yaml`. It does not claim the M2 gates that ops-warden has not met.
## Identity and scope
- Workload id: `ops-warden`.
- Runtime binding: Kubernetes ServiceAccount
`system:serviceaccount:ops-warden:ops-warden`, issued by railiance01 and
verified against the enforcing flex-auth pin on 2026-08-19.
- Responsible party: `team:platform-security` in this repository.
- Scope: attended issuance of short-lived SSH certificates plus a pointer-only
credential catalog. Secret values are not stored in the catalog or audit.
## M1 evidence
- Owned front door: `warden sign` is the sole certificate-issuance interface;
actor inventory, principal allow-list, and TTL ceilings are enforced before
the CA backend.
- Basic service objective: production signing is bounded by the actor TTL
policy (`adm` 48h, `agt` 24h, `atm` 8h); `warden status` and the production
verification records expose backend readiness.
- Data handling: `ADR-0002` makes ops-warden a transparent conduit and
`ADR-0004`/`ADR-0007` prevent raw agent reads and fail safe on ungraded lanes.
- Policy path: `history/2026-08-19-flex-auth-caller-identity-evidence.md` proves
the authenticated caller path and anonymous rejection.
## Why not z2
Ops-warden has security review artifacts, but not the complete M2 promotion
set: there is no SLO history, on-call rotation, or exercised signing-path
incident/recovery runbook. Its tenancy posture therefore remains V0 and its
accurate zone membership remains `z1-operational`.

View file

@ -10,6 +10,9 @@ actors:
- agt-task-bridge
ttl_hours: 24
description: "ops-bridge tunnel agent for state-hub"
zone_subject:
applicability: applicable
workload_id: ops-bridge-tunnel
agt-codex-interhub-bootstrap:
type: agt
@ -17,6 +20,9 @@ actors:
- agt-interhub-bootstrap
ttl_hours: 2
description: "Short-lived agent access for attended Inter-Hub bootstrap"
zone_subject:
applicability: applicable
workload_id: codex-interhub-bootstrap
adm-example:
type: adm
@ -24,6 +30,9 @@ actors:
- adm-full
ttl_hours: 48
description: "Example human operator — replace with per-person adm-* actors"
zone_subject:
applicability: not-applicable
reason: human operator retains native actor identity
atm-backup-daily:
type: atm
@ -31,6 +40,9 @@ actors:
- atm-backup-daily
ttl_hours: 8
description: "Example nightly automation actor"
zone_subject:
applicability: applicable
workload_id: backup-daily
hosts:
example-host:
@ -38,4 +50,4 @@ hosts:
agt:
- agt-task-bridge
atm:
- atm-backup-daily
- atm-backup-daily

View file

@ -15,18 +15,24 @@ vault:
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Opt-in flex-auth gate — enable only when flex-auth is reachable at flex_auth_url.
# Zone-aware flex-auth gate. Missing target membership is the explicit unknown
# profile; there is no repo-wide enable switch.
# Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory).
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
policy:
# Keep false until the flex-auth ops-warden pin runs callerAuth.mode: enforce.
# Gate: python scripts/check_policy_caller_identity.py (WARDEN-WP-0031)
enabled: false
# The in-cluster pin for ops-warden's signing policy (FLEX-WP-0016). A bare
# flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it
# through a port-forward or tunnel and point this at that local address.
flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
fail_closed: true
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
@ -44,4 +50,4 @@ policy:
# command: kubectl create token ops-warden -n ops-warden
# --audience flex-auth --duration 10m
token_env: WARDEN_POLICY_CALLER_TOKEN
audience: flex-auth
audience: flex-auth

View file

@ -48,8 +48,8 @@
],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"ops_warden_policy_gate": "security-zones-v0.1",
"security_zone_standard": "security-zones_v0.1",
"tenant": "tenant:platform"
}
}
@ -66,7 +66,6 @@
"ssh-signing",
"adm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "adm-example",
@ -78,7 +77,10 @@
"allowed_principals": [
"adm-full"
],
"max_ttl_hours": 48
"max_ttl_hours": 48,
"security_zone": "unknown",
"security_zone_admission": "not-applicable",
"security_zone_reason": "human operator retains native actor identity"
}
},
{
@ -88,7 +90,6 @@
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-codex-interhub-bootstrap",
@ -100,7 +101,11 @@
"allowed_principals": [
"agt-interhub-bootstrap"
],
"max_ttl_hours": 2
"max_ttl_hours": 2,
"workload_id": "codex-interhub-bootstrap",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
},
{
@ -110,7 +115,6 @@
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-state-hub-bridge",
@ -122,7 +126,11 @@
"allowed_principals": [
"agt-task-bridge"
],
"max_ttl_hours": 24
"max_ttl_hours": 24,
"workload_id": "ops-bridge-tunnel",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
},
{
@ -132,7 +140,6 @@
"ssh-signing",
"atm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "atm-backup-daily",
@ -144,7 +151,11 @@
"allowed_principals": [
"atm-backup-daily"
],
"max_ttl_hours": 8
"max_ttl_hours": 8,
"workload_id": "backup-daily",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
}
],

View file

@ -10,11 +10,11 @@
# declares it, and is null where the field set has not been established --
# null means unknown, never 'one field'.
generated_at: "2026-08-21T11:28:33Z"
generated_at: "2026-08-22T12:59:27Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
catalog_revision_date: "2026-08-21T13:26:10+02:00"
catalog_dirty: false
catalog_dirty: true
high_risk_lane_count: 19
concrete_path_count: 14

View file

@ -26,6 +26,11 @@
# canon_ref upstream net-kingdom doc the wiki section tracks
# reviewed date this pointer was last checked against canon (YYYY-MM-DD)
# status active (surfaced by default) | draft (hidden unless --all)
# workload_ref explicit workload applicability and authoritative join.
# Managed deployables use rapp_id + name (+ optional deployable);
# operational workloads use name + declaration_ref; unresolved
# applicable lanes carry unknown_reason; native non-workload
# subjects carry not-applicable + reason. Never infer from paths.
# steps ONLY when warden_executes: true
# cert_command ONLY when warden_executes: true
# delegation WP-0030 register. mode: native | interim | permanent.
@ -39,6 +44,9 @@ entries:
title: Short-lived SSH certificate for host / ops reachability
# Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: not-applicable
reason: "Generic certificate-signing lane; each actor resource must resolve its own target workload."
need_keywords: [ssh, certificate, cert, host, access, sign, adm, agt, atm, reachability, ops]
owner_repo: ops-warden
subsystem: ops-warden
@ -61,6 +69,10 @@ entries:
title: Scoped OpenBao token for ops-warden SSH signing (warden-sign)
# A scoped VAULT_TOKEN is a credential in its own right. Graded on what the value is, not on whether ops-warden currently proxies it (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: applicable
name: ops-warden
declaration_ref: tenancy.yaml
need_keywords: [vault_token, vault, token, warden-sign, warden, ops-warden, signing, sign, smoke, flex-auth, credential, broker, lease, openbao, ssh, production]
owner_repo: railiance-platform
subsystem: OpenBao credential broker
@ -97,6 +109,9 @@ entries:
title: API key, DB credential, or dynamic lease
# Wildcard lane over platform/workloads/<domain>/<workload>/<bundle>: its ceiling is the most dangerous bundle it can resolve to (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: not-applicable
reason: "Generic credential-path pattern; concrete lanes carry the workload reference."
need_keywords: [api, key, secret, database, db, password, token, lease, openbao, vault, kv, dynamic, credential, npm, npm_auth_token, registry]
owner_repo: railiance-platform
subsystem: OpenBao
@ -133,6 +148,9 @@ entries:
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "whynot-design has not published an authoritative workload identity declaration."
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package]
owner_repo: railiance-platform
subsystem: OpenBao
@ -177,6 +195,9 @@ entries:
title: Authorization decision — may this actor perform this action
# Returns an authorization decision; no credential flows (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: not-applicable
reason: "Generic authorization action; the governed resource supplies workload identity."
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision]
owner_repo: flex-auth
subsystem: flex-auth
@ -195,6 +216,9 @@ entries:
title: Interactive login, OIDC token, or MFA
# Interactive browser OIDC: a login flow, not a KV read. No stored value is fetched, and warden access already excludes is_login from raw-value streaming (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: not-applicable
reason: "Interactive login action; caller identity is native context, not a workload target."
need_keywords: [login, oidc, identity, mfa, token, jwt, sso, keycloak, key-cape, iam, claims, authenticate, signin]
owner_repo: key-cape
subsystem: key-cape / Keycloak
@ -222,6 +246,9 @@ entries:
title: SSH tunnel or port forward
# Routes to ops-bridge and supplies a cert_command; no secret value flows (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: applicable
unknown_reason: "ops-bridge has not published the operational tunnel workload declaration."
need_keywords: [tunnel, port, forward, bridge, ops-bridge, reverse, transport, ssh-tunnel, cert_command]
owner_repo: ops-bridge
subsystem: ops-bridge
@ -240,6 +267,9 @@ entries:
title: Host SSH principal file or force-command deployment
# Principal-file deployment via Ansible; no secret value flows (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: not-applicable
reason: "Host principal-file deployment is an infrastructure action, not a workload."
need_keywords: [principal, auth_principals, force-command, host, sshd, hardening, railiance-infra, ansible]
owner_repo: railiance-infra
subsystem: railiance-infra
@ -258,6 +288,9 @@ entries:
title: Inter-Hub bootstrap SSH envelope
# Graded high conservatively: ops-warden could not establish from the lane definition that no key material moves in the envelope. Regrade with evidence, do not assume down (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The inter-hub bootstrap execution unit has no authoritative workload declaration."
need_keywords: [inter-hub, interhub, bootstrap, ops-hub, agt-interhub-bootstrap, envelope, force-command, CUST-WP-0049]
owner_repo: ops-warden
subsystem: ops-warden + railiance-infra
@ -276,6 +309,9 @@ entries:
title: activity-core IssueSink → issue-core REST emission
# Emission routing only — the API key is a separate lane (WARDEN-WP-0032-T05).
risk: standard
workload_ref:
applicability: applicable
unknown_reason: "activity-core has not published an authoritative workload identity declaration."
need_keywords: [activity-core, issue-sink, issue-core, emission, issue_core_url, issue_core_api_key, tasks, ingest, rest, issuesink]
owner_repo: activity-core
subsystem: activity-core + issue-core
@ -301,6 +337,11 @@ entries:
# A Forgejo backend token is not recovered by rotating an ingestion key.
# Found by secrets-engine reviewing SECRETS-WP-0006 -- not by us.
risk: high
workload_ref:
applicability: applicable
rapp_id: rapp-issue-core
name: issue-core
deployable: issue-core
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN] # CCR-2026-0002
need_keywords: [issue-core, ingestion, api, key, openbao, issue_core_api_key, eso, external-secrets]
owner_repo: railiance-platform
@ -349,6 +390,9 @@ entries:
# rotation. Disclosure lets an attacker forge webhook deliveries into the
# federation hub, which rotating the write token alone does not undo.
risk: high
workload_ref:
applicability: applicable
unknown_reason: "reuse-surface has not published an authoritative workload identity declaration."
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET] # CCR-2026-0005
need_keywords: [reuse-surface, reuse_surface, hub, register, federation, write, token, bearer, REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET, reuse.coulomb.social]
owner_repo: railiance-platform
@ -404,6 +448,9 @@ entries:
verified: owner-confirmed
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "llm-connect has not published an authoritative workload identity declaration."
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0003 / RAILIANCE-WP-0010
# (promoted 2026-07-02): policy workload-kv-read-llm-connect-provider-secrets and k8s
# auth role external-secrets-activity-core applied; ExternalSecret
@ -445,6 +492,9 @@ entries:
verified: unverified
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The backup execution unit has no authoritative workload identity declaration."
# CCR-2026-0004: policy + OIDC role applied; values provisioned 2026-07-07.
# Capabilities-safe re-verify 2026-07-16 (WP-0026 T07): lane-policy token
# capabilities=read on data path; default-policy and agent-high-risk-boundary = deny;
@ -491,6 +541,9 @@ entries:
verified: owner-confirmed
# High-risk: site-admin PAT (WP-0026 T04).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The Forgejo administration workload has no authoritative workload identity declaration."
# CCR-2026-0006: approved by platform-operator 2026-07-12; policy
# workload-kv-read-forgejo-admin + OIDC role forgejo-admin-workload-kv-read live on
# bao.coulomb.social; PAT attended-minted and stored under field API_TOKEN at
@ -533,6 +586,9 @@ entries:
reviewed: "2026-08-11"
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The Binky email consumer has no authoritative workload identity declaration."
# CCR-2026-0007: tenants/ mount + policy + OIDC role applied; founder provisioned
# values via UI (version ≥2, not placeholder). Capabilities-safe verify 2026-07-17:
# lane-policy read; default deny. Host: imap.ionos.de:993 (binky-control config).
@ -568,6 +624,9 @@ entries:
reviewed: "2026-08-11"
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The Binky Qonto MCP consumer has no authoritative workload identity declaration."
# CCR-2026-0008: policy + OIDC role applied; secret at tenants/binky/qonto-api
# (fields API_KEY, API_USER). Map to QONTO_API_KEY / QONTO_ORGANIZATION_ID for
# qonto-mcp-server. First read-only pull 2026-07-21 (BINKY-WP-0005-T05).
@ -604,6 +663,11 @@ entries:
reviewed: "2026-08-17"
verified: unverified
risk: high
workload_ref:
applicability: applicable
rapp_id: rapp-qonto
name: qonto
deployable: rapp-qonto
auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange"
path_template: "platform/workloads/rapp-qonto/keycape-client"
fetch_command: "bao kv get -field=client_secret platform/workloads/rapp-qonto/keycape-client"
@ -635,6 +699,9 @@ entries:
reviewed: "2026-08-11"
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: "agent-harness has not published an authoritative workload identity declaration."
# Provisioned 2026-07-17 on railiance01: ed25519 keypair on host, OpenBao copy at
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
# coulomb/executor-sandbox (title agent-harness-railiance01). Git push verified.
@ -671,6 +738,9 @@ entries:
reviewed: "2026-08-15"
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: "agent-harness has not published an authoritative workload identity declaration."
# Provisioned 2026-07-17: role agent-harness-binky-mail bound to existing policy
# workload-kv-read-binky-company-email-imap; role_id/secret_id delivered to
# railiance01 ~/.local/agent-harness/approle-binky-mail (0600). Positive IMAP field
@ -696,6 +766,9 @@ entries:
title: Object-storage STS / temporary S3 credentials
# Temporary S3 credentials are still credentials (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: not-applicable
reason: "Generic STS credential-vending pattern; concrete consumers carry workload references."
need_keywords: [s3, sts, object-storage, minio, artifact-store, temporary, credentials, bucket, vending]
owner_repo: net-kingdom
subsystem: flex-auth + OpenBao + artifact-store
@ -726,6 +799,9 @@ entries:
reviewed: "2026-08-15"
verified: unverified
risk: high
workload_ref:
applicability: not-applicable
reason: "Generic database credential-vending pattern; concrete consumers carry workload references."
exec_capable: false
- id: rein-openweights-openrouter-approle
@ -745,6 +821,9 @@ entries:
reviewed: "2026-08-15"
verified: unverified
risk: standard
workload_ref:
applicability: applicable
unknown_reason: "rein-openweights has not published an authoritative workload identity declaration."
# Built 2026-07-27 by ops-mason (MASON-WP-0001-T05), approved by Bernd
# Worsch 2026-07-27. Policy + AppRole live; reins/ KV v2 mount created
# (no existing mount fit without widening scope beyond what was
@ -805,6 +884,9 @@ entries:
# railiance-apps; user-engine is consumer-only and claims no lane here.
consumers: [user-engine]
risk: standard
workload_ref:
applicability: applicable
unknown_reason: "coulomb-social has not published an authoritative workload identity declaration."
# K8s assembly is the live handoff today (same pattern as vergage-teilnahme-env).
# OpenBao KV platform/workloads/coulomb/coulomb-social/runtime-env is the
# future custody home — CCR not yet applied; resolvable via operator script.
@ -827,6 +909,9 @@ entries:
title: audit-core sender registry (write and operator-read tokens)
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "audit-core has not published an authoritative workload identity declaration."
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS]
owner_repo: ops-mason
subsystem: OpenBao + audit-core
@ -864,6 +949,9 @@ entries:
reviewed: "2026-08-21"
verified: owner-confirmed
risk: high
workload_ref:
applicability: applicable
unknown_reason: "email-connect has not published an authoritative workload identity declaration."
# CCR-2026-0010 approved 2026-08-12; applied same day (EMAIL-WP-0004-T03):
# policies external-secrets-email-connect + workload-kv-read-email-connect-transactional,
# KV platform/workloads/email-connect/transactional v1, ESO token Secret,
@ -906,6 +994,9 @@ entries:
reviewed: "2026-08-15"
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: "The attended Scaleway bootstrap execution unit has no authoritative workload identity declaration."
# CCR-2026-0011. Values via founder paste-once or local tfvars ingest.
# Not the Barman runtime key (platform-pg-backup-s3).
auth_method: "caller's own OpenBao token (founder / operator workstation)"

View file

@ -55,12 +55,79 @@ def _caring_descriptor(actor_type: str, resource_id: str) -> dict[str, Any]:
}
def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
def _resolved_by_workload(zone_resolutions: dict[str, Any] | None) -> dict[str, Any]:
records = (zone_resolutions or {}).get("records") or []
resolved: dict[str, Any] = {}
for record in records:
workload_id = str(record.get("workload_id") or "")
if not workload_id:
continue
if workload_id in resolved:
raise ValueError(f"duplicate security-zone resolution for {workload_id!r}")
resolved[workload_id] = record
return resolved
def _zone_attributes(
actor: str,
entry: dict[str, Any],
resolutions: dict[str, Any],
) -> dict[str, Any]:
subject = entry.get("zone_subject")
if not isinstance(subject, dict):
return {
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "catalog_applicability_absent",
}
applicability = subject.get("applicability")
if applicability == "not-applicable":
reason = str(subject.get("reason") or "").strip()
if not reason:
raise ValueError(f"{actor}.zone_subject.reason is required")
return {
"security_zone": "unknown",
"security_zone_admission": "not-applicable",
"security_zone_reason": reason,
}
if applicability != "applicable":
raise ValueError(
f"{actor}.zone_subject.applicability must be applicable or not-applicable"
)
workload_id = str(subject.get("workload_id") or "").strip()
if not workload_id:
return {
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_reference_absent",
}
record = resolutions.get(workload_id)
if record is None:
return {
"workload_id": workload_id,
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent",
}
return {
"workload_id": workload_id,
"security_zone": str(record.get("effective_zone") or "unknown"),
"security_zone_declared": record.get("declared_zone"),
"security_zone_admission": str(record.get("admission") or "unknown"),
"security_zone_reason": str(record.get("admission_reason") or "unknown"),
"security_zone_revision": record.get("membership_revision"),
}
def build_registry(
inventory: dict[str, Any], zone_resolutions: dict[str, Any] | None = None
) -> dict[str, Any]:
actors: dict[str, Any] = inventory.get("actors") or {}
resources: list[dict[str, Any]] = []
subjects: list[dict[str, Any]] = []
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
relationships: list[dict[str, Any]] = []
resolutions = _resolved_by_workload(zone_resolutions)
for name, entry in sorted(actors.items()):
actor_type = str(entry["type"])
@ -74,7 +141,6 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"id": resource_id,
"type": "ssh-certificate",
"labels": ["ssh-signing", actor_type],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": name,
@ -82,6 +148,7 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"allowed_subjects": [name, f"iam:{name}"],
"allowed_principals": principals,
"max_ttl_hours": ttl_hours,
**_zone_attributes(name, entry, resolutions),
},
}
)
@ -156,8 +223,8 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"caring_profiles": ["caring-0.4.0-rc2"],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"ops_warden_policy_gate": "security-zones-v0.1",
"security_zone_standard": "security-zones_v0.1",
"tenant": "tenant:platform",
},
}
@ -186,14 +253,24 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
parser.add_argument("-o", "--output", type=Path, required=True)
parser.add_argument(
"--zone-resolutions",
type=Path,
help="zone-engine resolved-view JSON; absent references remain unknown",
)
args = parser.parse_args()
inventory = yaml.safe_load(args.inventory.read_text()) or {}
registry = build_registry(inventory)
zone_resolutions = (
json.loads(args.zone_resolutions.read_text())
if args.zone_resolutions is not None
else None
)
registry = build_registry(inventory, zone_resolutions)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(registry, indent=2) + "\n")
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")
if __name__ == "__main__":
main()
main()

View file

@ -24,7 +24,6 @@ from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys

View file

@ -1,13 +1,13 @@
#!/usr/bin/env python3
"""Readiness gate for the flex-auth ``policy.enabled`` flip (WARDEN-WP-0031).
"""Readiness gate for the zone-aware flex-auth caller identity.
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
but a caller that sends no ``Authorization`` header only produces a
``caller authentication warning`` and is still served. That pin cannot move to
``enforce`` and therefore ``policy.enabled: true`` cannot be set until
ops-warden's calling side actually presents a token.
``enforce`` until ops-warden's calling side actually presents a token. The
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
This script asserts the calling side *without* flipping anything:
@ -57,9 +57,7 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
return [("fail", "warden.yaml", str(e))]
policy = cfg.policy
checks.append(
("ok", "warden.yaml", f"loaded; policy.enabled={str(policy.enabled).lower()}")
)
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
mode = policy.caller_auth.mode
if mode == "none":
@ -84,12 +82,12 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
)
target = url or policy.flex_auth_url
if url is None and not policy.enabled:
if target is None:
checks.append(
(
"skip",
"live /v1/check",
f"policy.enabled=false; pass --url to smoke {target} anyway",
"policy.flex_auth_url is absent; pass --url to run the live smoke",
)
)
return checks
@ -188,13 +186,13 @@ def main() -> int:
if failed:
print(
f"\nNOT READY — {len(failed)} check(s) failed. "
"Do not ask flex-auth to enforce, and do not set policy.enabled: true."
"Do not ask flex-auth to enforce caller authentication."
)
return 1
print(
"\nREADY — the calling side presents an identity. Next: tell flex-auth to set "
"callerAuth.mode: enforce on flex-auth-ops-warden, re-run this check, then set "
"policy.enabled: true with fail_closed: true."
"callerAuth.mode: enforce on flex-auth-ops-warden and re-run this check. "
"Zone-specific PEP failure modes already replace the retired global switches."
)
return 0

View file

@ -136,8 +136,13 @@ def main() -> int:
if args.check:
current = args.out.read_text() if args.out.exists() else ""
# generated_at always differs; compare everything else.
strip = lambda t: "\n".join(l for l in t.splitlines() if not l.startswith("generated_at:"))
if strip(current) != strip(content):
def strip_generated_at(text: str) -> str:
return "\n".join(
line for line in text.splitlines()
if not line.startswith("generated_at:")
)
if strip_generated_at(current) != strip_generated_at(content):
print(f"STALE: {args.out} does not match the catalog. Re-run without --check.")
return 1
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")

View file

@ -66,9 +66,8 @@ ca_key: $SMOKE_DIR/ca_key
state_dir: $SMOKE_DIR/state
inventory_path: $INVENTORY
policy:
enabled: true
flex_auth_url: http://$ADDR
fail_closed: true
zone_registry_path: $REGISTRY
tenant: tenant:platform
system: ops-warden
EOF
@ -106,9 +105,8 @@ vault:
inventory_path: $INVENTORY
state_dir: $SMOKE_DIR/state-vault
policy:
enabled: true
flex_auth_url: http://$ADDR
fail_closed: true
zone_registry_path: $REGISTRY
tenant: tenant:platform
system: ops-warden
EOF
@ -118,4 +116,4 @@ EOF
python3 -c "import json,sys; e=json.loads(sys.argv[1]); assert e.get('backend')=='vault' and e.get('policy_decision_id'); print('vault policy_decision_id:', e['policy_decision_id'])" "$VAULT_LINE"
fi
echo "OK — production registry policy gate smoke passed"
echo "OK — production registry policy gate smoke passed"

234
scripts/report_workload_join.py Executable file → Normal file
View file

@ -1,128 +1,156 @@
#!/usr/bin/env python3
"""Report the lane -> workload join, for the security-zone model (ZONE-WP-0001-T03).
"""Report explicit lane -> workload resolution for security-zones_v0.1.
A security zone's subject is the **workload**, not the lane — policy is about the
running thing and whoever answers for it. ops-warden's catalog is a credential
surface, so on its own it cannot name that subject. The other side of the join
lives in `rapp-*/declarations/rapp.yaml`, which declares `workload_identity`
along with `data_classification` and `criticality`; ops-warden's
`registry/policy/security-posture.yaml` then maps classification to a minimum
maturity via `dataclass_floor`.
The catalog owner declares whether each lane is workload-applicable. Managed
deployables use the exact Repo Manager v1 ``(rapp_id, name, deployable?)``
reference. Independently governed operational workloads use ``name`` plus an
owner declaration reference. Unknown and not-applicable are explicit results.
This script computes the join and reports its coverage. It asserts nothing it
cannot derive: a lane whose workload cannot be established is reported as
unmatched rather than guessed, because the unmatched set is the informative
output those are lanes existing for something that is not a declared workload.
Read-only. Touches no secret value and no live system.
Usage:
python scripts/report_workload_join.py [--rapp-root ~] [--json]
This script never parses a credential path, consults ``owner_repo`` as an
identity hint, or substitutes a repository name. It reads declarations only.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any
_SRC = Path(__file__).resolve().parent.parent / "src"
if _SRC.is_dir() and str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
import yaml # noqa: E402
import yaml
def load_rapp_workloads(root: Path) -> Dict[str, Dict[str, Any]]:
"""workload name -> declaration, from every rapp-*/declarations/rapp.yaml."""
out: Dict[str, Dict[str, Any]] = {}
def load_rapp_workloads(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
"""Exact Repo Manager v1 key -> authoritative declaration projection."""
out: dict[tuple[str, str], dict[str, Any]] = {}
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
try:
data = yaml.safe_load(decl.read_text()) or {}
except yaml.YAMLError:
continue
identity = data.get("workload_identity") or {}
rapp_id = data.get("rapp_id")
name = identity.get("name")
if not name:
if not rapp_id or not name:
continue
out[str(name)] = {
"rapp_id": data.get("rapp_id"),
deployables = []
for member in (data.get("composition") or {}).get("member_repos") or []:
deployables.extend(str(value) for value in member.get("deployables") or [])
out[(str(rapp_id), str(name))] = {
"source": str(decl),
"deployables": sorted(set(deployables)),
"data_classification": data.get("data_classification"),
"criticality": data.get("criticality"),
"readiness_state": data.get("readiness_state"),
"bound_reefs": data.get("bound_reefs"),
}
return out
def candidate_workloads(path_template: Optional[str]) -> List[str]:
"""Workload names a path could plausibly name — never a single guess.
The convention is `platform/workloads/<domain>/<workload>/<bundle>`, but it
is not applied consistently: `platform/workloads/rapp-qonto/keycape-client`
has one level fewer, so position alone cannot say which segment is the
workload. Both are offered and matched against declarations.
"""
if not path_template or "<" in path_template:
return []
parts = [p for p in path_template.split("/") if p]
if len(parts) >= 4 and parts[1] == "workloads":
return [parts[3], parts[2]]
if parts and parts[0] == "tenants" and len(parts) >= 3:
return [parts[2], parts[1]]
return []
def _direct_declaration_path(
declaration_ref: str, *, catalog_path: Path, estate_root: Path
) -> Path:
ref = Path(declaration_ref)
if ref.is_absolute():
return ref
local = catalog_path.resolve().parents[2] / ref
return local if local.exists() else estate_root / ref
def build(catalog_path: Path, rapp_root: Path) -> Dict[str, Any]:
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
declared = load_rapp_workloads(rapp_root)
floor = (
yaml.safe_load(
(catalog_path.parent.parent / "policy" / "security-posture.yaml").read_text()
def _resolve_direct(
ref: dict[str, Any], *, catalog_path: Path, estate_root: Path
) -> tuple[dict[str, Any] | None, str | None]:
source = _direct_declaration_path(
str(ref["declaration_ref"]), catalog_path=catalog_path, estate_root=estate_root
)
if not source.exists():
return None, f"declaration not found: {source}"
try:
declaration = yaml.safe_load(source.read_text()) or {}
except yaml.YAMLError as exc:
return None, f"invalid declaration YAML: {exc}"
identity = declaration.get("workload_identity") or {}
if identity.get("name") != ref.get("name"):
return None, (
f"declared workload_identity.name={identity.get('name')!r}, "
f"expected {ref.get('name')!r}"
)
or {}
).get("dataclass_floor", {})
context = (declaration.get("zones") or {}).get("context", {})
return {
"source": str(source),
"data_classification": context.get("data_classification"),
"criticality": context.get("criticality"),
"maturity": context.get("maturity"),
"declared_zone": (declaration.get("zones") or {}).get("membership"),
}, None
matched, unmatched, no_path = [], [], []
def build(catalog_path: Path, estate_root: Path) -> dict[str, Any]:
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
managed = load_rapp_workloads(estate_root)
posture_path = catalog_path.parent.parent / "policy" / "security-posture.yaml"
floor = (yaml.safe_load(posture_path.read_text()) or {}).get("dataclass_floor", {})
resolved: list[dict[str, Any]] = []
unknown: list[dict[str, Any]] = []
not_applicable: list[dict[str, Any]] = []
for entry in entries:
lane = entry.get("id")
cands = candidate_workloads(entry.get("path_template"))
if not cands:
no_path.append({"lane": lane, "risk": entry.get("risk")})
lane = str(entry.get("id"))
ref = entry.get("workload_ref") or {}
applicability = ref.get("applicability")
if applicability == "not-applicable":
not_applicable.append({"lane": lane, "reason": ref.get("reason")})
continue
hit = next((c for c in cands if c in declared), None)
if hit is None:
unmatched.append(
{"lane": lane, "risk": entry.get("risk"), "candidates": cands}
if applicability != "applicable":
unknown.append({"lane": lane, "reason": "applicability missing or invalid"})
continue
if ref.get("unknown_reason"):
unknown.append({"lane": lane, "reason": ref["unknown_reason"]})
continue
projection: dict[str, Any] | None
error: str | None = None
if ref.get("rapp_id"):
key = (str(ref.get("rapp_id")), str(ref.get("name")))
projection = managed.get(key)
if projection is None:
error = f"Repo Manager reference does not resolve: {key[0]}/{key[1]}"
elif ref.get("deployable") and ref["deployable"] not in projection["deployables"]:
error = f"deployable {ref['deployable']!r} is not declared by {key[0]}/{key[1]}"
else:
projection, error = _resolve_direct(
ref, catalog_path=catalog_path, estate_root=estate_root
)
if error or projection is None:
unknown.append({"lane": lane, "reason": error or "reference unresolved"})
continue
decl = declared[hit]
cls = decl.get("data_classification")
matched.append(
classification = projection.get("data_classification")
resolved.append(
{
"lane": lane,
"workload": hit,
"risk": entry.get("risk"),
"data_classification": cls,
"criticality": decl.get("criticality"),
"min_maturity": floor.get(cls),
"unmapped_classification": bool(cls) and cls not in floor,
"workload_ref": ref,
"source": projection.get("source"),
"data_classification": classification,
"criticality": projection.get("criticality"),
"maturity": projection.get("maturity") or floor.get(classification),
"declared_zone": projection.get("declared_zone"),
"unmapped_classification": bool(classification) and classification not in floor,
}
)
return {
"declared_workloads": declared,
"matched": matched,
"unmatched": unmatched,
"no_path": no_path,
"dataclass_floor": floor,
"contract": "helixforge.workload-reference/v1",
"resolved": resolved,
"unknown": unknown,
"not_applicable": not_applicable,
"ok": len(resolved) + len(unknown) + len(not_applicable) == len(entries),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--rapp-root", type=Path, default=Path.home())
parser.add_argument(
"--estate-root", "--rapp-root", dest="estate_root", type=Path, default=Path.home()
)
parser.add_argument("--json", action="store_true")
parser.add_argument(
"--catalog",
@ -133,40 +161,24 @@ def main() -> int:
/ "catalog.yaml",
)
args = parser.parse_args()
report = build(args.catalog, Path(os.path.expanduser(str(args.rapp_root))))
report = build(args.catalog, args.estate_root)
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
return 0
return 0 if report["ok"] else 1
print("lane -> workload join\n")
print(f" declared workloads (rapp-*): {len(report['declared_workloads'])}")
print(f" lanes matched to a workload: {len(report['matched'])}")
print(f" lanes unmatched: {len(report['unmatched'])}")
print(f" lanes with no usable path: {len(report['no_path'])}\n")
if report["matched"]:
print("MATCHED")
for m in report["matched"]:
flag = " <-- classification unmapped by dataclass_floor" if m[
"unmapped_classification"
] else ""
print(
f" {m['lane']:34} -> {str(m['workload']):16} "
f"{str(m['data_classification']):13} crit={str(m['criticality']):9} "
f"min={str(m['min_maturity']):4} risk={m['risk']}{flag}"
)
if report["unmatched"]:
print("\nUNMATCHED — a lane exists for something no rapp declares")
for u in report["unmatched"]:
print(
f" {u['lane']:34} risk={str(u['risk']):9} "
f"candidates={', '.join(u['candidates'])}"
)
if report["no_path"]:
print("\nNO USABLE PATH — not a KV lane, or a pattern rather than an address")
print(" " + ", ".join(n["lane"] for n in report["no_path"]))
return 0
print("explicit lane -> workload resolution\n")
print(f" resolved: {len(report['resolved'])}")
print(f" unknown: {len(report['unknown'])}")
print(f" not-applicable: {len(report['not_applicable'])}\n")
for row in report["resolved"]:
ref = row["workload_ref"]
prefix = f"{ref.get('rapp_id')}/" if ref.get("rapp_id") else ""
print(f"RESOLVED {row['lane']:34} -> {prefix}{ref.get('name')}")
for row in report["unknown"]:
print(f"UNKNOWN {row['lane']:34} {row['reason']}")
for row in report["not_applicable"]:
print(f"NOT-APPLICABLE {row['lane']:34} {row['reason']}")
return 0 if report["ok"] else 1
if __name__ == "__main__":

View file

@ -71,6 +71,6 @@ def policy_gate_status() -> str:
cfg = load_config()
except ConfigError:
return "advisory — no warden.yaml (caller identity; gate not enforced)"
if cfg.policy.enabled:
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
if cfg.policy.flex_auth_url:
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"

View file

@ -9,7 +9,7 @@ import os
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterable, Optional
from typing import Any, Optional
_AUDIT_FILENAME = "audit.jsonl"
_MAX_BYTES = 5 * 1024 * 1024
@ -215,7 +215,6 @@ def collect_activity(
since = datetime.now(timezone.utc) - timedelta(days=days)
events = read_events(state_dir, since=since, kinds=kinds)
if include_legacy:
legacy_kinds = kinds or {"sign", "access", "worker"}
if not kinds or "sign" in kinds:
events.extend(_legacy_sign_events(state_dir, since))
if not kinds or "access" in kinds:
@ -278,4 +277,4 @@ def fetch_hub_notes(*, days: int = 7, hub_url: Optional[str] = None) -> list[dic
"summary": summary,
}
)
return notes
return notes

View file

@ -58,6 +58,12 @@ def _append_signature_log(
}
if spec.policy_decision_id:
entry["policy_decision_id"] = spec.policy_decision_id
if spec.policy_zone:
entry["policy_zone"] = spec.policy_zone
if spec.policy_failure_mode:
entry["policy_failure_mode"] = spec.policy_failure_mode
if spec.policy_outcome:
entry["policy_outcome"] = spec.policy_outcome
state_dir.mkdir(parents=True, exist_ok=True)
with (state_dir / "signatures.log").open("a") as f:
f.write(json.dumps(entry) + "\n")
@ -76,6 +82,9 @@ def _append_signature_log(
actor_type=spec.actor_type.value,
backend=backend,
ttl_hours=spec.ttl_hours,
policy_zone=spec.policy_zone,
policy_failure_mode=spec.policy_failure_mode,
policy_outcome=spec.policy_outcome,
)
except Exception:
pass # audit must not block signing

View file

@ -5,7 +5,7 @@ before it evaluates the request: `Authorization: Bearer <token>` is passed to a
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
that header, the pin logs `caller authentication warning` and can only run in
`warn` mode which is why `policy.enabled` cannot flip.
`warn` mode which is why enforcing caller authentication is a separate gate.
This module resolves the token at call time and hands it straight to the request.
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it

View file

@ -16,6 +16,7 @@ from warden.config import ConfigError, WardenConfig, load_config
from warden.policy import check_sign_policy
from warden.inventory import ActorEntry, InventoryError, PrincipalsInventory, load_inventory, save_inventory
from warden.models import ActorType, CertSpec, DEFAULT_TTL_HOURS, validate_actor_name
from warden.routing.catalog import blocker_stale_days
from warden.scorecard import run_scorecard
app = typer.Typer(
@ -120,7 +121,7 @@ def _get_ca(cfg: WardenConfig):
def _apply_policy_gate(cfg: WardenConfig, spec: CertSpec) -> None:
"""Run flex-auth check when policy.enabled; sets spec.policy_decision_id."""
"""Run the zone-aware flex-auth check; record any returned decision id."""
decision_id = check_sign_policy(cfg.policy, spec)
if decision_id:
spec.policy_decision_id = decision_id
@ -644,6 +645,7 @@ def _entry_summary(entry) -> dict:
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
"risk": entry.risk,
"high_risk": entry.is_high_risk,
"workload_ref": entry.workload_ref.to_dict(),
# Renewal guidance (WP-0026 T06) — advisory, no secret values. `has_rotation`
# lets a caller gate before asking for the full block via `warden rotate-guide`.
"has_rotation": entry.has_rotation,
@ -771,9 +773,6 @@ def route_list(
)
from warden.routing.catalog import blocker_stale_days
def _gap_is_stale(entry, delegation, reviewed: str, stale_days) -> bool:
"""An interim lane needs attention on either of two independent grounds.
@ -1255,6 +1254,14 @@ def _access_proxy(
"token stays in the caller's own store.[/dim]"
)
else:
if no_policy:
err.print(
"[red]--no-policy is retired[/red]: security-zones_v0.1 selects "
"the policy stance and failure mode. Remove the flag; an unresolved "
"workload uses the explicit unknown-zone profile."
)
raise typer.Exit(2)
# G1 — caller identity. ops-warden adds no token of its own.
if not caller_auth_present():
err.print(
@ -1263,24 +1270,20 @@ def _access_proxy(
)
raise typer.Exit(3)
# G3 — policy gate before fetch.
if cfg.policy.enabled:
try:
decision_id = check_fetch_policy(
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
)
except CAError as e:
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
raise typer.Exit(4)
err.print(f"[green]flex-auth allow[/green] (decision {decision_id}).")
elif not no_policy:
err.print(
"[yellow]flex-auth gate is not enforced[/yellow] (policy.enabled=false). "
"Re-run with [bold]--no-policy[/bold] to proxy ungated, or enable the gate."
# G3 — the zone-aware policy gate always runs before fetch.
try:
decision_id = check_fetch_policy(
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
)
except CAError as e:
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
raise typer.Exit(4)
if decision_id:
err.print(f"[green]flex-auth decision[/green] ({decision_id}).")
else:
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
err.print(
"[yellow]flex-auth unavailable; unknown-zone fail_open applied[/yellow]."
)
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
# share the resolved fetch command.
@ -1434,7 +1437,10 @@ def access(
] = False,
no_policy: Annotated[
bool,
typer.Option("--no-policy", help="Acknowledge proxying when the flex-auth gate is not enforced"),
typer.Option(
"--no-policy",
help="Retired compatibility flag; zone-aware policy evaluation cannot be bypassed",
),
] = False,
) -> None:
"""Operator front door: how to obtain any credential, gated and audited.

View file

@ -45,9 +45,19 @@ class CallerAuthConfig:
@dataclass
class PolicyConfig:
enabled: bool = False
flex_auth_url: str = "http://127.0.0.1:8080"
fail_closed: bool = True
flex_auth_url: Optional[str] = None
zone_registry_path: Optional[Path] = None
failure_modes: Dict[str, str] = field(
default_factory=lambda: {
"z0-experimental": "fail_open",
"z1-operational": "fail_open",
"z2-protected": "fail_open",
"z2-continuity": "fail_open",
"z3-critical": "fail_closed",
"unknown": "fail_open",
"not-applicable": "fail_closed",
}
)
tenant: str = "tenant:platform"
subject_env: str = "WARDEN_POLICY_SUBJECT"
system: str = "ops-warden"
@ -148,6 +158,13 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
)
policy_raw = raw.get("policy") or {}
retired = sorted({"enabled", "fail_closed"}.intersection(policy_raw))
if retired:
raise ConfigError(
"retired policy setting(s) "
+ ", ".join(f"policy.{key}" for key in retired)
+ "; security-zones_v0.1 now selects stance and failure mode"
)
caller_raw = policy_raw.get("caller_auth") or {}
caller_command = caller_raw.get("command")
if isinstance(caller_command, str):
@ -175,10 +192,33 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
if caller_cfg.mode == "command" and not caller_cfg.command:
raise ConfigError("policy.caller_auth.command is required for mode: command")
failure_modes = PolicyConfig().failure_modes
configured_failure_modes = policy_raw.get("failure_modes") or {}
if not isinstance(configured_failure_modes, dict):
raise ConfigError("policy.failure_modes must be a mapping")
failure_modes.update(
{str(zone): str(mode) for zone, mode in configured_failure_modes.items()}
)
invalid_modes = {
zone: mode
for zone, mode in failure_modes.items()
if mode not in {"fail_open", "fail_closed"}
}
if invalid_modes:
raise ConfigError(
"policy.failure_modes values must be fail_open or fail_closed: "
f"{invalid_modes}"
)
zone_registry_path = policy_raw.get("zone_registry_path")
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
policy_cfg = PolicyConfig(
enabled=bool(policy_raw.get("enabled", False)),
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
fail_closed=bool(policy_raw.get("fail_closed", True)),
flex_auth_url=flex_auth_url,
zone_registry_path=(
Path(os.path.expanduser(str(zone_registry_path)))
if zone_registry_path
else None
),
failure_modes=failure_modes,
tenant=str(policy_raw.get("tenant", "tenant:platform")),
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
system=str(policy_raw.get("system", "ops-warden")),

View file

@ -53,6 +53,9 @@ class CertSpec:
principals: List[str]
identity: str = "" # defaults to actor_name if empty
policy_decision_id: Optional[str] = None
policy_zone: Optional[str] = None
policy_failure_mode: Optional[str] = None
policy_outcome: Optional[str] = None
def __post_init__(self) -> None:
if not self.identity:

View file

@ -1,7 +1,8 @@
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
"""Zone-aware flex-auth policy gates for OpsWarden."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
@ -20,21 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
return f"sha256:{digest}"
def _caller_headers(cfg: PolicyConfig) -> dict[str, str]:
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
When the token cannot be obtained we refuse the call under ``fail_closed``
When the token cannot be obtained we refuse the call under the selected
zone's ``fail_closed`` behavior
rather than silently falling back to an unauthenticated request an
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
"""
try:
return caller_auth_headers(cfg.caller_auth)
except CallerIdentityError as e:
if cfg.fail_closed:
if fail_closed:
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
return {}
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
if cfg.zone_registry_path is None:
return "unknown"
try:
registry = json.loads(cfg.zone_registry_path.read_text())
resources = registry["resource_manifests"][0]["resources"]
resource = next(item for item in resources if item.get("id") == resource_id)
attributes = resource.get("attributes") or {}
if attributes.get("security_zone_admission") == "not-applicable":
return "not-applicable"
zone = str(attributes.get("security_zone") or "unknown")
return zone if zone in cfg.failure_modes else "unknown"
except (OSError, ValueError, KeyError, StopIteration, TypeError):
return "unknown"
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
def _evaluator_failure(
message: str,
*,
fail_closed: bool,
cause: Exception | None = None,
spec: CertSpec | None = None,
) -> None:
if fail_closed:
if spec is not None:
spec.policy_outcome = "fail_closed"
if cause is None:
raise CAError(message)
raise CAError(message) from cause
if spec is not None:
spec.policy_outcome = "fail_open"
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
@ -42,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
"""Call flex-auth /v1/check before signing.
Returns decision id when policy is enabled and effect is allow.
Returns None when policy is disabled.
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
Evaluator failures use the PEP-owned failure mode for the target workload's
compiled zone; absent resolution is the explicit ``unknown`` profile.
"""
if not cfg.enabled:
resource_id = f"ssh-cert:actor/{spec.actor_name}"
zone = _resource_zone(cfg, resource_id)
fail_closed = _is_fail_closed(cfg, zone)
spec.policy_zone = zone
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
if cfg.flex_auth_url is None:
_evaluator_failure(
f"flex-auth URL is not configured for security zone {zone!r}",
fail_closed=fail_closed,
spec=spec,
)
return None
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
@ -76,37 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
}
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg)
headers = _caller_headers(cfg, fail_closed=fail_closed)
try:
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
) from e
_evaluator_failure(
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
f"for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
except httpx.RequestError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
f"(fail_closed=true): {e}"
) from e
_evaluator_failure(
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
try:
decision = response.json()
except ValueError as e:
raise CAError("flex-auth returned non-JSON decision") from e
_evaluator_failure(
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
effect = str(decision.get("effect", "")).lower()
decision_id = decision.get("id") or decision.get("request_id")
if effect != "allow":
if effect not in {"allow", "audit_only"}:
spec.policy_outcome = "deny"
reason = decision.get("reason") or "no reason provided"
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
if not decision_id:
raise CAError("flex-auth allow decision missing id")
_evaluator_failure(
f"flex-auth {effect} decision missing id for security zone {zone!r}",
fail_closed=fail_closed,
spec=spec,
)
return None
spec.policy_outcome = effect
return str(decision_id)
@ -116,13 +183,17 @@ def check_fetch_policy(
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
The action is ``read`` on a ``secret`` resource owned by another subsystem
ops-warden is the conduit, not the owner. Returns the decision id on allow,
None when policy is disabled, and raises CAError on deny (or on an unreachable
flex-auth when fail_closed). No secret value is ever part of this request.
ops-warden is the conduit, not the owner. Unresolved target workload identity
selects the explicit ``unknown`` profile; no secret value enters the request.
"""
if not cfg.enabled:
zone = "unknown"
fail_closed = _is_fail_closed(cfg, zone)
if cfg.flex_auth_url is None:
_evaluator_failure(
"flex-auth URL is not configured for security zone 'unknown'",
fail_closed=fail_closed,
)
return None
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
request = {
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
@ -137,33 +208,44 @@ def check_fetch_policy(
}
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg)
headers = _caller_headers(cfg, fail_closed=fail_closed)
try:
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
) from e
_evaluator_failure(
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
fail_closed=fail_closed,
cause=e,
)
return None
except httpx.RequestError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
) from e
_evaluator_failure(
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
fail_closed=fail_closed,
cause=e,
)
return None
try:
decision = response.json()
except ValueError as e:
raise CAError("flex-auth returned non-JSON decision") from e
_evaluator_failure(
"flex-auth returned a non-JSON decision for security zone 'unknown'",
fail_closed=fail_closed,
cause=e,
)
return None
effect = str(decision.get("effect", "")).lower()
decision_id = decision.get("id") or decision.get("request_id")
if effect != "allow":
if effect not in {"allow", "audit_only"}:
reason = decision.get("reason") or "no reason provided"
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
if not decision_id:
raise CAError("flex-auth allow decision missing id")
return str(decision_id)
_evaluator_failure(
f"flex-auth {effect} decision missing id for security zone 'unknown'",
fail_closed=fail_closed,
)
return None
return str(decision_id)

View file

@ -12,7 +12,7 @@ from warden.routing.catalog import (
find_catalog_path,
load_catalog,
)
from warden.routing.models import Delegation, RouteEntry
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
__all__ = [
"Catalog",
@ -20,6 +20,7 @@ __all__ = [
"CatalogFreshness",
"Delegation",
"RouteEntry",
"WorkloadReference",
"find_catalog_path",
"load_catalog",
]

View file

@ -26,9 +26,11 @@ import yaml
from warden.routing.models import (
VALID_DELEGATION_MODES,
VALID_RISK,
VALID_WORKLOAD_APPLICABILITY,
Delegation,
RotationGuide,
RouteEntry,
WorkloadReference,
)
# Structured handoff string fields (WP-0014) — templates and pointers only.
@ -64,6 +66,7 @@ _REQUIRED_FIELDS = (
"canon_ref",
"reviewed",
"status",
"workload_ref",
)
_VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login")
@ -516,6 +519,82 @@ def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation
)
def _parse_workload_ref(entry_id: str, raw: object) -> WorkloadReference:
"""Parse an explicit workload join without attempting identity inference."""
if not isinstance(raw, dict):
raise CatalogError(
f"entry {entry_id!r} workload_ref must be a mapping; every lane must "
"declare applicable or not-applicable"
)
applicability = str(raw.get("applicability", "")).strip()
if applicability not in VALID_WORKLOAD_APPLICABILITY:
raise CatalogError(
f"entry {entry_id!r} workload_ref.applicability {applicability!r} invalid "
f"(expected one of {VALID_WORKLOAD_APPLICABILITY})"
)
def optional(name: str) -> Optional[str]:
value = raw.get(name)
return str(value).strip() if value is not None and str(value).strip() else None
ref = WorkloadReference(
applicability=applicability,
rapp_id=optional("rapp_id"),
name=optional("name"),
deployable=optional("deployable"),
declaration_ref=optional("declaration_ref"),
reason=optional("reason"),
unknown_reason=optional("unknown_reason"),
)
target_fields = (ref.rapp_id, ref.name, ref.deployable, ref.declaration_ref)
if applicability == "not-applicable":
if not ref.reason:
raise CatalogError(
f"entry {entry_id!r} workload_ref.reason is required for not-applicable"
)
if any(target_fields) or ref.unknown_reason:
raise CatalogError(
f"entry {entry_id!r} not-applicable workload_ref must not carry a "
"workload target or unknown_reason"
)
return ref
if ref.unknown_reason:
if any(target_fields) or ref.reason:
raise CatalogError(
f"entry {entry_id!r} unknown workload_ref must carry only "
"applicability and unknown_reason"
)
return ref
if not ref.name:
raise CatalogError(
f"entry {entry_id!r} applicable workload_ref requires name or "
"unknown_reason"
)
if ref.rapp_id:
if ref.declaration_ref:
raise CatalogError(
f"entry {entry_id!r} managed workload_ref must not also carry "
"declaration_ref"
)
elif not ref.declaration_ref:
raise CatalogError(
f"entry {entry_id!r} operational workload_ref requires declaration_ref"
)
if ref.deployable and not ref.rapp_id:
raise CatalogError(
f"entry {entry_id!r} workload_ref.deployable requires rapp_id"
)
if ref.reason:
raise CatalogError(
f"entry {entry_id!r} applicable workload_ref must not carry reason"
)
return ref
def _parse_entry(raw: dict, index: int) -> RouteEntry:
if not isinstance(raw, dict):
raise CatalogError(f"entry #{index} is not a mapping")
@ -576,12 +655,16 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
)
risk = str(raw.get("risk", "standard")).strip() or "standard"
if risk not in VALID_RISK:
risk_value = raw.get("risk")
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
risk = risk or "ungraded"
if risk != "ungraded" and risk not in VALID_RISK:
raise CatalogError(
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
)
workload_ref = _parse_workload_ref(entry_id, raw.get("workload_ref"))
return RouteEntry(
id=entry_id,
title=str(raw["title"]),
@ -593,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
canon_ref=str(raw["canon_ref"]),
reviewed=str(raw["reviewed"]),
status=status,
workload_ref=workload_ref,
steps=[str(s) for s in steps],
cert_command=str(cert_command) if cert_command else None,
auth_method=handoff["auth_method"],

View file

@ -46,6 +46,7 @@ VALID_RISK = ("standard", "high")
# interim — ops-warden covers a gap; intended_owner + blocked_on required
# permanent — ops-warden is the designed owner of this front door (SSH only today)
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
IMPLICIT_DELEGATION_BLOCKED_ON = (
"unclassified — no delegation block; treat as a question, not a settlement"
@ -100,6 +101,46 @@ class Delegation:
}
@dataclass(frozen=True)
class WorkloadReference:
"""Authoritative workload join for a catalog lane (WARDEN-WP-0032).
Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple.
Independently governed operational workloads use ``name`` plus an owner
declaration reference. An applicable lane whose owner has not published an
identity remains explicitly ``unknown``; it is never inferred from the
credential path or repository name.
"""
applicability: str # applicable | not-applicable
rapp_id: Optional[str] = None
name: Optional[str] = None
deployable: Optional[str] = None
declaration_ref: Optional[str] = None
reason: Optional[str] = None
unknown_reason: Optional[str] = None
@property
def resolution(self) -> str:
if self.applicability == "not-applicable":
return "not-applicable"
if self.unknown_reason:
return "unknown"
return "resolved"
def to_dict(self) -> dict:
return {
"applicability": self.applicability,
"rapp_id": self.rapp_id,
"name": self.name,
"deployable": self.deployable,
"declaration_ref": self.declaration_ref,
"reason": self.reason,
"unknown_reason": self.unknown_reason,
"resolution": self.resolution,
}
@dataclass
class RouteEntry:
id: str
@ -112,6 +153,8 @@ class RouteEntry:
canon_ref: str
reviewed: str
status: str # "active" | "draft"
# Explicit workload applicability and authoritative join. Never inferred.
workload_ref: Optional[WorkloadReference] = None
# SSH lane only — None/empty for routed (non-executed) needs.
steps: List[str] = field(default_factory=list)
cert_command: Optional[str] = None
@ -169,6 +212,34 @@ class RouteEntry:
"""
return self.risk not in LOW_RISK_GRADES
def risk_for_zone(
self,
*,
effective_zone: str = "unknown",
admission: str = "unknown",
synthetic_only: bool = False,
) -> str:
"""Resolve an absent grade using security-zones_v0.1 section 5.1.
Explicit grades always win. The sole lower default is a satisfied
``z0-experimental`` workload proven synthetic-only. Every other zone,
failed/unknown admission, and missing context fails safe to at least
``high``; z3 reports ``critical`` (which the read boundary treats as
high). Catalog CI still requires explicit grades, so this is the safe
runtime behavior for malformed or newer external catalogs.
"""
if self.is_graded:
return self.risk
if (
effective_zone == "z0-experimental"
and admission == "satisfied"
and synthetic_only
):
return "standard"
if effective_zone == "z3-critical" and admission == "satisfied":
return "critical"
return "high"
@property
def has_rotation(self) -> bool:
"""True when this lane carries renewal guidance (WP-0026 T06)."""

View file

@ -8,6 +8,17 @@ framework: netkingdom-tenancy-posture
service: ops-warden
role: ssh-certificate-authority
workload_identity:
name: ops-warden
kind: operational-control-plane
responsible_repo: ops-warden
identity_bindings:
- scheme: kubernetes-service-account
authority: railiance01
subject: system:serviceaccount:ops-warden:ops-warden
principal_type: service
environment: prod
tenancy:
current:
I: 1
@ -108,13 +119,25 @@ tenancy:
is single-node with a shared control plane, and under Decision 4.6.1 that
caps V for everything bound to it (see NK-WP-0027).
# zones: (reserved, tenancy-posture_v0.1 Decision 5.6)
#
# Deliberately absent. The security-zone standard is being drafted by
# zone-engine (ZONE-WP-0001) and no zone vocabulary exists yet, so any value
# here would be fabricated — the failure §6 exists to prevent. ops-warden is
# the first intended consumer and will populate this key once the sibling
# standard defines it (WARDEN-WP-0032-T03).
zones:
standard: security-zones_v0.1
membership: z1-operational
responsible_party: team:platform-security
justification: >-
The attended platform signing service has a bounded operational scope and
internal metadata exposure. It has M1 evidence, but no SLO history, on-call
rotation, or incident exercise that would support z2-protected.
context:
maturity: M1
criticality: medium
data_classification: internal
evidence:
- ref: docs/evidence/security-zone-admission-2026-08-22.md
supports: [M1, platform-only-scope, basic-slo, data-handling-note]
- ref: history/2026-08-19-flex-auth-caller-identity-evidence.md
supports: [production-policy-path, authenticated-caller]
reviewed: "2026-08-22"
review_due: "2026-11-22"
evidence:
A: >-

View file

@ -84,13 +84,13 @@ def test_default_vault_token_env(tmp_path):
assert cfg.vault.token_env == "VAULT_TOKEN"
def test_policy_defaults_disabled(tmp_path):
def test_policy_defaults_to_unknown_zone_profile(tmp_path):
cfg_path = tmp_path / "warden.yaml"
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
cfg = load_config(cfg_path)
assert cfg.policy.enabled is False
assert cfg.policy.flex_auth_url == "http://127.0.0.1:8080"
assert cfg.policy.fail_closed is True
assert cfg.policy.flex_auth_url is None
assert cfg.policy.failure_modes["unknown"] == "fail_open"
assert cfg.policy.failure_modes["z3-critical"] == "fail_closed"
def test_policy_block_parsed(tmp_path):
@ -99,18 +99,30 @@ def test_policy_block_parsed(tmp_path):
"backend": "local",
"ca_key": str(tmp_path / "ca"),
"policy": {
"enabled": True,
"flex_auth_url": "http://flex-auth:8080",
"fail_closed": False,
"zone_registry_path": str(tmp_path / "zones.json"),
"failure_modes": {"z2-protected": "fail_closed"},
"tenant": "tenant:coulomb",
"subject_env": "MY_SUBJECT",
"system": "warden-test",
},
})
cfg = load_config(cfg_path)
assert cfg.policy.enabled is True
assert cfg.policy.flex_auth_url == "http://flex-auth:8080"
assert cfg.policy.fail_closed is False
assert cfg.policy.zone_registry_path == tmp_path / "zones.json"
assert cfg.policy.failure_modes["z2-protected"] == "fail_closed"
assert cfg.policy.tenant == "tenant:coulomb"
assert cfg.policy.subject_env == "MY_SUBJECT"
assert cfg.policy.system == "warden-test"
@pytest.mark.parametrize("retired", ["enabled", "fail_closed"])
def test_retired_global_policy_switches_are_rejected(tmp_path, retired):
cfg_path = tmp_path / "warden.yaml"
write_yaml(cfg_path, {
"backend": "local",
"ca_key": str(tmp_path / "ca"),
"policy": {retired: True},
})
with pytest.raises(ConfigError, match=f"policy.{retired}"):
load_config(cfg_path)

View file

@ -1,14 +1,11 @@
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
from __future__ import annotations
import json
import threading
import urllib.error
import urllib.parse
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
import pytest
from typer.testing import CliRunner

View file

@ -31,4 +31,53 @@ def test_build_registry_from_inventory_seed(tmp_path):
)
assert bridge["attributes"]["actor_type"] == "agt"
assert bridge["attributes"]["max_ttl_hours"] == 24
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
assert "trust_zone" not in bridge
assert bridge["attributes"]["security_zone"] == "unknown"
assert bridge["attributes"]["security_zone_admission"] == "unknown"
assert bridge["attributes"]["workload_id"] == "ops-bridge-tunnel"
human = next(
r
for r in registry["resource_manifests"][0]["resources"]
if r["id"] == "ssh-cert:actor/adm-example"
)
assert human["attributes"]["security_zone_admission"] == "not-applicable"
def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
zones = tmp_path / "zones.json"
zones.write_text(json.dumps({
"records": [{
"workload_id": "ops-bridge-tunnel",
"declared_zone": "z2-continuity",
"admission": "satisfied",
"admission_reason": "admission_floor_met",
"effective_zone": "z2-continuity",
"membership_revision": "sha256:zone-revision",
}]
}))
out = tmp_path / "registry.json"
subprocess.run(
[
sys.executable,
str(SCRIPT),
str(INVENTORY),
"--zone-resolutions",
str(zones),
"-o",
str(out),
],
check=True,
cwd=ROOT,
)
registry = json.loads(out.read_text())
bridge = next(
r
for r in registry["resource_manifests"][0]["resources"]
if r["id"] == "ssh-cert:actor/agt-state-hub-bridge"
)
attrs = bridge["attributes"]
assert attrs["security_zone"] == "z2-continuity"
assert attrs["security_zone_admission"] == "satisfied"
assert attrs["security_zone_revision"] == "sha256:zone-revision"

View file

@ -7,7 +7,7 @@ import json
from typer.testing import CliRunner
from warden.cli import app
from warden.memory import activate, enabled, record_command_episode, status, store_path
from warden.memory import activate, record_command_episode, status, store_path
from warden.worker import RuleBrain, _plan_with_memory, build_plans
runner = CliRunner()
@ -140,4 +140,4 @@ def test_route_find_implicitly_activates_memory_without_explicit_command(tmp_pat
activation = ensure_memory_context(need="ssh tunnel", implicit=True)
assert activation is not None
assert activation.get("implicit") is True
assert status()["episode_count"] >= 1
assert status()["episode_count"] >= 1

View file

@ -27,6 +27,17 @@ def _spec(pubkey_path: Path) -> CertSpec:
)
def _zone_registry(tmp_path: Path, zone: str) -> Path:
path = tmp_path / "registry.json"
path.write_text(
'{"resource_manifests":[{"resources":[{"id":'
'"ssh-cert:actor/agt-state-hub-bridge","attributes":{'
f'"security_zone":"{zone}","security_zone_admission":"satisfied"'
'}}]}]}'
)
return path
def test_pubkey_fingerprint(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
@ -35,26 +46,32 @@ def test_pubkey_fingerprint(tmp_path):
assert len(fp) == 7 + 64
def test_disabled_returns_none(tmp_path):
def test_unconfigured_evaluator_uses_unknown_fail_open_profile(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=False)
assert check_sign_policy(cfg, _spec(pubkey)) is None
cfg = PolicyConfig()
spec = _spec(pubkey)
assert check_sign_policy(cfg, spec) is None
assert spec.policy_zone == "unknown"
assert spec.policy_failure_mode == "fail_open"
assert spec.policy_outcome == "fail_open"
def test_allow_returns_decision_id(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, flex_auth_url="http://flex-auth.test")
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
mock_response = MagicMock()
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
mock_response.raise_for_status = MagicMock()
spec = _spec(pubkey)
with patch("warden.policy.httpx.post", return_value=mock_response) as post:
result = check_sign_policy(cfg, _spec(pubkey))
result = check_sign_policy(cfg, spec)
assert result == "dec-123"
assert spec.policy_outcome == "allow"
post.assert_called_once()
call_kwargs = post.call_args
assert call_kwargs[0][0] == "http://flex-auth.test/v1/check"
@ -67,7 +84,7 @@ def test_allow_returns_decision_id(tmp_path):
def test_deny_raises_ca_error(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
mock_response = MagicMock()
mock_response.json.return_value = {
@ -84,7 +101,10 @@ def test_deny_raises_ca_error(tmp_path):
def test_unreachable_fail_closed_raises(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=True)
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
)
with patch(
"warden.policy.httpx.post",
@ -97,7 +117,7 @@ def test_unreachable_fail_closed_raises(tmp_path):
def test_unreachable_fail_open_returns_none(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=False)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
with patch(
"warden.policy.httpx.post",
@ -109,7 +129,10 @@ def test_unreachable_fail_open_returns_none(tmp_path):
def test_http_error_fail_closed_raises(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=True)
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
)
mock_response = MagicMock()
mock_response.status_code = 403
@ -123,7 +146,7 @@ def test_http_error_fail_closed_raises(tmp_path):
def test_missing_pubkey_raises(tmp_path):
cfg = PolicyConfig(enabled=True)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
spec = _spec(tmp_path / "missing.pub")
with pytest.raises(CAError, match="Public key not found"):
check_sign_policy(cfg, spec)
@ -132,7 +155,10 @@ def test_missing_pubkey_raises(tmp_path):
def test_subject_from_env(tmp_path, monkeypatch):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, subject_env="WARDEN_POLICY_SUBJECT")
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
subject_env="WARDEN_POLICY_SUBJECT",
)
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "iam:bernd")
mock_response = MagicMock()
@ -212,7 +238,7 @@ def test_sign_policy_sends_authorization_header(tmp_path, monkeypatch):
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(
enabled=True,
flex_auth_url="http://flex-auth.test",
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
)
spec = CertSpec(
@ -250,8 +276,8 @@ def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(
enabled=True,
fail_closed=True,
flex_auth_url="http://flex-auth.test",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
)
spec = CertSpec(
@ -263,3 +289,21 @@ def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
)
with pytest.raises(CAError, match="caller identity unavailable"):
policy_mod.check_sign_policy(cfg, spec)
def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
response = MagicMock()
response.json.return_value = {
"effect": "audit_only",
"reason": "advisory_would_deny_disallowed_principal",
"id": "decision:advisory",
}
response.raise_for_status = MagicMock()
spec = _spec(pubkey)
with patch("warden.policy.httpx.post", return_value=response):
assert check_sign_policy(cfg, spec) == "decision:advisory"
assert spec.policy_zone == "unknown"
assert spec.policy_outcome == "audit_only"

View file

@ -193,7 +193,6 @@ def _warden_yaml(tmp_path: Path) -> Path:
(tmp_path / "ca").write_text("")
cfg.write_text(
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
"policy:\n enabled: false\n"
)
return cfg
@ -203,10 +202,11 @@ def _proxy_env(monkeypatch, tmp_path):
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path)))
def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path):
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller")
# subprocess must never run if the gate blocks first.
# The unknown-zone profile proceeds when no evaluator is configured, then
# the independent safe-transport boundary still refuses captured stdout.
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
@ -216,8 +216,8 @@ def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch"],
)
assert r.exit_code == 4
assert "not enforced" in r.stdout or "not enforced" in str(r.output)
assert r.exit_code == 6
assert "unknown-zone fail_open" in r.output
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
@ -228,11 +228,27 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
r = runner.invoke(
app,
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
"--path", "platform/x/y/z", "--fetch"],
)
assert r.exit_code == 3
def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path):
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller")
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite retired flag")),
)
r = runner.invoke(
app,
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
)
assert r.exit_code == 2
assert "--no-policy is retired" in r.output
# --- T4: login lane --------------------------------------------------------
def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path):
@ -281,6 +297,7 @@ def test_invalid_lane_rejected(tmp_path):
id="x", title="t", need_keywords=["k"], owner_repo="o", subsystem="s",
warden_executes=False, wiki_ref="w", canon_ref="c", reviewed="2026-06-27",
status="active", lane="bogus",
workload_ref={"applicability": "not-applicable", "reason": "fixture"},
)
p = tmp_path / "c.yaml"
p.write_text(yaml.dump({"version": 1, "entries": [entry]}))
@ -361,7 +378,7 @@ def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")),
)
# CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout.
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch", "--no-policy"])
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"])
assert r.exit_code == 6
assert "sanctioned transport" in r.output.lower() or "refusing" in r.output.lower()
@ -378,7 +395,7 @@ def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_pat
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
r = runner.invoke(
app,
["access", "whynot-design-npm-publish", "--fingerprint", "--no-policy"],
["access", "whynot-design-npm-publish", "--fingerprint"],
)
assert r.exit_code == 0
assert "top-secret-token-value" not in r.output # value never shown
@ -395,7 +412,7 @@ def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch):
app,
[
"access", "railiance-backup-offsite-lane",
"--fetch", "--no-policy", "--unsafe-stdout",
"--fetch", "--unsafe-stdout",
],
)
assert r.exit_code == 7, r.output
@ -415,7 +432,7 @@ def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
r = runner.invoke(
app,
["access", "railiance-backup-offsite-lane", "--fingerprint", "--no-policy"],
["access", "railiance-backup-offsite-lane", "--fingerprint"],
)
assert r.exit_code == 0, r.output
assert "should-not-appear" not in r.output

View file

@ -4,6 +4,7 @@ No test here requires a live subsystem — routing is a read-only pointer layer.
"""
import json
import re
from datetime import date
from pathlib import Path
import pytest
@ -11,10 +12,9 @@ import yaml
from typer.testing import CliRunner
from warden.cli import app
from datetime import date
from warden.routing import CatalogError, load_catalog
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
from warden.scorecard import check_catalog_rotation_coverage
runner = CliRunner()
@ -40,6 +40,10 @@ SSH_ENTRY = {
"canon_ref": "net-kingdom/docs/x.md",
"reviewed": "2026-06-18",
"status": "active",
"workload_ref": {
"applicability": "not-applicable",
"reason": "generic certificate action",
},
"cert_command": "warden sign <actor> --pubkey <path>",
"steps": ["confirm inventory", "sign"],
}
@ -55,6 +59,10 @@ ROUTED_ENTRY = {
"canon_ref": "net-kingdom/docs/x.md",
"reviewed": "2026-06-18",
"status": "active",
"workload_ref": {
"applicability": "not-applicable",
"reason": "generic credential pattern",
},
}
@ -76,6 +84,45 @@ def test_real_catalog_has_one_executed_lane():
assert [e.id for e in executed] == ["ssh-cert-host-access"]
def test_every_catalog_lane_declares_workload_applicability():
catalog = load_catalog(_repo_catalog())
assert all(entry.workload_ref is not None for entry in catalog.entries)
assert {entry.workload_ref.resolution for entry in catalog.entries} == {
"resolved", "unknown", "not-applicable"
}
def test_managed_and_operational_workload_references_parse():
catalog = load_catalog(_repo_catalog())
managed = catalog.get("issue-core-ingestion-api-key").workload_ref
assert managed.resolution == "resolved"
assert (managed.rapp_id, managed.name, managed.deployable) == (
"rapp-issue-core", "issue-core", "issue-core"
)
operational = catalog.get("ops-warden-warden-sign-token").workload_ref
assert operational.resolution == "resolved"
assert operational.rapp_id is None
assert operational.name == "ops-warden"
assert operational.declaration_ref == "tenancy.yaml"
def test_workload_reference_rejects_ambiguous_absence(tmp_path):
bad = dict(ROUTED_ENTRY)
bad.pop("workload_ref")
with pytest.raises(CatalogError, match="workload_ref"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_workload_reference_rejects_malformed_managed_target(tmp_path):
bad = dict(ROUTED_ENTRY)
bad["workload_ref"] = {
"applicability": "applicable",
"rapp_id": "rapp-issue-core",
}
with pytest.raises(CatalogError, match="requires name"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_ops_warden_warden_sign_lane_has_native_exec():
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
catalog = load_catalog(_repo_catalog())
@ -454,9 +501,6 @@ def test_every_entry_has_reviewed_date():
# Rotation / re-establishment guidance registry (WARDEN-WP-0026 T06)
# ---------------------------------------------------------------------------
from warden.scorecard import check_catalog_rotation_coverage
def test_every_active_vending_lane_has_rotation_guidance():
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
catalog = load_catalog(_repo_catalog())
@ -774,6 +818,32 @@ def test_unrecognised_grade_is_treated_as_high():
assert entry.is_graded is False
def test_ungraded_risk_uses_maturity_derived_zone_default():
entry = _bare_entry()
assert entry.risk_for_zone(
effective_zone="z0-experimental",
admission="satisfied",
synthetic_only=True,
) == "standard"
assert entry.risk_for_zone(
effective_zone="z0-experimental",
admission="unknown",
synthetic_only=True,
) == "high"
assert entry.risk_for_zone(
effective_zone="z3-critical",
admission="satisfied",
) == "critical"
assert entry.risk_for_zone(effective_zone="unknown") == "high"
def test_explicit_risk_grade_always_wins_over_zone_default():
entry = _bare_entry(risk="standard")
assert entry.risk_for_zone(
effective_zone="z3-critical", admission="satisfied"
) == "standard"
def test_low_risk_vocabulary_is_explicit():
for grade in ("standard", "low", "accepted"):
entry = _bare_entry(risk=grade)
@ -870,6 +940,11 @@ def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env):
assert result.exit_code == 3
rows = json.loads(result.stdout)
assert any(r["stale"] for r in rows)
# A lane can be stale on age or on never having been verified; both must be
# expressible, or asked-and-waiting silently passes the gate.
assert any(r["stale"] and r["days_since_review"] == 0 for r in rows)
# A freshly reviewed lane can still be stale because it was never verified;
# asked-and-waiting must not silently pass the gate as the calendar moves.
assert any(
r["stale"]
and r["days_since_review"] <= 1
and r["verified"] == "asked-and-waiting"
for r in rows
)

View file

@ -1,10 +1,6 @@
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from typer.testing import CliRunner
from warden.cli import app

View file

@ -0,0 +1,52 @@
"""Explicit lane-to-workload join tests (WARDEN-WP-0032 / RMGR-WP-0010-T06)."""
from pathlib import Path
import yaml
from scripts.report_workload_join import build
ROOT = Path(__file__).resolve().parents[1]
def test_repo_catalog_uses_only_explicit_workload_references():
report = build(ROOT / "registry/routing/catalog.yaml", Path.home())
assert report["ok"] is True
assert len(report["resolved"]) == 3
assert len(report["unknown"]) == 17
assert len(report["not_applicable"]) == 7
assert {row["lane"] for row in report["resolved"]} == {
"ops-warden-warden-sign-token",
"issue-core-ingestion-api-key",
"rapp-qonto-keycape-client",
}
def test_invalid_exact_deployable_resolves_unknown(tmp_path):
rapp = tmp_path / "rapp-x" / "declarations"
rapp.mkdir(parents=True)
(rapp / "rapp.yaml").write_text(yaml.safe_dump({
"rapp_id": "rapp-x",
"workload_identity": {"name": "x"},
"composition": {"member_repos": [{"deployables": ["api"]}]},
}))
catalog_dir = tmp_path / "ops-warden" / "registry" / "routing"
catalog_dir.mkdir(parents=True)
catalog = catalog_dir / "catalog.yaml"
catalog.write_text(yaml.safe_dump({"entries": [{
"id": "x",
"workload_ref": {
"applicability": "applicable",
"rapp_id": "rapp-x",
"name": "x",
"deployable": "missing",
},
}]}))
posture = catalog_dir.parent / "policy"
posture.mkdir()
(posture / "security-posture.yaml").write_text("dataclass_floor: {}\n")
report = build(catalog, tmp_path)
assert not report["resolved"]
assert report["unknown"][0]["lane"] == "x"
assert "deployable" in report["unknown"][0]["reason"]

View file

@ -135,11 +135,12 @@ schedule rather than in lockstep. The binding condition is sequencing, not a dat
flex-auth warn-only -> ops-warden pre-sign gate presents its SA token
-> logs clean of unauthenticated callers
-> flex-auth fail-closed
-> policy.enabled: true (ops-warden side, FLEX-WP-0007)
-> zone-specific enforce stance (flex-auth policy package)
```
`policy.enabled` must not flip anywhere while `/v1/check` still answers
unauthenticated callers.
An enforce stance must not be assigned while `/v1/check` still answers
unauthenticated callers. ops-warden has no global enable switch or gate bypass;
it applies the compiled zone stance and its local per-zone failure mode.
**Division of the call:** the mechanism above is an architecture recommendation and
ops-warden's to make. Accepting the pod-spec change and the rollout timing are the
@ -179,4 +180,4 @@ and automation work — not platform-admin equivalents on hosts.
- `wiki/AccessRouting.md` — issue-vs-route role and boundary
- `wiki/CredentialRouting.md`
- `wiki/PolicyGatedSigning.md` (future flex-auth hook)
- `net-kingdom/docs/platform-identity-security-architecture.md`
- `net-kingdom/docs/platform-identity-security-architecture.md`

View file

@ -76,7 +76,7 @@ prevent, and duplicates OpenBao.
| --- | --- | --- |
| **G1** | **Caller identity, never warden's** | The proxy runs the owner's tool with the caller's own environment; ops-warden injects no token of its own. Secret lanes require the caller to already hold a credential (`caller_auth_present`), else they fail with the auth pointer. |
| **G2** | **Transit only — no persistence/logging of values** | Sanctioned transports keep the value off stdout: `--out` writes it to a mode-0600 file, `--exec` injects it into a child process env, `--wrap` returns a single-use OpenBao wrapping token (not the value). Raw `--fetch` to stdout is refused for captured/piped output (`--unsafe-stdout` overrides for a human terminal). warden never writes the value to disk or log; the audit record is **metadata only**. (WP-0026 T02) |
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before any secret-lane fetch. With `policy.enabled: false` the proxy refuses unless `--no-policy` is given to acknowledge proxying ungated. |
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before every secret-lane fetch. Zone membership selects stance and local failure mode; an unresolved workload uses the explicit `unknown` profile. The retired `--no-policy` flag is rejected, so this gate has no CLI bypass. |
The catalog side enforces a fourth, upstream guard: **handoff fields are templates,
never values.** `_assert_no_secret_material` rejects any known token prefix or

View file

@ -36,11 +36,19 @@ ca_key: ~/.ssh/ops-ca-user
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Optional flex-auth gate (default off — see wiki/PolicyGatedSigning.md)
# Zone-aware flex-auth gate. With no URL, the explicit unknown/build profile
# fails open and records that no evaluator decision was available.
policy:
enabled: false
flex_auth_url: http://127.0.0.1:8080
fail_closed: true
zone_registry_path: /path/to/compiled-flex-auth-registry.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
```
### Bootstrapping the local CA key
@ -85,11 +93,10 @@ vault:
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Enable after flex-auth ssh-certificate policies are deployed:
# Configure after flex-auth ssh-certificate policies are deployed:
# policy:
# enabled: true
# flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
# fail_closed: true
# flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
# zone_registry_path: /etc/warden/production_registry_snapshot.json
```
### Example — in-cluster caller (pod or trusted host)
@ -235,22 +242,36 @@ hosts:
---
## Policy gate (flex-auth, opt-in)
## Policy gate (flex-auth, zone-aware)
When `policy.enabled: true`, `warden sign` and `warden issue` call flex-auth
`POST /v1/check` before signing. Deny or unreachable (with `fail_closed: true`)
blocks issuance. Allowed decisions store `policy_decision_id` in `signatures.log`.
`warden sign` and `warden issue` evaluate flex-auth whenever a URL is configured.
The target resource's compiled `security_zone` selects the local PEP failure
mode. A rendered deny always blocks; evaluator failure blocks or proceeds per
zone. Signing records `policy_decision_id` when present plus `policy_zone`,
`policy_failure_mode`, and `policy_outcome`.
```yaml
policy:
enabled: false # default — no behavior change
flex_auth_url: http://127.0.0.1:8080
fail_closed: true # deny when flex-auth unreachable
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
```
`policy.enabled` and the global `policy.fail_closed` are retired; configuration
loading rejects them with a migration error. Stance is owned by flex-auth's
versioned policy package, not this block. The failure-mode map is PEP behavior
for an unavailable or invalid evaluator.
Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
---
@ -261,7 +282,7 @@ Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
|----------|---------|-------------|
| `WARDEN_CONFIG` | `~/.config/warden/warden.yaml` | Config file path |
| `VAULT_TOKEN` | — | API token for `backend: vault` (OpenBao or Vault; name configurable via `vault.token_env`) |
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks (when `policy.enabled`) |
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks |
---
@ -284,4 +305,4 @@ tunnels:
`ops-bridge` runs `cert_command` before each SSH launch, captures stdout as the cert,
and passes it alongside the private key via `ssh -i <key> -i <cert>`.
See `wiki/CertCommandInterface.md` for the full contract and
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.

View file

@ -1,347 +1,196 @@
# Policy-Gated SSH Signing
# Zone-aware policy-gated signing
Date: 2026-06-23
Status: **implemented (opt-in)** — WARDEN-WP-0007; policy package confirmed FLEX-WP-0006
Ops-warden asks flex-auth for a decision before SSH certificate issuance. The
gate is resource-scoped through security-zone membership; there is no repo-wide
enable switch.
By default `warden sign` authorizes via **inventory allow-list** and TTL policy
only. When `policy.enabled: true` in `warden.yaml`, ops-warden calls flex-auth
before signing and records the decision id in `signatures.log`.
Authority stays split:
---
- flex-auth owns the versioned pre-sign stance (`enforced`, `advisory`, or
`exempt`) and returns the decision;
- ops-warden owns what the PEP does when flex-auth is unavailable or invalid;
- the workload owner declares identity and zone membership;
- zone-engine owns `security-zones_v0.1` admission and resolution semantics.
## Flow
Binding decisions: `ADR-0009` (current) and `ADR-0006` (superseded rationale).
## Request path
```text
warden sign <actor> --pubkey <path>
|
v
Load actor from inventory (type, principals, ttl)
|
v
policy.enabled?
no -> skip
yes -> flex-auth POST /v1/check
|
+-- DENY / unreachable (fail_closed) -> CAError
|
v ALLOW
CABackend.sign() (local or OpenBao SSH engine)
|
v
Append signatures.log (+ policy_decision_id when set)
warden sign <actor>
-> inventory, principal, actor-type, and TTL checks
-> resource id ssh-cert:actor/<actor>
-> read compiled security_zone for that resource
-> POST flex-auth /v1/check with authenticated caller identity
allow -> sign; record decision and zone evidence
audit_only -> sign; record advisory decision and zone evidence
deny -> refuse before the CA backend
unavailable/invalid
-> apply that zone's PEP failure mode
-> record fail_open when issuance proceeds
```
The same gate runs for `warden issue` (local backend only).
The request contains actor id/type, requested principals, TTL, and a SHA-256
fingerprint of the public key. It never contains a private key or secret value.
---
## Compiled membership
## flex-auth request shape
| Field | Source |
| --- | --- |
| `subject.id` | `WARDEN_POLICY_SUBJECT` env var, or actor name |
| `subject.type` | Actor type (`adm` / `agt` / `atm`) |
| `tenant` | `policy.tenant` (default `tenant:platform`) |
| `resource.id` | `ssh-cert:actor/<actor-name>` |
| `resource.type` | `ssh-certificate` |
| `action` | `sign` |
| `context.principals` | From inventory |
| `context.actor_type` | adm \| agt \| atm |
| `context.pubkey_fingerprint` | SHA256 of pubkey text |
| `context.ttl_hours` | Requested TTL |
flex-auth must return `effect: allow` and an `id` (or `request_id`) on allow.
Deny responses include a `reason` surfaced in the CLI error.
---
## Configuration
`scripts/build_flex_auth_registry.py` compiles inventory actor resources. Each
actor carries an explicit `zone_subject`:
```yaml
# warden.yaml — policy gate (opt-in, default off)
policy:
enabled: false
flex_auth_url: http://127.0.0.1:8080
fail_closed: true
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
caller_auth: # how ops-warden identifies itself — see "Caller identity"
mode: none
actors:
agt-state-hub-bridge:
type: agt
principals: [agt-task-bridge]
ttl_hours: 24
zone_subject:
applicability: applicable
workload_id: ops-bridge-tunnel
```
| Key | Default | Description |
| --- | --- | --- |
| `enabled` | `false` | When `true`, call flex-auth before every sign/issue |
| `flex_auth_url` | `http://127.0.0.1:8080` | flex-auth base URL |
| `fail_closed` | `true` | Deny sign when flex-auth is unreachable or returns HTTP error |
| `tenant` | `tenant:platform` | Tenant sent in subject and resource |
| `subject_env` | `WARDEN_POLICY_SUBJECT` | Env var for IAM subject id override |
| `system` | `ops-warden` | Resource system identifier |
| `caller_auth.mode` | `none` | `none` \| `file` \| `env` \| `command` — source of the `Authorization` bearer token flex-auth TokenReviews ([Caller identity](#caller-identity-warden-wp-0031)) |
The compiler consumes zone-engine's resolved view when available and emits
resource attributes:
Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available.
If unset, the actor name is used as subject id.
---
## Versioning
| Version | Gate | Status |
| --- | --- | --- |
| **v1** | Inventory + TTL max | Shipped |
| **v2** | flex-auth opt-in via `policy.enabled` | Shipped (WP-0007) |
| **v2.1** | Identity claims required for `adm` signs | Planned |
| **v3** | Tenant-scoped policies per `tenant:*` | Planned |
---
## What stays in inventory
- Actor registration (name, type, default principals, default TTL)
- Host reference documentation
- Scorecard local checks
flex-auth decides **whether this sign request is allowed now**; inventory
defines **what the actor is allowed to request**.
---
## flex-auth policy package (FLEX-WP-0006)
flex-auth owns the `ssh-certificate` / `sign` policy package. ops-warden consumes
it via `POST /v1/check` when `policy.enabled: true`.
**Handoff (canonical):** `~/flex-auth/docs/ops-warden-policy-gate-handoff.md`
| Asset | flex-auth path |
| --- | --- |
| Policy package | `examples/ops-warden/policy_package.md` |
| Allow/deny fixtures | `examples/ops-warden/policy_fixtures.yaml` |
| Registry snapshot | `examples/ops-warden/registry_snapshot.json` |
| Subject manifest | `examples/ops-warden/subject_manifest.yaml` |
| Resource manifest | `examples/ops-warden/resource_manifest.yaml` |
### Tenant and subject bindings
| Field | Value |
| --- | --- |
| Tenant | `tenant:platform` (`policy.tenant`) |
| Resource system | `ops-warden` (`policy.system`) |
| Resource type | `ssh-certificate` |
| Action | `sign` |
| Resource id | `ssh-cert:actor/<actor-name>` |
| Actor type | Example flex-auth subject | ops-warden inventory name pattern |
| --- | --- | --- |
| `adm` | `platform-steward` | `adm-*` |
| `agt` | `ci-deploy-agent` | `agt-*` |
| `atm` | `backup-automation` | `atm-*` |
**Subject id sent to flex-auth:** `WARDEN_POLICY_SUBJECT` when set, otherwise the
inventory actor name. flex-auth may also allow `iam:<actor-name>` when listed in
`allowed_subjects` on the resource.
**Principals and TTL:** Taken from the sign request (inventory defaults). flex-auth
denies when principals are empty/disallowed or TTL exceeds `max_ttl_hours` on the
registered resource.
### Fixture coverage (flex-auth)
Allow: `fixture:ops-warden-adm-sign-allow`, `fixture:ops-warden-agt-sign-allow`,
`fixture:ops-warden-atm-sign-allow`.
Deny: `fixture:ops-warden-unknown-subject-deny`,
`fixture:ops-warden-actor-type-mismatch-deny`, `fixture:ops-warden-ttl-above-max-deny`,
`fixture:ops-warden-disallowed-principal-deny`,
`fixture:ops-warden-missing-fingerprint-deny`.
### Local smoke
```bash
# flex-auth (from ~/flex-auth)
flex-auth serve --addr 127.0.0.1:8080 \
--registry examples/ops-warden/registry_snapshot.json \
--policy examples/ops-warden/policy_package.md \
--log /tmp/flex-auth-ops-warden-decisions.jsonl
# warden.yaml — policy.enabled: true, flex_auth_url pointing at flex-auth
# Use an actor registered in the flex-auth registry (example fixtures use
# template names; production needs a registry slice for real inventory actors).
```json
{
"workload_id": "ops-bridge-tunnel",
"security_zone": "z2-continuity",
"security_zone_admission": "satisfied",
"security_zone_revision": "sha256:..."
}
```
Local end-to-end evidence: `history/2026-06-23-flex-auth-policy-gate-local-smoke.md`.
If the workload reference or resolved membership is absent, the resource says
`security_zone: unknown` with a reason. A native non-workload actor/action says
`security_zone_admission: not-applicable`. The compiler never parses a path or
repository name to repair missing identity.
### Production registry from inventory
`trust_zone: platform` was a dormant, unrelated field and is retired. It must
not coexist with `security_zone` as a competing membership source.
Build a flex-auth registry snapshot that mirrors `inventory.yaml` actors:
Build the snapshot:
```bash
python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml \
python3 scripts/build_flex_auth_registry.py \
~/.config/warden/inventory.yaml \
--zone-resolutions /path/to/zone-resolved-view.json \
-o registry/flex-auth/production_registry_snapshot.json
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json
```
Re-run after adding or changing actors. Deploy the snapshot to the production
flex-auth runtime together with `~/flex-auth/examples/ops-warden/policy_package.md`.
Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not
to a guessed zone.
Smoke (non-secret):
## PEP failure modes
```bash
./scripts/policy_gate_production_smoke.sh
# OpenBao-backed — preferred: credential broker (no manual VAULT_TOKEN):
cd ~/railiance-platform && make credential-exec-ops-warden-smoke
# Manual fallback when broker unavailable:
SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh
```
The initial build profile accepted by ops-warden is:
Evidence: `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`.
| Zone/result | Dependency failure |
| --- | --- |
| `z0-experimental` | `fail_open` |
| `z1-operational` | `fail_open` |
| `z2-protected` | `fail_open` |
| `z2-continuity` | `fail_open` |
| `z3-critical` | `fail_closed` |
| `unknown` | `fail_open` under the versioned build profile |
| `not-applicable` | `fail_closed` for this pre-sign PEP |
---
These are dependency failure modes, not policy stance. A rendered deny always
blocks. The `unknown` row does not grant membership or an exception; it is the
explicit build-stage treatment until authoritative declarations land.
## Caller identity (WARDEN-WP-0031)
flex-auth authenticates the **caller** before it evaluates the request. The
in-cluster pin `flex-auth-ops-warden` (FLEX-WP-0016) passes the bearer token to a
Kubernetes TokenReview and requires the principal
`system:serviceaccount:ops-warden:ops-warden` for any request carrying
`resource.system: ops-warden`.
Until ops-warden sends that header the pin logs
```
caller authentication warning: caller is not authenticated
```
and can only run `callerAuth.mode: warn`. Per ADHOC-2026-08-17-T01,
`policy.enabled` must not flip while `/v1/check` still answers unauthenticated
callers — so **the missing header is what blocks the flip**, and it is
ops-warden's to fix, not flex-auth's.
### Configure a token source
Configuration:
```yaml
policy:
flex_auth_url: http://127.0.0.1:19090
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
caller_auth:
mode: none # none | file | env | command
token_path: /var/run/secrets/flex-auth/token # mode: file
token_env: WARDEN_POLICY_CALLER_TOKEN # mode: env
command: kubectl create token ops-warden -n ops-warden --audience flex-auth --duration 10m
audience: flex-auth
mode: command
command:
- kubectl
- create
- token
- ops-warden
- -n
- ops-warden
- --audience
- flex-auth
- --duration
- 10m
```
| Mode | Use it when |
| --- | --- |
| `none` | Default. No header — pre-FLEX-WP-0016 behaviour, only viable while the pin is in `warn` |
| `file` | In-cluster PEP with a projected, audience-bound ServiceAccount token |
| `env` | The token is already in the environment (CI, a wrapper) |
| `command` | Workstation `warden sign` — mint a short-lived bound token per call |
`policy.enabled` and the global `policy.fail_closed` are retired. The loader
rejects either key so old and new controls cannot coexist as two sources of
truth.
A workstation is not a ServiceAccount, which is why `command` exists. The token is
read, sent, and dropped: never cached, written, or logged (ADR-0002). If a token
is configured but cannot be obtained, the sign is **refused** under
`fail_closed` — falling back to an anonymous call would defeat the gate.
## Caller identity
### Readiness gate
The production flex-auth pin authenticates ops-warden with Kubernetes
TokenReview and binds `resource.system: ops-warden` to
`system:serviceaccount:ops-warden:ops-warden`. Supported token sources are:
- `file` — projected ServiceAccount token for an in-cluster PEP;
- `command` — short-lived `kubectl create token` on a workstation;
- `env` — attended fallback;
- `none` — no identity header; only useful for an intentionally unauthenticated
development evaluator.
Tokens are resolved per call, never cached, logged, or echoed. Under a
fail-closed zone, an unavailable caller token blocks. Under a fail-open zone it
becomes a recorded evaluator failure; ops-warden never retries anonymously with
a secret copied into its own state.
Re-establish the value-safe caller proof:
```bash
python scripts/check_policy_caller_identity.py # offline
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090 # port-forward of the warn pin
python3 scripts/check_policy_caller_identity.py \
--url http://127.0.0.1:19090
```
Exit 0 ready / 1 not ready / 2 bad input. It prints the token's length and a
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`.
Expected evidence is HTTP 200 with a decision id and anonymous HTTP 401 on the
enforcing pin. The script reports only token length and a truncated fingerprint.
### Enablement is deferred by decision (ADR-0006)
## Audit evidence
`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.
Successful signing records:
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.
- `policy_decision_id` when flex-auth returned one;
- `policy_zone`;
- `policy_failure_mode`;
- `policy_outcome` (`allow`, `audit_only`, or `fail_open`).
`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.
A fail-open result must therefore be visible rather than indistinguishable from
an unevaluated request. Denies do not reach the CA backend and produce no
certificate.
The sequence below is retained as the *mechanics*, for whoever enables the gate
for a zone once zones exist.
## Rollout and rollback
### Flip sequence
1. Validate `tenancy.yaml` and the workload declarations referenced by actor
`zone_subject` entries.
2. Compile the registry and inspect unknown/not-applicable results.
3. Run `scripts/check_policy_caller_identity.py` against the enforcing pin.
4. Deploy the same compiled registry revision and matching flex-auth policy
package.
5. Smoke an allow/advisory path, an enforced deny, and evaluator loss for one
fail-open and one fail-closed zone.
1. Configure `caller_auth`; `check_policy_caller_identity.py` exits 0 offline.
2. Smoke against a port-forward of the warn pin. The evidence is the **absence**
of `caller authentication warning` in its log, not the `allow` — warn serves
unauthenticated callers too, so an allow alone proves nothing.
3. Ask flex-auth to set `callerAuth.mode: enforce` (their FLEX-WP-0016 T03).
4. Re-run the gate against the enforcing pin.
5. Only then: `policy.enabled: true`, `fail_closed: true`.
Reversing 3 and 5 401s every `warden sign`.
---
## Production rollout
**Keep `policy.enabled: false` until flex-auth is reachable** at `policy.flex_auth_url`
with `fail_closed: true`, unreachable flex-auth blocks all signs.
### Operator checklist
| Step | Owner | Action |
| --- | --- | --- |
| 1 | flex-auth | Deploy runtime; confirm `curl <flex_auth_url>/healthz` → 200 (**FLEX-WP-0007**) |
| 2 | flex-auth | Load production registry + policy package (`~/flex-auth/examples/ops-warden/`) |
| 3 | ops-warden | Regenerate registry from inventory: `scripts/build_flex_auth_registry.py` |
| 4 | ops-warden | Local smoke: `./scripts/policy_gate_production_smoke.sh` |
| 5 | operator | Vault smoke: `make credential-exec-ops-warden-smoke` in `railiance-platform` (or manual `SMOKE_VAULT=1` fallback) |
| 6 | operator | Set `policy.flex_auth_url` in `~/.config/warden/warden.yaml` — the pin is `flex-auth-ops-warden.flex-auth.svc.cluster.local:8080`, reached from a workstation via port-forward or tunnel |
| 6a | ops-warden | Configure `policy.caller_auth`; `scripts/check_policy_caller_identity.py` exits 0 (see **Caller identity**) |
| 6b | flex-auth | Set `callerAuth.mode: enforce` on the ops-warden pin (FLEX-WP-0016 T03) |
| 7 | operator | Set `policy.enabled: true`; keep `fail_closed: true` |
| 8 | operator | Allow smoke: `warden sign <actor>``signatures.log` has `policy_decision_id` |
| 9 | operator | Deny smoke: e.g. `--ttl` above max — CLI shows flex-auth `reason`, no cert |
Cross-repo references:
- `~/flex-auth/workplans/FLEX-WP-0007-ops-warden-policy-gate-production-deployment.md`
- `history/2026-06-23-flex-auth-production-pickup-suggestion.md`
- `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`
### Summary
1. Deploy the flex-auth registry and policy package to the production flex-auth
runtime — **not** only the example fixtures.
2. Set `policy.flex_auth_url` to the production flex-auth base URL.
3. Enable `policy.enabled: true` only after steps 15 pass.
4. Keep `fail_closed: true` unless an explicit break-glass procedure exists.
5. Smoke allow and deny paths; preserve non-secret evidence only.
### Rollback
If signs are blocked after enabling the gate:
1. Set `policy.enabled: false` in `warden.yaml` (inventory + TTL gate only).
2. Confirm `warden sign` succeeds without flex-auth.
3. File a State Hub note to `flex-auth` with non-secret symptoms (HTTP status,
`fail_closed` behaviour, actor name).
4. Re-enable only after flex-auth runtime and registry are verified.
Evidence fields for the flip: flex-auth health URL, smoke script exit codes,
`warden activity --kind sign --json` showing `policy_decision_id` on allow path.
---
Rollback is a versioned profile or registry rollback. Do not reintroduce
`policy.enabled: false`: that would erase per-zone evidence and recreate the
global control ADR-0009 supersedes.
## See also
- `wiki/OpsWardenConfig.md` — full config reference
- `wiki/CredentialRouting.md`
- `~/flex-auth/docs/ops-warden-policy-gate-handoff.md` — flex-auth handoff
- `flex-auth/INTENT.md`
- `net-kingdom/docs/platform-identity-security-architecture.md`
- `tenancy.yaml`
- `docs/evidence/security-zone-admission-2026-08-22.md`
- `wiki/OpsWardenConfig.md`
- `wiki/WorkloadSecurityPosture.md`
- `history/2026-08-19-flex-auth-caller-identity-evidence.md`

View file

@ -111,6 +111,28 @@ This is the practical bridge from WARDEN-WP-0014 (`warden access`) to WP-0015: a
assist can remove manual secret handling friction, while posture/maturity decides
whether the secret may flow at all.
## Security-zone consumer contract
Security zones are now the prescriptive sibling of this descriptive posture
model. `zone-engine` owns `security-zones_v0.1`; ops-warden follows it as a
consumer under `ADR-0009`.
- Workload maturity and data classification are admission evidence, not zone
names and not control stance.
- The workload owner declares authoritative `workload_identity` and `zones:` in
the same `tenancy.yaml` service entry.
- Credential lanes and actor resources reference that workload explicitly.
Managed deployables use Repo Manager's exact `(rapp_id, name, deployable?)`
tuple; independent operational workloads reference their owner declaration.
- Missing identity, membership, or evidence resolves `unknown`. No compiler may
infer it from a path, repository owner, actor class, or environment.
- flex-auth owns pre-sign stance; ops-warden owns PEP dependency failure behavior,
the agent read boundary, and `warden plan` escalation behavior.
Ops-warden declares `z1-operational` with M1/internal/medium evidence. That is an
accuracy statement, not a target: M2 remains unavailable until SLO history,
on-call, and incident/recovery evidence exist.
---
## Canon layering (where each part lands)

View file

@ -62,9 +62,9 @@ When `WARDEN_AGENT_ID` is set and the lane is `risk: high`, `warden access --fet
```bash
export WARDEN_AGENT_ID=grok # or claude, codex
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
warden access railiance-backup-offsite-lane --no-policy --wrap
warden access railiance-backup-offsite-lane --no-policy --fingerprint
warden access railiance-backup-offsite-lane --out /tmp/nc.token
warden access railiance-backup-offsite-lane --wrap
warden access railiance-backup-offsite-lane --fingerprint
```
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.

View file

@ -47,10 +47,10 @@ Config (host/port/env names only):
2. Fetch via sanctioned transport (never paste into chat):
```bash
warden access binky-company-email-imap --all --no-policy --out /tmp/imap.user
warden access binky-company-email-imap --all --out /tmp/imap.user
# primary field is IMAP_USERNAME; for password use --field after template support
# or:
warden access binky-company-email-imap --all --no-policy --exec -- \
warden access binky-company-email-imap --all --exec -- \
env IMAP_USERNAME=… # prefer secrets-engine / dual-field exec when wired
```

View file

@ -53,7 +53,7 @@ Design: `binky-control/integrations/qonto-mcp.md`
```bash
# lengths / presence only when debugging
warden access binky-qonto-api --all --no-policy --exec -- \
warden access binky-qonto-api --all --exec -- \
sh -c 'export QONTO_API_KEY="$API_KEY" QONTO_ORGANIZATION_ID="$API_USER";
# then: qonto-mcp-server or curl thirdparty with Authorization login:key
:'

View file

@ -122,10 +122,11 @@ bridge up state-hub-railiance01
- [ ] Tunnel establishes without static cert file on disk
- [ ] Re-run `bridge up` after cert TTL expires — `cert_command` re-issues automatically
### 5. Policy gate (optional, after FLEX-WP-0007)
### 5. Zone-aware policy evidence
When `policy.enabled: true`, confirm `signatures.log` includes `policy_decision_id`
on tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
Confirm `signatures.log` records `policy_zone`, `policy_failure_mode`,
`policy_outcome`, and `policy_decision_id` when flex-auth returns a decision on
tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
---
@ -177,4 +178,4 @@ starting cutover (WARDEN-WP-0023).
- `wiki/OpsWardenConfig.md` — cert_command example
- `wiki/playbooks/operator-openbao-token-hygiene.md`
- `wiki/AuditTrail.md` — query recent signs via `warden activity`
- `warden route show ops-bridge-tunnel --json`
- `warden route show ops-bridge-tunnel --json`

View file

@ -35,9 +35,9 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
```bash
# Preferred: file or exec injection via warden
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
warden access railiance-backup-offsite-lane --out /tmp/nc.token
# or:
warden access railiance-backup-offsite-lane --no-policy --exec -- env | grep -v .
warden access railiance-backup-offsite-lane --exec -- env | grep -v .
```
If you must use raw bao in an interactive human shell (not an agent session):
@ -54,7 +54,7 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
3. **Proxy via warden access** (catalog active + resolvable):
```bash
warden access railiance-backup-offsite-lane --no-policy --fetch --out /tmp/nc.token
warden access railiance-backup-offsite-lane --fetch --out /tmp/nc.token
# Primary field is NC_WEBDAV_TOKEN. AGE_PRIVATE_KEY is recovery escrow only.
```

View file

@ -61,7 +61,7 @@ Never paste values into chat, State Hub, workplans, or Git.
3. **Or proxy via warden access** (same `bao kv get`, audited metadata only):
```bash
warden route show reuse-surface-hub-write-token --json
warden access reuse-surface-hub-write-token --no-policy --fetch
warden access reuse-surface-hub-write-token --fetch
```
4. **Webhook HMAC** (same path, second field — must match Forgejo org webhook):
@ -87,4 +87,4 @@ Never paste values into chat, State Hub, workplans, or Git.
Rotation: `railiance-platform/docs/reuse-surface-runtime-secrets-rotation-runbook.md`
(OpenBao patch → ESO `force-sync` → hub rollout → `make reuse-forgejo-webhook` when
the webhook HMAC changes → `make reuse-webhook-smoke`). Lifecycle:
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).

View file

@ -69,7 +69,7 @@ First worked lane: `tenants/binky/company-email/imap` (CCR-2026-0007).
```bash
bao login -method=oidc -path=netkingdom role=<lane>-workload-kv-read
warden access <catalog-id> --no-policy --out /tmp/secret.file # mode 0600
warden access <catalog-id> --out /tmp/secret.file # mode 0600
# agents: never raw --fetch stream; WARDEN_AGENT_ID + risk=high → exit 7
```

View file

@ -48,11 +48,10 @@ this token** — it is the access front door: `warden access` proxies the read f
**ops-warden transparent fallback** — same lane via the `warden access` proxy (fetches as
you, holds nothing). Field-verified flags (whynot-design, @whynot/design@0.4.0):
```bash
# --exec needs the env-var name; --no-policy is required while the gate is advisory
# (policy.enabled=false), else the call exits 4.
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN \
# --exec needs the env-var name. The zone-aware policy gate always runs first.
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
--exec -- npm publish
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN --fetch
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch
```
On either path the value transits to you (or the child env) and never enters
ops-warden's memory, disk, or audit log.

View file

@ -4,14 +4,14 @@ type: workplan
title: "Adopt security zones as a consumer — retire the global policy.enabled"
domain: infotech
repo: ops-warden
status: proposed
status: finished
owner: ops-warden
topic_slug: netkingdom
planning_priority: P1
depends_on_workplans:
- WARDEN-WP-0031
created: "2026-08-19"
updated: "2026-08-19"
updated: "2026-08-22"
state_hub_workstream_id: "f1b6cdcb-5e16-48ab-a72b-339e909e92f0"
---
@ -98,7 +98,7 @@ this repo's own input rather than out of supplying a new one.
```task
id: WARDEN-WP-0032-T02
status: wait
status: done
priority: high
state_hub_task_id: "08735f17-fe7e-4a6a-9b71-f350f98c30b1"
```
@ -144,9 +144,39 @@ 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.
```task
id: WARDEN-WP-0032-T03
status: wait
status: done
priority: medium
state_hub_task_id: "fd642377-8d64-473c-b684-f8f1a842c223"
```
@ -183,9 +213,16 @@ 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.
```task
id: WARDEN-WP-0032-T04
status: wait
status: done
priority: medium
state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
```
@ -195,6 +232,13 @@ state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
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.
```task
id: WARDEN-WP-0032-T05
status: done
@ -241,7 +285,7 @@ policy covers these paths (T06).
```task
id: WARDEN-WP-0032-T06
status: progress
status: done
priority: medium
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
```
@ -330,8 +374,53 @@ 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.
**Still open:** whether the maturity-derived default replaces the `ungraded`
sentinel entirely (waits on `ZONE-WP-0001-T03`).
**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 `z3`
`critical`; everything else → `high`). An explicit catalog grade always wins.
The platform deny policy and its live proof above close the direct-OpenBao half.
```task
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.
## Related