feat: adopt security zones and explicit workload refs
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
parent
12c637cbf2
commit
7ce58ae638
52 changed files with 1547 additions and 658 deletions
37
SCOPE.md
37
SCOPE.md
|
|
@ -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
|
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
|
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
|
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).
|
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
|
**The policy gate is zone-aware.** The caller-identity path is production proven
|
||||||
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
|
and the flex-auth pin enforces caller authentication. WP-0032 adopted
|
||||||
policy package (FLEX-WP-0006) and now runs the in-cluster pin
|
`security-zones_v0.1`: the repo-wide `policy.enabled` and `policy.fail_closed`
|
||||||
`flex-auth-ops-warden` (FLEX-WP-0007 finished; FLEX-WP-0016). `policy.enabled`
|
settings are retired, target workload membership compiles into flex-auth resource
|
||||||
remains **false**: that pin is in `callerAuth.mode: warn` because ops-warden sent
|
attributes, and ops-warden selects dependency failure behavior from the target
|
||||||
no caller identity. WP-0031 ships the calling side (`policy.caller_auth` +
|
zone. Unknown membership is explicit and uses the versioned build profile.
|
||||||
`scripts/check_policy_caller_identity.py`); the flip waits on an operator picking
|
Ops-warden itself declares `z1-operational` in `tenancy.yaml`.
|
||||||
a token source and flex-auth then enforcing.
|
|
||||||
|
|
||||||
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
|
**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
|
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
|
**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`).
|
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
|
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
|
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).
|
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-0003` | Cover gaps, but never silently own them |
|
||||||
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
|
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
|
||||||
| `ADR-0005` | Implement one lane narrowly, route everything else |
|
| `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-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-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
|
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
|
||||||
credential-management standard, the-custodian's ADR-001 workplan convention — are
|
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
|
- `cert_command`: `warden sign <actor> --pubkey <path>` → cert on stdout
|
||||||
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
|
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
|
||||||
- `warden status`, cleanup, scorecard, signatures log
|
- `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`,
|
- Production flex-auth registry builder (`scripts/build_flex_auth_registry.py`,
|
||||||
`registry/flex-auth/production_registry_snapshot.json`)
|
`registry/flex-auth/production_registry_snapshot.json`)
|
||||||
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
|
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
|
||||||
|
|
@ -247,7 +248,7 @@ for the rest.
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| WP-0001–0005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
|
| WP-0001–0005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
|
||||||
| WP-0006 | Credential routing, security map, inventory patterns, OpenBao checklist |
|
| 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-0008 | Production sign verification, stewardship closeout, archive hygiene |
|
||||||
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
|
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
|
||||||
| WP-0010 | Access routing charter + pointer catalog |
|
| WP-0010 | Access routing charter + pointer catalog |
|
||||||
|
|
@ -280,7 +281,7 @@ for the rest.
|
||||||
| WP | Status | Focus |
|
| WP | Status | Focus |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| WP-0027 | `backlog` | Tamper-resistant credential governance + mass rotation/lockdown (Strand B follow-on to WP-0026) |
|
| 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 |
|
| 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).
|
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).
|
- **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
|
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**
|
`callerAuth.mode: enforce` (FLEX-WP-0016) — the gate is **ready and verified**
|
||||||
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). `policy.enabled`
|
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). WP-0032 and
|
||||||
remains **false by decision**, not by blocker: `ADR-0006` scopes enforcement to
|
`ADR-0009` retired the global switch: the compiled target workload selects the
|
||||||
security zones, which `zone-engine` defines (`ZONE-WP-0001`). Re-check with
|
zone, flex-auth owns stance, and ops-warden applies the zone's PEP failure mode.
|
||||||
`scripts/check_policy_caller_identity.py`
|
Re-check caller identity with `scripts/check_policy_caller_identity.py`.
|
||||||
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
|
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
|
||||||
conformance checker, dev doubles); canon landing owner-driven
|
conformance checker, dev doubles); canon landing owner-driven
|
||||||
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +
|
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
| 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-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-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 |
|
| 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-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
|
||||||
| task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.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-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-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-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-T02 | progress | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||||
| task | WARDEN-WP-0032-T03 | wait | — | 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 | wait | — | 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-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-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-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@ type: adr
|
||||||
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
|
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: ops-warden
|
repo: ops-warden
|
||||||
status: accepted
|
status: superseded
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
revision: "1"
|
revision: "1"
|
||||||
owner: ops-warden
|
owner: ops-warden
|
||||||
binds: "ops-warden"
|
binds: "ops-warden"
|
||||||
created: "2026-08-19"
|
created: "2026-08-19"
|
||||||
updated: "2026-08-19"
|
updated: "2026-08-22"
|
||||||
last_reviewed: "2026-08-19"
|
last_reviewed: "2026-08-19"
|
||||||
review_interval: 6m
|
review_interval: 6m
|
||||||
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
|
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
|
||||||
supersedes: ""
|
supersedes: ""
|
||||||
successor: ""
|
successor: "ops-warden-adr-0009"
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-0006 — Enforcement is zone-scoped, never a global flag
|
# ADR-0006 — Enforcement is zone-scoped, never a global flag
|
||||||
|
|
|
||||||
101
docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md
Normal file
101
docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md
Normal 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`
|
||||||
|
|
@ -67,6 +67,7 @@ and the publication is a defect.
|
||||||
| `ADR-0003` | Cover gaps, but never silently own them | ops-warden |
|
| `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-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-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-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-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 |
|
||||||
|
|
|
||||||
34
docs/evidence/security-zone-admission-2026-08-22.md
Normal file
34
docs/evidence/security-zone-admission-2026-08-22.md
Normal 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`.
|
||||||
|
|
@ -10,6 +10,9 @@ actors:
|
||||||
- agt-task-bridge
|
- agt-task-bridge
|
||||||
ttl_hours: 24
|
ttl_hours: 24
|
||||||
description: "ops-bridge tunnel agent for state-hub"
|
description: "ops-bridge tunnel agent for state-hub"
|
||||||
|
zone_subject:
|
||||||
|
applicability: applicable
|
||||||
|
workload_id: ops-bridge-tunnel
|
||||||
|
|
||||||
agt-codex-interhub-bootstrap:
|
agt-codex-interhub-bootstrap:
|
||||||
type: agt
|
type: agt
|
||||||
|
|
@ -17,6 +20,9 @@ actors:
|
||||||
- agt-interhub-bootstrap
|
- agt-interhub-bootstrap
|
||||||
ttl_hours: 2
|
ttl_hours: 2
|
||||||
description: "Short-lived agent access for attended Inter-Hub bootstrap"
|
description: "Short-lived agent access for attended Inter-Hub bootstrap"
|
||||||
|
zone_subject:
|
||||||
|
applicability: applicable
|
||||||
|
workload_id: codex-interhub-bootstrap
|
||||||
|
|
||||||
adm-example:
|
adm-example:
|
||||||
type: adm
|
type: adm
|
||||||
|
|
@ -24,6 +30,9 @@ actors:
|
||||||
- adm-full
|
- adm-full
|
||||||
ttl_hours: 48
|
ttl_hours: 48
|
||||||
description: "Example human operator — replace with per-person adm-* actors"
|
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:
|
atm-backup-daily:
|
||||||
type: atm
|
type: atm
|
||||||
|
|
@ -31,6 +40,9 @@ actors:
|
||||||
- atm-backup-daily
|
- atm-backup-daily
|
||||||
ttl_hours: 8
|
ttl_hours: 8
|
||||||
description: "Example nightly automation actor"
|
description: "Example nightly automation actor"
|
||||||
|
zone_subject:
|
||||||
|
applicability: applicable
|
||||||
|
workload_id: backup-daily
|
||||||
|
|
||||||
hosts:
|
hosts:
|
||||||
example-host:
|
example-host:
|
||||||
|
|
@ -38,4 +50,4 @@ hosts:
|
||||||
agt:
|
agt:
|
||||||
- agt-task-bridge
|
- agt-task-bridge
|
||||||
atm:
|
atm:
|
||||||
- atm-backup-daily
|
- atm-backup-daily
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,24 @@ vault:
|
||||||
inventory_path: ~/.config/warden/inventory.yaml
|
inventory_path: ~/.config/warden/inventory.yaml
|
||||||
state_dir: ~/.local/state/warden
|
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).
|
# 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
|
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
|
||||||
policy:
|
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
|
# 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
|
# 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.
|
# 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
|
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
|
tenant: tenant:platform
|
||||||
subject_env: WARDEN_POLICY_SUBJECT
|
subject_env: WARDEN_POLICY_SUBJECT
|
||||||
system: ops-warden
|
system: ops-warden
|
||||||
|
|
@ -44,4 +50,4 @@ policy:
|
||||||
# command: kubectl create token ops-warden -n ops-warden
|
# command: kubectl create token ops-warden -n ops-warden
|
||||||
# --audience flex-auth --duration 10m
|
# --audience flex-auth --duration 10m
|
||||||
token_env: WARDEN_POLICY_CALLER_TOKEN
|
token_env: WARDEN_POLICY_CALLER_TOKEN
|
||||||
audience: flex-auth
|
audience: flex-auth
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"flex_auth_contract": "protected-system-v0",
|
"flex_auth_contract": "protected-system-v0",
|
||||||
"ops_warden_policy_gate": "v2",
|
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||||
"policy_enabled_config": "policy.enabled",
|
"security_zone_standard": "security-zones_v0.1",
|
||||||
"tenant": "tenant:platform"
|
"tenant": "tenant:platform"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,6 @@
|
||||||
"ssh-signing",
|
"ssh-signing",
|
||||||
"adm"
|
"adm"
|
||||||
],
|
],
|
||||||
"trust_zone": "platform",
|
|
||||||
"owner": "team:platform-security",
|
"owner": "team:platform-security",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"actor_id": "adm-example",
|
"actor_id": "adm-example",
|
||||||
|
|
@ -78,7 +77,10 @@
|
||||||
"allowed_principals": [
|
"allowed_principals": [
|
||||||
"adm-full"
|
"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",
|
"ssh-signing",
|
||||||
"agt"
|
"agt"
|
||||||
],
|
],
|
||||||
"trust_zone": "platform",
|
|
||||||
"owner": "team:platform-security",
|
"owner": "team:platform-security",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"actor_id": "agt-codex-interhub-bootstrap",
|
"actor_id": "agt-codex-interhub-bootstrap",
|
||||||
|
|
@ -100,7 +101,11 @@
|
||||||
"allowed_principals": [
|
"allowed_principals": [
|
||||||
"agt-interhub-bootstrap"
|
"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",
|
"ssh-signing",
|
||||||
"agt"
|
"agt"
|
||||||
],
|
],
|
||||||
"trust_zone": "platform",
|
|
||||||
"owner": "team:platform-security",
|
"owner": "team:platform-security",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"actor_id": "agt-state-hub-bridge",
|
"actor_id": "agt-state-hub-bridge",
|
||||||
|
|
@ -122,7 +126,11 @@
|
||||||
"allowed_principals": [
|
"allowed_principals": [
|
||||||
"agt-task-bridge"
|
"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",
|
"ssh-signing",
|
||||||
"atm"
|
"atm"
|
||||||
],
|
],
|
||||||
"trust_zone": "platform",
|
|
||||||
"owner": "team:platform-security",
|
"owner": "team:platform-security",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"actor_id": "atm-backup-daily",
|
"actor_id": "atm-backup-daily",
|
||||||
|
|
@ -144,7 +151,11 @@
|
||||||
"allowed_principals": [
|
"allowed_principals": [
|
||||||
"atm-backup-daily"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@
|
||||||
# declares it, and is null where the field set has not been established --
|
# declares it, and is null where the field set has not been established --
|
||||||
# null means unknown, never 'one field'.
|
# 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
|
source: ops-warden/registry/routing/catalog.yaml
|
||||||
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
|
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
|
||||||
catalog_revision_date: "2026-08-21T13:26:10+02:00"
|
catalog_revision_date: "2026-08-21T13:26:10+02:00"
|
||||||
catalog_dirty: false
|
catalog_dirty: true
|
||||||
high_risk_lane_count: 19
|
high_risk_lane_count: 19
|
||||||
concrete_path_count: 14
|
concrete_path_count: 14
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@
|
||||||
# canon_ref upstream net-kingdom doc the wiki section tracks
|
# canon_ref upstream net-kingdom doc the wiki section tracks
|
||||||
# reviewed date this pointer was last checked against canon (YYYY-MM-DD)
|
# reviewed date this pointer was last checked against canon (YYYY-MM-DD)
|
||||||
# status active (surfaced by default) | draft (hidden unless --all)
|
# 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
|
# steps ONLY when warden_executes: true
|
||||||
# cert_command ONLY when warden_executes: true
|
# cert_command ONLY when warden_executes: true
|
||||||
# delegation WP-0030 register. mode: native | interim | permanent.
|
# delegation WP-0030 register. mode: native | interim | permanent.
|
||||||
|
|
@ -39,6 +44,9 @@ entries:
|
||||||
title: Short-lived SSH certificate for host / ops reachability
|
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).
|
# Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05).
|
||||||
risk: standard
|
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]
|
need_keywords: [ssh, certificate, cert, host, access, sign, adm, agt, atm, reachability, ops]
|
||||||
owner_repo: ops-warden
|
owner_repo: ops-warden
|
||||||
subsystem: ops-warden
|
subsystem: ops-warden
|
||||||
|
|
@ -61,6 +69,10 @@ entries:
|
||||||
title: Scoped OpenBao token for ops-warden SSH signing (warden-sign)
|
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).
|
# 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
|
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]
|
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
|
owner_repo: railiance-platform
|
||||||
subsystem: OpenBao credential broker
|
subsystem: OpenBao credential broker
|
||||||
|
|
@ -97,6 +109,9 @@ entries:
|
||||||
title: API key, DB credential, or dynamic lease
|
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).
|
# 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
|
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]
|
need_keywords: [api, key, secret, database, db, password, token, lease, openbao, vault, kv, dynamic, credential, npm, npm_auth_token, registry]
|
||||||
owner_repo: railiance-platform
|
owner_repo: railiance-platform
|
||||||
subsystem: OpenBao
|
subsystem: OpenBao
|
||||||
|
|
@ -133,6 +148,9 @@ entries:
|
||||||
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
|
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).
|
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
|
||||||
risk: high
|
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]
|
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package]
|
||||||
owner_repo: railiance-platform
|
owner_repo: railiance-platform
|
||||||
subsystem: OpenBao
|
subsystem: OpenBao
|
||||||
|
|
@ -177,6 +195,9 @@ entries:
|
||||||
title: Authorization decision — may this actor perform this action
|
title: Authorization decision — may this actor perform this action
|
||||||
# Returns an authorization decision; no credential flows (WARDEN-WP-0032-T05).
|
# Returns an authorization decision; no credential flows (WARDEN-WP-0032-T05).
|
||||||
risk: standard
|
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]
|
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision]
|
||||||
owner_repo: flex-auth
|
owner_repo: flex-auth
|
||||||
subsystem: flex-auth
|
subsystem: flex-auth
|
||||||
|
|
@ -195,6 +216,9 @@ entries:
|
||||||
title: Interactive login, OIDC token, or MFA
|
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).
|
# 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
|
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]
|
need_keywords: [login, oidc, identity, mfa, token, jwt, sso, keycloak, key-cape, iam, claims, authenticate, signin]
|
||||||
owner_repo: key-cape
|
owner_repo: key-cape
|
||||||
subsystem: key-cape / Keycloak
|
subsystem: key-cape / Keycloak
|
||||||
|
|
@ -222,6 +246,9 @@ entries:
|
||||||
title: SSH tunnel or port forward
|
title: SSH tunnel or port forward
|
||||||
# Routes to ops-bridge and supplies a cert_command; no secret value flows (WARDEN-WP-0032-T05).
|
# Routes to ops-bridge and supplies a cert_command; no secret value flows (WARDEN-WP-0032-T05).
|
||||||
risk: standard
|
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]
|
need_keywords: [tunnel, port, forward, bridge, ops-bridge, reverse, transport, ssh-tunnel, cert_command]
|
||||||
owner_repo: ops-bridge
|
owner_repo: ops-bridge
|
||||||
subsystem: ops-bridge
|
subsystem: ops-bridge
|
||||||
|
|
@ -240,6 +267,9 @@ entries:
|
||||||
title: Host SSH principal file or force-command deployment
|
title: Host SSH principal file or force-command deployment
|
||||||
# Principal-file deployment via Ansible; no secret value flows (WARDEN-WP-0032-T05).
|
# Principal-file deployment via Ansible; no secret value flows (WARDEN-WP-0032-T05).
|
||||||
risk: standard
|
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]
|
need_keywords: [principal, auth_principals, force-command, host, sshd, hardening, railiance-infra, ansible]
|
||||||
owner_repo: railiance-infra
|
owner_repo: railiance-infra
|
||||||
subsystem: railiance-infra
|
subsystem: railiance-infra
|
||||||
|
|
@ -258,6 +288,9 @@ entries:
|
||||||
title: Inter-Hub bootstrap SSH envelope
|
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).
|
# 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
|
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]
|
need_keywords: [inter-hub, interhub, bootstrap, ops-hub, agt-interhub-bootstrap, envelope, force-command, CUST-WP-0049]
|
||||||
owner_repo: ops-warden
|
owner_repo: ops-warden
|
||||||
subsystem: ops-warden + railiance-infra
|
subsystem: ops-warden + railiance-infra
|
||||||
|
|
@ -276,6 +309,9 @@ entries:
|
||||||
title: activity-core IssueSink → issue-core REST emission
|
title: activity-core IssueSink → issue-core REST emission
|
||||||
# Emission routing only — the API key is a separate lane (WARDEN-WP-0032-T05).
|
# Emission routing only — the API key is a separate lane (WARDEN-WP-0032-T05).
|
||||||
risk: standard
|
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]
|
need_keywords: [activity-core, issue-sink, issue-core, emission, issue_core_url, issue_core_api_key, tasks, ingest, rest, issuesink]
|
||||||
owner_repo: activity-core
|
owner_repo: activity-core
|
||||||
subsystem: activity-core + issue-core
|
subsystem: activity-core + issue-core
|
||||||
|
|
@ -301,6 +337,11 @@ entries:
|
||||||
# A Forgejo backend token is not recovered by rotating an ingestion key.
|
# A Forgejo backend token is not recovered by rotating an ingestion key.
|
||||||
# Found by secrets-engine reviewing SECRETS-WP-0006 -- not by us.
|
# Found by secrets-engine reviewing SECRETS-WP-0006 -- not by us.
|
||||||
risk: high
|
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
|
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]
|
need_keywords: [issue-core, ingestion, api, key, openbao, issue_core_api_key, eso, external-secrets]
|
||||||
owner_repo: railiance-platform
|
owner_repo: railiance-platform
|
||||||
|
|
@ -349,6 +390,9 @@ entries:
|
||||||
# rotation. Disclosure lets an attacker forge webhook deliveries into the
|
# rotation. Disclosure lets an attacker forge webhook deliveries into the
|
||||||
# federation hub, which rotating the write token alone does not undo.
|
# federation hub, which rotating the write token alone does not undo.
|
||||||
risk: high
|
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
|
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]
|
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
|
owner_repo: railiance-platform
|
||||||
|
|
@ -404,6 +448,9 @@ entries:
|
||||||
verified: owner-confirmed
|
verified: owner-confirmed
|
||||||
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
||||||
risk: high
|
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
|
# 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
|
# (promoted 2026-07-02): policy workload-kv-read-llm-connect-provider-secrets and k8s
|
||||||
# auth role external-secrets-activity-core applied; ExternalSecret
|
# auth role external-secrets-activity-core applied; ExternalSecret
|
||||||
|
|
@ -445,6 +492,9 @@ entries:
|
||||||
verified: unverified
|
verified: unverified
|
||||||
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
||||||
risk: high
|
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.
|
# 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-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;
|
# capabilities=read on data path; default-policy and agent-high-risk-boundary = deny;
|
||||||
|
|
@ -491,6 +541,9 @@ entries:
|
||||||
verified: owner-confirmed
|
verified: owner-confirmed
|
||||||
# High-risk: site-admin PAT (WP-0026 T04).
|
# High-risk: site-admin PAT (WP-0026 T04).
|
||||||
risk: high
|
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
|
# 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
|
# 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
|
# bao.coulomb.social; PAT attended-minted and stored under field API_TOKEN at
|
||||||
|
|
@ -533,6 +586,9 @@ entries:
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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
|
# CCR-2026-0007: tenants/ mount + policy + OIDC role applied; founder provisioned
|
||||||
# values via UI (version ≥2, not placeholder). Capabilities-safe verify 2026-07-17:
|
# 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).
|
# lane-policy read; default deny. Host: imap.ionos.de:993 (binky-control config).
|
||||||
|
|
@ -568,6 +624,9 @@ entries:
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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
|
# 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
|
# (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).
|
# qonto-mcp-server. First read-only pull 2026-07-21 (BINKY-WP-0005-T05).
|
||||||
|
|
@ -604,6 +663,11 @@ entries:
|
||||||
reviewed: "2026-08-17"
|
reviewed: "2026-08-17"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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"
|
auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange"
|
||||||
path_template: "platform/workloads/rapp-qonto/keycape-client"
|
path_template: "platform/workloads/rapp-qonto/keycape-client"
|
||||||
fetch_command: "bao kv get -field=client_secret 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"
|
reviewed: "2026-08-11"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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
|
# Provisioned 2026-07-17 on railiance01: ed25519 keypair on host, OpenBao copy at
|
||||||
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
|
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
|
||||||
# coulomb/executor-sandbox (title agent-harness-railiance01). Git push verified.
|
# coulomb/executor-sandbox (title agent-harness-railiance01). Git push verified.
|
||||||
|
|
@ -671,6 +738,9 @@ entries:
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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
|
# 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
|
# workload-kv-read-binky-company-email-imap; role_id/secret_id delivered to
|
||||||
# railiance01 ~/.local/agent-harness/approle-binky-mail (0600). Positive IMAP field
|
# railiance01 ~/.local/agent-harness/approle-binky-mail (0600). Positive IMAP field
|
||||||
|
|
@ -696,6 +766,9 @@ entries:
|
||||||
title: Object-storage STS / temporary S3 credentials
|
title: Object-storage STS / temporary S3 credentials
|
||||||
# Temporary S3 credentials are still credentials (WARDEN-WP-0032-T05).
|
# Temporary S3 credentials are still credentials (WARDEN-WP-0032-T05).
|
||||||
risk: high
|
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]
|
need_keywords: [s3, sts, object-storage, minio, artifact-store, temporary, credentials, bucket, vending]
|
||||||
owner_repo: net-kingdom
|
owner_repo: net-kingdom
|
||||||
subsystem: flex-auth + OpenBao + artifact-store
|
subsystem: flex-auth + OpenBao + artifact-store
|
||||||
|
|
@ -726,6 +799,9 @@ entries:
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
|
workload_ref:
|
||||||
|
applicability: not-applicable
|
||||||
|
reason: "Generic database credential-vending pattern; concrete consumers carry workload references."
|
||||||
exec_capable: false
|
exec_capable: false
|
||||||
|
|
||||||
- id: rein-openweights-openrouter-approle
|
- id: rein-openweights-openrouter-approle
|
||||||
|
|
@ -745,6 +821,9 @@ entries:
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: standard
|
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
|
# 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
|
# Worsch 2026-07-27. Policy + AppRole live; reins/ KV v2 mount created
|
||||||
# (no existing mount fit without widening scope beyond what was
|
# (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.
|
# railiance-apps; user-engine is consumer-only and claims no lane here.
|
||||||
consumers: [user-engine]
|
consumers: [user-engine]
|
||||||
risk: standard
|
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).
|
# K8s assembly is the live handoff today (same pattern as vergage-teilnahme-env).
|
||||||
# OpenBao KV platform/workloads/coulomb/coulomb-social/runtime-env is the
|
# OpenBao KV platform/workloads/coulomb/coulomb-social/runtime-env is the
|
||||||
# future custody home — CCR not yet applied; resolvable via operator script.
|
# 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)
|
title: audit-core sender registry (write and operator-read tokens)
|
||||||
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
|
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
|
||||||
risk: high
|
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]
|
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS]
|
||||||
owner_repo: ops-mason
|
owner_repo: ops-mason
|
||||||
subsystem: OpenBao + audit-core
|
subsystem: OpenBao + audit-core
|
||||||
|
|
@ -864,6 +949,9 @@ entries:
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
verified: owner-confirmed
|
verified: owner-confirmed
|
||||||
risk: high
|
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):
|
# 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,
|
# policies external-secrets-email-connect + workload-kv-read-email-connect-transactional,
|
||||||
# KV platform/workloads/email-connect/transactional v1, ESO token Secret,
|
# KV platform/workloads/email-connect/transactional v1, ESO token Secret,
|
||||||
|
|
@ -906,6 +994,9 @@ entries:
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
verified: unverified
|
verified: unverified
|
||||||
risk: high
|
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.
|
# CCR-2026-0011. Values via founder paste-once or local tfvars ingest.
|
||||||
# Not the Barman runtime key (platform-pg-backup-s3).
|
# Not the Barman runtime key (platform-pg-backup-s3).
|
||||||
auth_method: "caller's own OpenBao token (founder / operator workstation)"
|
auth_method: "caller's own OpenBao token (founder / operator workstation)"
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
actors: dict[str, Any] = inventory.get("actors") or {}
|
||||||
resources: list[dict[str, Any]] = []
|
resources: list[dict[str, Any]] = []
|
||||||
subjects: list[dict[str, Any]] = []
|
subjects: list[dict[str, Any]] = []
|
||||||
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
|
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
|
||||||
relationships: list[dict[str, Any]] = []
|
relationships: list[dict[str, Any]] = []
|
||||||
|
resolutions = _resolved_by_workload(zone_resolutions)
|
||||||
|
|
||||||
for name, entry in sorted(actors.items()):
|
for name, entry in sorted(actors.items()):
|
||||||
actor_type = str(entry["type"])
|
actor_type = str(entry["type"])
|
||||||
|
|
@ -74,7 +141,6 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
||||||
"id": resource_id,
|
"id": resource_id,
|
||||||
"type": "ssh-certificate",
|
"type": "ssh-certificate",
|
||||||
"labels": ["ssh-signing", actor_type],
|
"labels": ["ssh-signing", actor_type],
|
||||||
"trust_zone": "platform",
|
|
||||||
"owner": "team:platform-security",
|
"owner": "team:platform-security",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"actor_id": name,
|
"actor_id": name,
|
||||||
|
|
@ -82,6 +148,7 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
||||||
"allowed_subjects": [name, f"iam:{name}"],
|
"allowed_subjects": [name, f"iam:{name}"],
|
||||||
"allowed_principals": principals,
|
"allowed_principals": principals,
|
||||||
"max_ttl_hours": ttl_hours,
|
"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"],
|
"caring_profiles": ["caring-0.4.0-rc2"],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"flex_auth_contract": "protected-system-v0",
|
"flex_auth_contract": "protected-system-v0",
|
||||||
"ops_warden_policy_gate": "v2",
|
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||||
"policy_enabled_config": "policy.enabled",
|
"security_zone_standard": "security-zones_v0.1",
|
||||||
"tenant": "tenant:platform",
|
"tenant": "tenant:platform",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -186,14 +253,24 @@ def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
|
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
|
||||||
parser.add_argument("-o", "--output", type=Path, required=True)
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
inventory = yaml.safe_load(args.inventory.read_text()) or {}
|
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.parent.mkdir(parents=True, exist_ok=True)
|
||||||
args.output.write_text(json.dumps(registry, indent=2) + "\n")
|
args.output.write_text(json.dumps(registry, indent=2) + "\n")
|
||||||
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")
|
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/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:
|
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
|
||||||
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
|
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
|
||||||
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
|
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
|
||||||
but a caller that sends no ``Authorization`` header only produces a
|
but a caller that sends no ``Authorization`` header only produces a
|
||||||
``caller authentication warning`` and is still served. That pin cannot move to
|
``caller authentication warning`` and is still served. That pin cannot move to
|
||||||
``enforce`` — and therefore ``policy.enabled: true`` cannot be set — until
|
``enforce`` until ops-warden's calling side actually presents a token. The
|
||||||
ops-warden's calling side actually presents a token.
|
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
|
||||||
|
|
||||||
This script asserts the calling side *without* flipping anything:
|
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))]
|
return [("fail", "warden.yaml", str(e))]
|
||||||
|
|
||||||
policy = cfg.policy
|
policy = cfg.policy
|
||||||
checks.append(
|
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
|
||||||
("ok", "warden.yaml", f"loaded; policy.enabled={str(policy.enabled).lower()}")
|
|
||||||
)
|
|
||||||
|
|
||||||
mode = policy.caller_auth.mode
|
mode = policy.caller_auth.mode
|
||||||
if mode == "none":
|
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
|
target = url or policy.flex_auth_url
|
||||||
if url is None and not policy.enabled:
|
if target is None:
|
||||||
checks.append(
|
checks.append(
|
||||||
(
|
(
|
||||||
"skip",
|
"skip",
|
||||||
"live /v1/check",
|
"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
|
return checks
|
||||||
|
|
@ -188,13 +186,13 @@ def main() -> int:
|
||||||
if failed:
|
if failed:
|
||||||
print(
|
print(
|
||||||
f"\nNOT READY — {len(failed)} check(s) failed. "
|
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
|
return 1
|
||||||
print(
|
print(
|
||||||
"\nREADY — the calling side presents an identity. Next: tell flex-auth to set "
|
"\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 "
|
"callerAuth.mode: enforce on flex-auth-ops-warden and re-run this check. "
|
||||||
"policy.enabled: true with fail_closed: true."
|
"Zone-specific PEP failure modes already replace the retired global switches."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,13 @@ def main() -> int:
|
||||||
if args.check:
|
if args.check:
|
||||||
current = args.out.read_text() if args.out.exists() else ""
|
current = args.out.read_text() if args.out.exists() else ""
|
||||||
# generated_at always differs; compare everything 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:"))
|
def strip_generated_at(text: str) -> str:
|
||||||
if strip(current) != strip(content):
|
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.")
|
print(f"STALE: {args.out} does not match the catalog. Re-run without --check.")
|
||||||
return 1
|
return 1
|
||||||
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")
|
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,8 @@ ca_key: $SMOKE_DIR/ca_key
|
||||||
state_dir: $SMOKE_DIR/state
|
state_dir: $SMOKE_DIR/state
|
||||||
inventory_path: $INVENTORY
|
inventory_path: $INVENTORY
|
||||||
policy:
|
policy:
|
||||||
enabled: true
|
|
||||||
flex_auth_url: http://$ADDR
|
flex_auth_url: http://$ADDR
|
||||||
fail_closed: true
|
zone_registry_path: $REGISTRY
|
||||||
tenant: tenant:platform
|
tenant: tenant:platform
|
||||||
system: ops-warden
|
system: ops-warden
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -106,9 +105,8 @@ vault:
|
||||||
inventory_path: $INVENTORY
|
inventory_path: $INVENTORY
|
||||||
state_dir: $SMOKE_DIR/state-vault
|
state_dir: $SMOKE_DIR/state-vault
|
||||||
policy:
|
policy:
|
||||||
enabled: true
|
|
||||||
flex_auth_url: http://$ADDR
|
flex_auth_url: http://$ADDR
|
||||||
fail_closed: true
|
zone_registry_path: $REGISTRY
|
||||||
tenant: tenant:platform
|
tenant: tenant:platform
|
||||||
system: ops-warden
|
system: ops-warden
|
||||||
EOF
|
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"
|
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
|
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
234
scripts/report_workload_join.py
Executable file → Normal file
|
|
@ -1,128 +1,156 @@
|
||||||
#!/usr/bin/env python3
|
#!/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
|
The catalog owner declares whether each lane is workload-applicable. Managed
|
||||||
running thing and whoever answers for it. ops-warden's catalog is a credential
|
deployables use the exact Repo Manager v1 ``(rapp_id, name, deployable?)``
|
||||||
surface, so on its own it cannot name that subject. The other side of the join
|
reference. Independently governed operational workloads use ``name`` plus an
|
||||||
lives in `rapp-*/declarations/rapp.yaml`, which declares `workload_identity`
|
owner declaration reference. Unknown and not-applicable are explicit results.
|
||||||
along with `data_classification` and `criticality`; ops-warden's
|
|
||||||
`registry/policy/security-posture.yaml` then maps classification to a minimum
|
|
||||||
maturity via `dataclass_floor`.
|
|
||||||
|
|
||||||
This script computes the join and reports its coverage. It asserts nothing it
|
This script never parses a credential path, consults ``owner_repo`` as an
|
||||||
cannot derive: a lane whose workload cannot be established is reported as
|
identity hint, or substitutes a repository name. It reads declarations only.
|
||||||
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]
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
_SRC = Path(__file__).resolve().parent.parent / "src"
|
import yaml
|
||||||
if _SRC.is_dir() and str(_SRC) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_SRC))
|
|
||||||
|
|
||||||
import yaml # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def load_rapp_workloads(root: Path) -> Dict[str, Dict[str, Any]]:
|
def load_rapp_workloads(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
|
||||||
"""workload name -> declaration, from every rapp-*/declarations/rapp.yaml."""
|
"""Exact Repo Manager v1 key -> authoritative declaration projection."""
|
||||||
out: Dict[str, Dict[str, Any]] = {}
|
out: dict[tuple[str, str], dict[str, Any]] = {}
|
||||||
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
|
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(decl.read_text()) or {}
|
data = yaml.safe_load(decl.read_text()) or {}
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
continue
|
continue
|
||||||
identity = data.get("workload_identity") or {}
|
identity = data.get("workload_identity") or {}
|
||||||
|
rapp_id = data.get("rapp_id")
|
||||||
name = identity.get("name")
|
name = identity.get("name")
|
||||||
if not name:
|
if not rapp_id or not name:
|
||||||
continue
|
continue
|
||||||
out[str(name)] = {
|
deployables = []
|
||||||
"rapp_id": data.get("rapp_id"),
|
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"),
|
"data_classification": data.get("data_classification"),
|
||||||
"criticality": data.get("criticality"),
|
"criticality": data.get("criticality"),
|
||||||
"readiness_state": data.get("readiness_state"),
|
"readiness_state": data.get("readiness_state"),
|
||||||
"bound_reefs": data.get("bound_reefs"),
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def candidate_workloads(path_template: Optional[str]) -> List[str]:
|
def _direct_declaration_path(
|
||||||
"""Workload names a path could plausibly name — never a single guess.
|
declaration_ref: str, *, catalog_path: Path, estate_root: Path
|
||||||
|
) -> Path:
|
||||||
The convention is `platform/workloads/<domain>/<workload>/<bundle>`, but it
|
ref = Path(declaration_ref)
|
||||||
is not applied consistently: `platform/workloads/rapp-qonto/keycape-client`
|
if ref.is_absolute():
|
||||||
has one level fewer, so position alone cannot say which segment is the
|
return ref
|
||||||
workload. Both are offered and matched against declarations.
|
local = catalog_path.resolve().parents[2] / ref
|
||||||
"""
|
return local if local.exists() else estate_root / ref
|
||||||
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 build(catalog_path: Path, rapp_root: Path) -> Dict[str, Any]:
|
def _resolve_direct(
|
||||||
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
|
ref: dict[str, Any], *, catalog_path: Path, estate_root: Path
|
||||||
declared = load_rapp_workloads(rapp_root)
|
) -> tuple[dict[str, Any] | None, str | None]:
|
||||||
floor = (
|
source = _direct_declaration_path(
|
||||||
yaml.safe_load(
|
str(ref["declaration_ref"]), catalog_path=catalog_path, estate_root=estate_root
|
||||||
(catalog_path.parent.parent / "policy" / "security-posture.yaml").read_text()
|
)
|
||||||
|
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 {}
|
context = (declaration.get("zones") or {}).get("context", {})
|
||||||
).get("dataclass_floor", {})
|
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:
|
for entry in entries:
|
||||||
lane = entry.get("id")
|
lane = str(entry.get("id"))
|
||||||
cands = candidate_workloads(entry.get("path_template"))
|
ref = entry.get("workload_ref") or {}
|
||||||
if not cands:
|
applicability = ref.get("applicability")
|
||||||
no_path.append({"lane": lane, "risk": entry.get("risk")})
|
if applicability == "not-applicable":
|
||||||
|
not_applicable.append({"lane": lane, "reason": ref.get("reason")})
|
||||||
continue
|
continue
|
||||||
hit = next((c for c in cands if c in declared), None)
|
if applicability != "applicable":
|
||||||
if hit is None:
|
unknown.append({"lane": lane, "reason": "applicability missing or invalid"})
|
||||||
unmatched.append(
|
continue
|
||||||
{"lane": lane, "risk": entry.get("risk"), "candidates": cands}
|
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
|
continue
|
||||||
decl = declared[hit]
|
|
||||||
cls = decl.get("data_classification")
|
classification = projection.get("data_classification")
|
||||||
matched.append(
|
resolved.append(
|
||||||
{
|
{
|
||||||
"lane": lane,
|
"lane": lane,
|
||||||
"workload": hit,
|
"workload_ref": ref,
|
||||||
"risk": entry.get("risk"),
|
"source": projection.get("source"),
|
||||||
"data_classification": cls,
|
"data_classification": classification,
|
||||||
"criticality": decl.get("criticality"),
|
"criticality": projection.get("criticality"),
|
||||||
"min_maturity": floor.get(cls),
|
"maturity": projection.get("maturity") or floor.get(classification),
|
||||||
"unmapped_classification": bool(cls) and cls not in floor,
|
"declared_zone": projection.get("declared_zone"),
|
||||||
|
"unmapped_classification": bool(classification) and classification not in floor,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"declared_workloads": declared,
|
"contract": "helixforge.workload-reference/v1",
|
||||||
"matched": matched,
|
"resolved": resolved,
|
||||||
"unmatched": unmatched,
|
"unknown": unknown,
|
||||||
"no_path": no_path,
|
"not_applicable": not_applicable,
|
||||||
"dataclass_floor": floor,
|
"ok": len(resolved) + len(unknown) + len(not_applicable) == len(entries),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
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("--json", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--catalog",
|
"--catalog",
|
||||||
|
|
@ -133,40 +161,24 @@ def main() -> int:
|
||||||
/ "catalog.yaml",
|
/ "catalog.yaml",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
report = build(args.catalog, args.estate_root)
|
||||||
report = build(args.catalog, Path(os.path.expanduser(str(args.rapp_root))))
|
|
||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps(report, indent=2, sort_keys=True))
|
print(json.dumps(report, indent=2, sort_keys=True))
|
||||||
return 0
|
return 0 if report["ok"] else 1
|
||||||
|
|
||||||
print("lane -> workload join\n")
|
print("explicit lane -> workload resolution\n")
|
||||||
print(f" declared workloads (rapp-*): {len(report['declared_workloads'])}")
|
print(f" resolved: {len(report['resolved'])}")
|
||||||
print(f" lanes matched to a workload: {len(report['matched'])}")
|
print(f" unknown: {len(report['unknown'])}")
|
||||||
print(f" lanes unmatched: {len(report['unmatched'])}")
|
print(f" not-applicable: {len(report['not_applicable'])}\n")
|
||||||
print(f" lanes with no usable path: {len(report['no_path'])}\n")
|
for row in report["resolved"]:
|
||||||
|
ref = row["workload_ref"]
|
||||||
if report["matched"]:
|
prefix = f"{ref.get('rapp_id')}/" if ref.get("rapp_id") else ""
|
||||||
print("MATCHED")
|
print(f"RESOLVED {row['lane']:34} -> {prefix}{ref.get('name')}")
|
||||||
for m in report["matched"]:
|
for row in report["unknown"]:
|
||||||
flag = " <-- classification unmapped by dataclass_floor" if m[
|
print(f"UNKNOWN {row['lane']:34} {row['reason']}")
|
||||||
"unmapped_classification"
|
for row in report["not_applicable"]:
|
||||||
] else ""
|
print(f"NOT-APPLICABLE {row['lane']:34} {row['reason']}")
|
||||||
print(
|
return 0 if report["ok"] else 1
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,6 @@ def policy_gate_status() -> str:
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
except ConfigError:
|
except ConfigError:
|
||||||
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
||||||
if cfg.policy.enabled:
|
if cfg.policy.flex_auth_url:
|
||||||
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
|
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
|
||||||
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
|
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
_AUDIT_FILENAME = "audit.jsonl"
|
_AUDIT_FILENAME = "audit.jsonl"
|
||||||
_MAX_BYTES = 5 * 1024 * 1024
|
_MAX_BYTES = 5 * 1024 * 1024
|
||||||
|
|
@ -215,7 +215,6 @@ def collect_activity(
|
||||||
since = datetime.now(timezone.utc) - timedelta(days=days)
|
since = datetime.now(timezone.utc) - timedelta(days=days)
|
||||||
events = read_events(state_dir, since=since, kinds=kinds)
|
events = read_events(state_dir, since=since, kinds=kinds)
|
||||||
if include_legacy:
|
if include_legacy:
|
||||||
legacy_kinds = kinds or {"sign", "access", "worker"}
|
|
||||||
if not kinds or "sign" in kinds:
|
if not kinds or "sign" in kinds:
|
||||||
events.extend(_legacy_sign_events(state_dir, since))
|
events.extend(_legacy_sign_events(state_dir, since))
|
||||||
if not kinds or "access" in kinds:
|
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,
|
"summary": summary,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return notes
|
return notes
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,12 @@ def _append_signature_log(
|
||||||
}
|
}
|
||||||
if spec.policy_decision_id:
|
if spec.policy_decision_id:
|
||||||
entry["policy_decision_id"] = 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)
|
state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
with (state_dir / "signatures.log").open("a") as f:
|
with (state_dir / "signatures.log").open("a") as f:
|
||||||
f.write(json.dumps(entry) + "\n")
|
f.write(json.dumps(entry) + "\n")
|
||||||
|
|
@ -76,6 +82,9 @@ def _append_signature_log(
|
||||||
actor_type=spec.actor_type.value,
|
actor_type=spec.actor_type.value,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
ttl_hours=spec.ttl_hours,
|
ttl_hours=spec.ttl_hours,
|
||||||
|
policy_zone=spec.policy_zone,
|
||||||
|
policy_failure_mode=spec.policy_failure_mode,
|
||||||
|
policy_outcome=spec.policy_outcome,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # audit must not block signing
|
pass # audit must not block signing
|
||||||
|
|
|
||||||
|
|
@ -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
|
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
|
||||||
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
|
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
|
||||||
that header, the pin logs `caller authentication warning` and can only run in
|
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.
|
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
|
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from warden.config import ConfigError, WardenConfig, load_config
|
||||||
from warden.policy import check_sign_policy
|
from warden.policy import check_sign_policy
|
||||||
from warden.inventory import ActorEntry, InventoryError, PrincipalsInventory, load_inventory, save_inventory
|
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.models import ActorType, CertSpec, DEFAULT_TTL_HOURS, validate_actor_name
|
||||||
|
from warden.routing.catalog import blocker_stale_days
|
||||||
from warden.scorecard import run_scorecard
|
from warden.scorecard import run_scorecard
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
|
|
@ -120,7 +121,7 @@ def _get_ca(cfg: WardenConfig):
|
||||||
|
|
||||||
|
|
||||||
def _apply_policy_gate(cfg: WardenConfig, spec: CertSpec) -> None:
|
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)
|
decision_id = check_sign_policy(cfg.policy, spec)
|
||||||
if decision_id:
|
if decision_id:
|
||||||
spec.policy_decision_id = 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.
|
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
|
||||||
"risk": entry.risk,
|
"risk": entry.risk,
|
||||||
"high_risk": entry.is_high_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`
|
# 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`.
|
# lets a caller gate before asking for the full block via `warden rotate-guide`.
|
||||||
"has_rotation": entry.has_rotation,
|
"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:
|
def _gap_is_stale(entry, delegation, reviewed: str, stale_days) -> bool:
|
||||||
"""An interim lane needs attention on either of two independent grounds.
|
"""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]"
|
"token stays in the caller's own store.[/dim]"
|
||||||
)
|
)
|
||||||
else:
|
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.
|
# G1 — caller identity. ops-warden adds no token of its own.
|
||||||
if not caller_auth_present():
|
if not caller_auth_present():
|
||||||
err.print(
|
err.print(
|
||||||
|
|
@ -1263,24 +1270,20 @@ def _access_proxy(
|
||||||
)
|
)
|
||||||
raise typer.Exit(3)
|
raise typer.Exit(3)
|
||||||
|
|
||||||
# G3 — policy gate before fetch.
|
# G3 — the zone-aware policy gate always runs before fetch.
|
||||||
if cfg.policy.enabled:
|
try:
|
||||||
try:
|
decision_id = check_fetch_policy(
|
||||||
decision_id = check_fetch_policy(
|
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
|
||||||
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."
|
|
||||||
)
|
)
|
||||||
|
except CAError as e:
|
||||||
|
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
|
||||||
raise typer.Exit(4)
|
raise typer.Exit(4)
|
||||||
|
if decision_id:
|
||||||
|
err.print(f"[green]flex-auth decision[/green] ({decision_id}).")
|
||||||
else:
|
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
|
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
|
||||||
# share the resolved fetch command.
|
# share the resolved fetch command.
|
||||||
|
|
@ -1434,7 +1437,10 @@ def access(
|
||||||
] = False,
|
] = False,
|
||||||
no_policy: Annotated[
|
no_policy: Annotated[
|
||||||
bool,
|
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,
|
] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Operator front door: how to obtain any credential, gated and audited.
|
"""Operator front door: how to obtain any credential, gated and audited.
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,19 @@ class CallerAuthConfig:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PolicyConfig:
|
class PolicyConfig:
|
||||||
enabled: bool = False
|
flex_auth_url: Optional[str] = None
|
||||||
flex_auth_url: str = "http://127.0.0.1:8080"
|
zone_registry_path: Optional[Path] = None
|
||||||
fail_closed: bool = True
|
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"
|
tenant: str = "tenant:platform"
|
||||||
subject_env: str = "WARDEN_POLICY_SUBJECT"
|
subject_env: str = "WARDEN_POLICY_SUBJECT"
|
||||||
system: str = "ops-warden"
|
system: str = "ops-warden"
|
||||||
|
|
@ -148,6 +158,13 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
policy_raw = raw.get("policy") or {}
|
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_raw = policy_raw.get("caller_auth") or {}
|
||||||
caller_command = caller_raw.get("command")
|
caller_command = caller_raw.get("command")
|
||||||
if isinstance(caller_command, str):
|
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")
|
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
|
||||||
if caller_cfg.mode == "command" and not caller_cfg.command:
|
if caller_cfg.mode == "command" and not caller_cfg.command:
|
||||||
raise ConfigError("policy.caller_auth.command is required for mode: 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(
|
policy_cfg = PolicyConfig(
|
||||||
enabled=bool(policy_raw.get("enabled", False)),
|
flex_auth_url=flex_auth_url,
|
||||||
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
|
zone_registry_path=(
|
||||||
fail_closed=bool(policy_raw.get("fail_closed", True)),
|
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")),
|
tenant=str(policy_raw.get("tenant", "tenant:platform")),
|
||||||
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
|
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
|
||||||
system=str(policy_raw.get("system", "ops-warden")),
|
system=str(policy_raw.get("system", "ops-warden")),
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ class CertSpec:
|
||||||
principals: List[str]
|
principals: List[str]
|
||||||
identity: str = "" # defaults to actor_name if empty
|
identity: str = "" # defaults to actor_name if empty
|
||||||
policy_decision_id: Optional[str] = None
|
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:
|
def __post_init__(self) -> None:
|
||||||
if not self.identity:
|
if not self.identity:
|
||||||
|
|
|
||||||
|
|
@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -20,21 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
|
||||||
return f"sha256:{digest}"
|
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).
|
"""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
|
rather than silently falling back to an unauthenticated request — an
|
||||||
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return caller_auth_headers(cfg.caller_auth)
|
return caller_auth_headers(cfg.caller_auth)
|
||||||
except CallerIdentityError as e:
|
except CallerIdentityError as e:
|
||||||
if cfg.fail_closed:
|
if fail_closed:
|
||||||
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
||||||
return {}
|
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:
|
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
||||||
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
|
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:
|
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
||||||
"""Call flex-auth /v1/check before signing.
|
"""Call flex-auth /v1/check before signing.
|
||||||
|
|
||||||
Returns decision id when policy is enabled and effect is allow.
|
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
|
||||||
Returns None when policy is disabled.
|
Evaluator failures use the PEP-owned failure mode for the target workload's
|
||||||
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
|
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
|
return None
|
||||||
|
|
||||||
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
|
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"
|
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||||
headers = _caller_headers(cfg)
|
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||||
try:
|
try:
|
||||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
if cfg.fail_closed:
|
_evaluator_failure(
|
||||||
raise CAError(
|
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||||
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
|
f"for security zone {zone!r}",
|
||||||
) from e
|
fail_closed=fail_closed,
|
||||||
|
cause=e,
|
||||||
|
spec=spec,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
if cfg.fail_closed:
|
_evaluator_failure(
|
||||||
raise CAError(
|
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
|
||||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
|
fail_closed=fail_closed,
|
||||||
f"(fail_closed=true): {e}"
|
cause=e,
|
||||||
) from e
|
spec=spec,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decision = response.json()
|
decision = response.json()
|
||||||
except ValueError as e:
|
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()
|
effect = str(decision.get("effect", "")).lower()
|
||||||
decision_id = decision.get("id") or decision.get("request_id")
|
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"
|
reason = decision.get("reason") or "no reason provided"
|
||||||
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
||||||
|
|
||||||
if not decision_id:
|
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)
|
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).
|
"""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 —
|
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,
|
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||||
None when policy is disabled, and raises CAError on deny (or on an unreachable
|
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||||
flex-auth when fail_closed). No secret value is ever part of this 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
|
return None
|
||||||
|
|
||||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||||
request = {
|
request = {
|
||||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
"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"
|
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||||
headers = _caller_headers(cfg)
|
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||||
try:
|
try:
|
||||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
if cfg.fail_closed:
|
_evaluator_failure(
|
||||||
raise CAError(
|
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||||
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
|
fail_closed=fail_closed,
|
||||||
) from e
|
cause=e,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
if cfg.fail_closed:
|
_evaluator_failure(
|
||||||
raise CAError(
|
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
|
||||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
|
fail_closed=fail_closed,
|
||||||
) from e
|
cause=e,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decision = response.json()
|
decision = response.json()
|
||||||
except ValueError as e:
|
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()
|
effect = str(decision.get("effect", "")).lower()
|
||||||
decision_id = decision.get("id") or decision.get("request_id")
|
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"
|
reason = decision.get("reason") or "no reason provided"
|
||||||
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
||||||
if not decision_id:
|
if not decision_id:
|
||||||
raise CAError("flex-auth allow decision missing id")
|
_evaluator_failure(
|
||||||
return str(decision_id)
|
f"flex-auth {effect} decision missing id for security zone 'unknown'",
|
||||||
|
fail_closed=fail_closed,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return str(decision_id)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from warden.routing.catalog import (
|
||||||
find_catalog_path,
|
find_catalog_path,
|
||||||
load_catalog,
|
load_catalog,
|
||||||
)
|
)
|
||||||
from warden.routing.models import Delegation, RouteEntry
|
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Catalog",
|
"Catalog",
|
||||||
|
|
@ -20,6 +20,7 @@ __all__ = [
|
||||||
"CatalogFreshness",
|
"CatalogFreshness",
|
||||||
"Delegation",
|
"Delegation",
|
||||||
"RouteEntry",
|
"RouteEntry",
|
||||||
|
"WorkloadReference",
|
||||||
"find_catalog_path",
|
"find_catalog_path",
|
||||||
"load_catalog",
|
"load_catalog",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ import yaml
|
||||||
from warden.routing.models import (
|
from warden.routing.models import (
|
||||||
VALID_DELEGATION_MODES,
|
VALID_DELEGATION_MODES,
|
||||||
VALID_RISK,
|
VALID_RISK,
|
||||||
|
VALID_WORKLOAD_APPLICABILITY,
|
||||||
Delegation,
|
Delegation,
|
||||||
RotationGuide,
|
RotationGuide,
|
||||||
RouteEntry,
|
RouteEntry,
|
||||||
|
WorkloadReference,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
||||||
|
|
@ -64,6 +66,7 @@ _REQUIRED_FIELDS = (
|
||||||
"canon_ref",
|
"canon_ref",
|
||||||
"reviewed",
|
"reviewed",
|
||||||
"status",
|
"status",
|
||||||
|
"workload_ref",
|
||||||
)
|
)
|
||||||
_VALID_STATUS = ("active", "draft")
|
_VALID_STATUS = ("active", "draft")
|
||||||
_VALID_LANES = ("secret", "login")
|
_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:
|
def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
raise CatalogError(f"entry #{index} is not a mapping")
|
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})"
|
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
|
||||||
)
|
)
|
||||||
|
|
||||||
risk = str(raw.get("risk", "standard")).strip() or "standard"
|
risk_value = raw.get("risk")
|
||||||
if risk not in VALID_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(
|
raise CatalogError(
|
||||||
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
|
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(
|
return RouteEntry(
|
||||||
id=entry_id,
|
id=entry_id,
|
||||||
title=str(raw["title"]),
|
title=str(raw["title"]),
|
||||||
|
|
@ -593,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||||
canon_ref=str(raw["canon_ref"]),
|
canon_ref=str(raw["canon_ref"]),
|
||||||
reviewed=str(raw["reviewed"]),
|
reviewed=str(raw["reviewed"]),
|
||||||
status=status,
|
status=status,
|
||||||
|
workload_ref=workload_ref,
|
||||||
steps=[str(s) for s in steps],
|
steps=[str(s) for s in steps],
|
||||||
cert_command=str(cert_command) if cert_command else None,
|
cert_command=str(cert_command) if cert_command else None,
|
||||||
auth_method=handoff["auth_method"],
|
auth_method=handoff["auth_method"],
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ VALID_RISK = ("standard", "high")
|
||||||
# interim — ops-warden covers a gap; intended_owner + blocked_on required
|
# 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)
|
# permanent — ops-warden is the designed owner of this front door (SSH only today)
|
||||||
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
|
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
|
||||||
|
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
|
||||||
|
|
||||||
IMPLICIT_DELEGATION_BLOCKED_ON = (
|
IMPLICIT_DELEGATION_BLOCKED_ON = (
|
||||||
"unclassified — no delegation block; treat as a question, not a settlement"
|
"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
|
@dataclass
|
||||||
class RouteEntry:
|
class RouteEntry:
|
||||||
id: str
|
id: str
|
||||||
|
|
@ -112,6 +153,8 @@ class RouteEntry:
|
||||||
canon_ref: str
|
canon_ref: str
|
||||||
reviewed: str
|
reviewed: str
|
||||||
status: str # "active" | "draft"
|
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.
|
# SSH lane only — None/empty for routed (non-executed) needs.
|
||||||
steps: List[str] = field(default_factory=list)
|
steps: List[str] = field(default_factory=list)
|
||||||
cert_command: Optional[str] = None
|
cert_command: Optional[str] = None
|
||||||
|
|
@ -169,6 +212,34 @@ class RouteEntry:
|
||||||
"""
|
"""
|
||||||
return self.risk not in LOW_RISK_GRADES
|
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
|
@property
|
||||||
def has_rotation(self) -> bool:
|
def has_rotation(self) -> bool:
|
||||||
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
||||||
|
|
|
||||||
37
tenancy.yaml
37
tenancy.yaml
|
|
@ -8,6 +8,17 @@ framework: netkingdom-tenancy-posture
|
||||||
service: ops-warden
|
service: ops-warden
|
||||||
role: ssh-certificate-authority
|
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:
|
tenancy:
|
||||||
current:
|
current:
|
||||||
I: 1
|
I: 1
|
||||||
|
|
@ -108,13 +119,25 @@ tenancy:
|
||||||
is single-node with a shared control plane, and under Decision 4.6.1 that
|
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).
|
caps V for everything bound to it (see NK-WP-0027).
|
||||||
|
|
||||||
# zones: (reserved, tenancy-posture_v0.1 Decision 5.6)
|
zones:
|
||||||
#
|
standard: security-zones_v0.1
|
||||||
# Deliberately absent. The security-zone standard is being drafted by
|
membership: z1-operational
|
||||||
# zone-engine (ZONE-WP-0001) and no zone vocabulary exists yet, so any value
|
responsible_party: team:platform-security
|
||||||
# here would be fabricated — the failure §6 exists to prevent. ops-warden is
|
justification: >-
|
||||||
# the first intended consumer and will populate this key once the sibling
|
The attended platform signing service has a bounded operational scope and
|
||||||
# standard defines it (WARDEN-WP-0032-T03).
|
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:
|
evidence:
|
||||||
A: >-
|
A: >-
|
||||||
|
|
|
||||||
|
|
@ -84,13 +84,13 @@ def test_default_vault_token_env(tmp_path):
|
||||||
assert cfg.vault.token_env == "VAULT_TOKEN"
|
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"
|
cfg_path = tmp_path / "warden.yaml"
|
||||||
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
|
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
|
||||||
cfg = load_config(cfg_path)
|
cfg = load_config(cfg_path)
|
||||||
assert cfg.policy.enabled is False
|
assert cfg.policy.flex_auth_url is None
|
||||||
assert cfg.policy.flex_auth_url == "http://127.0.0.1:8080"
|
assert cfg.policy.failure_modes["unknown"] == "fail_open"
|
||||||
assert cfg.policy.fail_closed is True
|
assert cfg.policy.failure_modes["z3-critical"] == "fail_closed"
|
||||||
|
|
||||||
|
|
||||||
def test_policy_block_parsed(tmp_path):
|
def test_policy_block_parsed(tmp_path):
|
||||||
|
|
@ -99,18 +99,30 @@ def test_policy_block_parsed(tmp_path):
|
||||||
"backend": "local",
|
"backend": "local",
|
||||||
"ca_key": str(tmp_path / "ca"),
|
"ca_key": str(tmp_path / "ca"),
|
||||||
"policy": {
|
"policy": {
|
||||||
"enabled": True,
|
|
||||||
"flex_auth_url": "http://flex-auth:8080",
|
"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",
|
"tenant": "tenant:coulomb",
|
||||||
"subject_env": "MY_SUBJECT",
|
"subject_env": "MY_SUBJECT",
|
||||||
"system": "warden-test",
|
"system": "warden-test",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cfg = load_config(cfg_path)
|
cfg = load_config(cfg_path)
|
||||||
assert cfg.policy.enabled is True
|
|
||||||
assert cfg.policy.flex_auth_url == "http://flex-auth:8080"
|
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.tenant == "tenant:coulomb"
|
||||||
assert cfg.policy.subject_env == "MY_SUBJECT"
|
assert cfg.policy.subject_env == "MY_SUBJECT"
|
||||||
assert cfg.policy.system == "warden-test"
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
|
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import threading
|
import threading
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from http.server import ThreadingHTTPServer
|
from http.server import ThreadingHTTPServer
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,53 @@ def test_build_registry_from_inventory_seed(tmp_path):
|
||||||
)
|
)
|
||||||
assert bridge["attributes"]["actor_type"] == "agt"
|
assert bridge["attributes"]["actor_type"] == "agt"
|
||||||
assert bridge["attributes"]["max_ttl_hours"] == 24
|
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"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import json
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from warden.cli import app
|
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
|
from warden.worker import RuleBrain, _plan_with_memory, build_plans
|
||||||
|
|
||||||
runner = CliRunner()
|
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)
|
activation = ensure_memory_context(need="ssh tunnel", implicit=True)
|
||||||
assert activation is not None
|
assert activation is not None
|
||||||
assert activation.get("implicit") is True
|
assert activation.get("implicit") is True
|
||||||
assert status()["episode_count"] >= 1
|
assert status()["episode_count"] >= 1
|
||||||
|
|
|
||||||
|
|
@ -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):
|
def test_pubkey_fingerprint(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||||
|
|
@ -35,26 +46,32 @@ def test_pubkey_fingerprint(tmp_path):
|
||||||
assert len(fp) == 7 + 64
|
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 = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||||
cfg = PolicyConfig(enabled=False)
|
cfg = PolicyConfig()
|
||||||
assert check_sign_policy(cfg, _spec(pubkey)) is None
|
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):
|
def test_allow_returns_decision_id(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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 = MagicMock()
|
||||||
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
|
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
spec = _spec(pubkey)
|
||||||
with patch("warden.policy.httpx.post", return_value=mock_response) as post:
|
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 result == "dec-123"
|
||||||
|
assert spec.policy_outcome == "allow"
|
||||||
post.assert_called_once()
|
post.assert_called_once()
|
||||||
call_kwargs = post.call_args
|
call_kwargs = post.call_args
|
||||||
assert call_kwargs[0][0] == "http://flex-auth.test/v1/check"
|
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):
|
def test_deny_raises_ca_error(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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 = MagicMock()
|
||||||
mock_response.json.return_value = {
|
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):
|
def test_unreachable_fail_closed_raises(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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(
|
with patch(
|
||||||
"warden.policy.httpx.post",
|
"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):
|
def test_unreachable_fail_open_returns_none(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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(
|
with patch(
|
||||||
"warden.policy.httpx.post",
|
"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):
|
def test_http_error_fail_closed_raises(tmp_path):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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 = MagicMock()
|
||||||
mock_response.status_code = 403
|
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):
|
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")
|
spec = _spec(tmp_path / "missing.pub")
|
||||||
with pytest.raises(CAError, match="Public key not found"):
|
with pytest.raises(CAError, match="Public key not found"):
|
||||||
check_sign_policy(cfg, spec)
|
check_sign_policy(cfg, spec)
|
||||||
|
|
@ -132,7 +155,10 @@ def test_missing_pubkey_raises(tmp_path):
|
||||||
def test_subject_from_env(tmp_path, monkeypatch):
|
def test_subject_from_env(tmp_path, monkeypatch):
|
||||||
pubkey = tmp_path / "key.pub"
|
pubkey = tmp_path / "key.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
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")
|
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "iam:bernd")
|
||||||
|
|
||||||
mock_response = MagicMock()
|
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")
|
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||||
|
|
||||||
cfg = PolicyConfig(
|
cfg = PolicyConfig(
|
||||||
enabled=True,
|
flex_auth_url="http://flex-auth.test",
|
||||||
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
|
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
|
||||||
)
|
)
|
||||||
spec = CertSpec(
|
spec = CertSpec(
|
||||||
|
|
@ -250,8 +276,8 @@ def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
|
||||||
pubkey = tmp_path / "id.pub"
|
pubkey = tmp_path / "id.pub"
|
||||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||||
cfg = PolicyConfig(
|
cfg = PolicyConfig(
|
||||||
enabled=True,
|
flex_auth_url="http://flex-auth.test",
|
||||||
fail_closed=True,
|
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||||
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
|
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
|
||||||
)
|
)
|
||||||
spec = CertSpec(
|
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"):
|
with pytest.raises(CAError, match="caller identity unavailable"):
|
||||||
policy_mod.check_sign_policy(cfg, spec)
|
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"
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,6 @@ def _warden_yaml(tmp_path: Path) -> Path:
|
||||||
(tmp_path / "ca").write_text("")
|
(tmp_path / "ca").write_text("")
|
||||||
cfg.write_text(
|
cfg.write_text(
|
||||||
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
|
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
|
||||||
"policy:\n enabled: false\n"
|
|
||||||
)
|
)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
@ -203,10 +202,11 @@ def _proxy_env(monkeypatch, tmp_path):
|
||||||
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(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)
|
_proxy_env(monkeypatch, tmp_path)
|
||||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
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(
|
monkeypatch.setattr(
|
||||||
"warden.proxy.subprocess.run",
|
"warden.proxy.subprocess.run",
|
||||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
|
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",
|
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||||
"--path", "platform/x/y/z", "--fetch"],
|
"--path", "platform/x/y/z", "--fetch"],
|
||||||
)
|
)
|
||||||
assert r.exit_code == 4
|
assert r.exit_code == 6
|
||||||
assert "not enforced" in r.stdout or "not enforced" in str(r.output)
|
assert "unknown-zone fail_open" in r.output
|
||||||
|
|
||||||
|
|
||||||
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
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(
|
r = runner.invoke(
|
||||||
app,
|
app,
|
||||||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
["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
|
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 --------------------------------------------------------
|
# --- T4: login lane --------------------------------------------------------
|
||||||
|
|
||||||
def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path):
|
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",
|
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",
|
warden_executes=False, wiki_ref="w", canon_ref="c", reviewed="2026-06-27",
|
||||||
status="active", lane="bogus",
|
status="active", lane="bogus",
|
||||||
|
workload_ref={"applicability": "not-applicable", "reason": "fixture"},
|
||||||
)
|
)
|
||||||
p = tmp_path / "c.yaml"
|
p = tmp_path / "c.yaml"
|
||||||
p.write_text(yaml.dump({"version": 1, "entries": [entry]}))
|
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")),
|
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.
|
# 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 r.exit_code == 6
|
||||||
assert "sanctioned transport" in r.output.lower() or "refusing" in r.output.lower()
|
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())
|
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||||
r = runner.invoke(
|
r = runner.invoke(
|
||||||
app,
|
app,
|
||||||
["access", "whynot-design-npm-publish", "--fingerprint", "--no-policy"],
|
["access", "whynot-design-npm-publish", "--fingerprint"],
|
||||||
)
|
)
|
||||||
assert r.exit_code == 0
|
assert r.exit_code == 0
|
||||||
assert "top-secret-token-value" not in r.output # value never shown
|
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,
|
app,
|
||||||
[
|
[
|
||||||
"access", "railiance-backup-offsite-lane",
|
"access", "railiance-backup-offsite-lane",
|
||||||
"--fetch", "--no-policy", "--unsafe-stdout",
|
"--fetch", "--unsafe-stdout",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert r.exit_code == 7, r.output
|
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())
|
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||||
r = runner.invoke(
|
r = runner.invoke(
|
||||||
app,
|
app,
|
||||||
["access", "railiance-backup-offsite-lane", "--fingerprint", "--no-policy"],
|
["access", "railiance-backup-offsite-lane", "--fingerprint"],
|
||||||
)
|
)
|
||||||
assert r.exit_code == 0, r.output
|
assert r.exit_code == 0, r.output
|
||||||
assert "should-not-appear" not in r.output
|
assert "should-not-appear" not in r.output
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ No test here requires a live subsystem — routing is a read-only pointer layer.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -11,10 +12,9 @@ import yaml
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from warden.cli import app
|
from warden.cli import app
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from warden.routing import CatalogError, load_catalog
|
from warden.routing import CatalogError, load_catalog
|
||||||
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
|
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
|
||||||
|
from warden.scorecard import check_catalog_rotation_coverage
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
@ -40,6 +40,10 @@ SSH_ENTRY = {
|
||||||
"canon_ref": "net-kingdom/docs/x.md",
|
"canon_ref": "net-kingdom/docs/x.md",
|
||||||
"reviewed": "2026-06-18",
|
"reviewed": "2026-06-18",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
|
"workload_ref": {
|
||||||
|
"applicability": "not-applicable",
|
||||||
|
"reason": "generic certificate action",
|
||||||
|
},
|
||||||
"cert_command": "warden sign <actor> --pubkey <path>",
|
"cert_command": "warden sign <actor> --pubkey <path>",
|
||||||
"steps": ["confirm inventory", "sign"],
|
"steps": ["confirm inventory", "sign"],
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +59,10 @@ ROUTED_ENTRY = {
|
||||||
"canon_ref": "net-kingdom/docs/x.md",
|
"canon_ref": "net-kingdom/docs/x.md",
|
||||||
"reviewed": "2026-06-18",
|
"reviewed": "2026-06-18",
|
||||||
"status": "active",
|
"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"]
|
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():
|
def test_ops_warden_warden_sign_lane_has_native_exec():
|
||||||
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
|
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
|
||||||
catalog = load_catalog(_repo_catalog())
|
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)
|
# 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():
|
def test_every_active_vending_lane_has_rotation_guidance():
|
||||||
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
|
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
|
||||||
catalog = load_catalog(_repo_catalog())
|
catalog = load_catalog(_repo_catalog())
|
||||||
|
|
@ -774,6 +818,32 @@ def test_unrecognised_grade_is_treated_as_high():
|
||||||
assert entry.is_graded is False
|
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():
|
def test_low_risk_vocabulary_is_explicit():
|
||||||
for grade in ("standard", "low", "accepted"):
|
for grade in ("standard", "low", "accepted"):
|
||||||
entry = _bare_entry(risk=grade)
|
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
|
assert result.exit_code == 3
|
||||||
rows = json.loads(result.stdout)
|
rows = json.loads(result.stdout)
|
||||||
assert any(r["stale"] for r in rows)
|
assert any(r["stale"] for r in rows)
|
||||||
# A lane can be stale on age or on never having been verified; both must be
|
# A freshly reviewed lane can still be stale because it was never verified;
|
||||||
# expressible, or asked-and-waiting silently passes the gate.
|
# asked-and-waiting must not silently pass the gate as the calendar moves.
|
||||||
assert any(r["stale"] and r["days_since_review"] == 0 for r in rows)
|
assert any(
|
||||||
|
r["stale"]
|
||||||
|
and r["days_since_review"] <= 1
|
||||||
|
and r["verified"] == "asked-and-waiting"
|
||||||
|
for r in rows
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
|
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from warden.cli import app
|
from warden.cli import app
|
||||||
|
|
|
||||||
52
tests/test_workload_join.py
Normal file
52
tests/test_workload_join.py
Normal 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"]
|
||||||
|
|
@ -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
|
flex-auth warn-only -> ops-warden pre-sign gate presents its SA token
|
||||||
-> logs clean of unauthenticated callers
|
-> logs clean of unauthenticated callers
|
||||||
-> flex-auth fail-closed
|
-> 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
|
An enforce stance must not be assigned while `/v1/check` still answers
|
||||||
unauthenticated callers.
|
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
|
**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
|
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/AccessRouting.md` — issue-vs-route role and boundary
|
||||||
- `wiki/CredentialRouting.md`
|
- `wiki/CredentialRouting.md`
|
||||||
- `wiki/PolicyGatedSigning.md` (future flex-auth hook)
|
- `wiki/PolicyGatedSigning.md` (future flex-auth hook)
|
||||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| **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) |
|
| **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,
|
The catalog side enforces a fourth, upstream guard: **handoff fields are templates,
|
||||||
never values.** `_assert_no_secret_material` rejects any known token prefix or
|
never values.** `_assert_no_secret_material` rejects any known token prefix or
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,19 @@ ca_key: ~/.ssh/ops-ca-user
|
||||||
inventory_path: ~/.config/warden/inventory.yaml
|
inventory_path: ~/.config/warden/inventory.yaml
|
||||||
state_dir: ~/.local/state/warden
|
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:
|
policy:
|
||||||
enabled: false
|
|
||||||
flex_auth_url: http://127.0.0.1:8080
|
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
|
### Bootstrapping the local CA key
|
||||||
|
|
@ -85,11 +93,10 @@ vault:
|
||||||
inventory_path: ~/.config/warden/inventory.yaml
|
inventory_path: ~/.config/warden/inventory.yaml
|
||||||
state_dir: ~/.local/state/warden
|
state_dir: ~/.local/state/warden
|
||||||
|
|
||||||
# Enable after flex-auth ssh-certificate policies are deployed:
|
# Configure after flex-auth ssh-certificate policies are deployed:
|
||||||
# policy:
|
# policy:
|
||||||
# enabled: true
|
# flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||||
# flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
|
# zone_registry_path: /etc/warden/production_registry_snapshot.json
|
||||||
# fail_closed: true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example — in-cluster caller (pod or trusted host)
|
### 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
|
`warden sign` and `warden issue` evaluate flex-auth whenever a URL is configured.
|
||||||
`POST /v1/check` before signing. Deny or unreachable (with `fail_closed: true`)
|
The target resource's compiled `security_zone` selects the local PEP failure
|
||||||
blocks issuance. Allowed decisions store `policy_decision_id` in `signatures.log`.
|
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
|
```yaml
|
||||||
policy:
|
policy:
|
||||||
enabled: false # default — no behavior change
|
|
||||||
flex_auth_url: http://127.0.0.1:8080
|
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
|
tenant: tenant:platform
|
||||||
subject_env: WARDEN_POLICY_SUBJECT
|
subject_env: WARDEN_POLICY_SUBJECT
|
||||||
system: ops-warden
|
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`.
|
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 |
|
| `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`) |
|
| `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,
|
`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>`.
|
and passes it alongside the private key via `ssh -i <key> -i <cert>`.
|
||||||
See `wiki/CertCommandInterface.md` for the full contract and
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,347 +1,196 @@
|
||||||
# Policy-Gated SSH Signing
|
# Zone-aware policy-gated signing
|
||||||
|
|
||||||
Date: 2026-06-23
|
Ops-warden asks flex-auth for a decision before SSH certificate issuance. The
|
||||||
Status: **implemented (opt-in)** — WARDEN-WP-0007; policy package confirmed FLEX-WP-0006
|
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
|
Authority stays split:
|
||||||
only. When `policy.enabled: true` in `warden.yaml`, ops-warden calls flex-auth
|
|
||||||
before signing and records the decision id in `signatures.log`.
|
|
||||||
|
|
||||||
---
|
- 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
|
```text
|
||||||
warden sign <actor> --pubkey <path>
|
warden sign <actor>
|
||||||
|
|
-> inventory, principal, actor-type, and TTL checks
|
||||||
v
|
-> resource id ssh-cert:actor/<actor>
|
||||||
Load actor from inventory (type, principals, ttl)
|
-> read compiled security_zone for that resource
|
||||||
|
|
-> POST flex-auth /v1/check with authenticated caller identity
|
||||||
v
|
allow -> sign; record decision and zone evidence
|
||||||
policy.enabled?
|
audit_only -> sign; record advisory decision and zone evidence
|
||||||
no -> skip
|
deny -> refuse before the CA backend
|
||||||
yes -> flex-auth POST /v1/check
|
unavailable/invalid
|
||||||
|
|
-> apply that zone's PEP failure mode
|
||||||
+-- DENY / unreachable (fail_closed) -> CAError
|
-> record fail_open when issuance proceeds
|
||||||
|
|
|
||||||
v ALLOW
|
|
||||||
CABackend.sign() (local or OpenBao SSH engine)
|
|
||||||
|
|
|
||||||
v
|
|
||||||
Append signatures.log (+ policy_decision_id when set)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
`scripts/build_flex_auth_registry.py` compiles inventory actor resources. Each
|
||||||
|
actor carries an explicit `zone_subject`:
|
||||||
| 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
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# warden.yaml — policy gate (opt-in, default off)
|
actors:
|
||||||
policy:
|
agt-state-hub-bridge:
|
||||||
enabled: false
|
type: agt
|
||||||
flex_auth_url: http://127.0.0.1:8080
|
principals: [agt-task-bridge]
|
||||||
fail_closed: true
|
ttl_hours: 24
|
||||||
tenant: tenant:platform
|
zone_subject:
|
||||||
subject_env: WARDEN_POLICY_SUBJECT
|
applicability: applicable
|
||||||
system: ops-warden
|
workload_id: ops-bridge-tunnel
|
||||||
caller_auth: # how ops-warden identifies itself — see "Caller identity"
|
|
||||||
mode: none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Key | Default | Description |
|
The compiler consumes zone-engine's resolved view when available and emits
|
||||||
| --- | --- | --- |
|
resource attributes:
|
||||||
| `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)) |
|
|
||||||
|
|
||||||
Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available.
|
```json
|
||||||
If unset, the actor name is used as subject id.
|
{
|
||||||
|
"workload_id": "ops-bridge-tunnel",
|
||||||
---
|
"security_zone": "z2-continuity",
|
||||||
|
"security_zone_admission": "satisfied",
|
||||||
## Versioning
|
"security_zone_revision": "sha256:..."
|
||||||
|
}
|
||||||
| 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).
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```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
|
-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
|
Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not
|
||||||
flex-auth runtime together with `~/flex-auth/examples/ops-warden/policy_package.md`.
|
to a guessed zone.
|
||||||
|
|
||||||
Smoke (non-secret):
|
## PEP failure modes
|
||||||
|
|
||||||
```bash
|
The initial build profile accepted by ops-warden is:
|
||||||
./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
|
|
||||||
```
|
|
||||||
|
|
||||||
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)
|
Configuration:
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
policy:
|
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:
|
caller_auth:
|
||||||
mode: none # none | file | env | command
|
mode: command
|
||||||
token_path: /var/run/secrets/flex-auth/token # mode: file
|
command:
|
||||||
token_env: WARDEN_POLICY_CALLER_TOKEN # mode: env
|
- kubectl
|
||||||
command: kubectl create token ops-warden -n ops-warden --audience flex-auth --duration 10m
|
- create
|
||||||
audience: flex-auth
|
- token
|
||||||
|
- ops-warden
|
||||||
|
- -n
|
||||||
|
- ops-warden
|
||||||
|
- --audience
|
||||||
|
- flex-auth
|
||||||
|
- --duration
|
||||||
|
- 10m
|
||||||
```
|
```
|
||||||
|
|
||||||
| Mode | Use it when |
|
`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
|
||||||
| `none` | Default. No header — pre-FLEX-WP-0016 behaviour, only viable while the pin is in `warn` |
|
truth.
|
||||||
| `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 |
|
|
||||||
|
|
||||||
A workstation is not a ServiceAccount, which is why `command` exists. The token is
|
## Caller identity
|
||||||
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.
|
|
||||||
|
|
||||||
### 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
|
```bash
|
||||||
python scripts/check_policy_caller_identity.py # offline
|
python3 scripts/check_policy_caller_identity.py \
|
||||||
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090 # port-forward of the warn pin
|
--url http://127.0.0.1:19090
|
||||||
```
|
```
|
||||||
|
|
||||||
Exit 0 ready / 1 not ready / 2 bad input. It prints the token's length and a
|
Expected evidence is HTTP 200 with a decision id and anonymous HTTP 401 on the
|
||||||
truncated fingerprint, never the value, so its output is safe to paste into a
|
enforcing pin. The script reports only token length and a truncated fingerprint.
|
||||||
handoff message. A live `401` means the token was sent but rejected (audience or
|
|
||||||
binding); `403` means it authenticated but may not represent `system: ops-warden`.
|
|
||||||
|
|
||||||
### Enablement is deferred by decision (ADR-0006)
|
## Audit evidence
|
||||||
|
|
||||||
`policy.enabled` is **false, and that is a decision rather than a blocker.** The
|
Successful signing records:
|
||||||
gate is ready: flex-auth's pin runs `callerAuth.mode: enforce`, the readiness
|
|
||||||
gate exits 0 against it, and an anonymous `/v1/check` returns 401.
|
|
||||||
|
|
||||||
It is not enabled because `policy.enabled` is one boolean over the whole repo,
|
- `policy_decision_id` when flex-auth returned one;
|
||||||
and with `fail_closed: true` it makes flex-auth a hard dependency of every
|
- `policy_zone`;
|
||||||
`warden sign` — including the certs the ops-bridge tunnels depend on, one of
|
- `policy_failure_mode`;
|
||||||
which carries the policy call. Uniform enforcement across an estate under active
|
- `policy_outcome` (`allow`, `audit_only`, or `fail_open`).
|
||||||
refactor hardens the access needed to perform the refactor.
|
|
||||||
|
|
||||||
`ADR-0006` scopes enforcement to security zones; `WARDEN-WP-0032` defines them.
|
A fail-open result must therefore be visible rather than indistinguishable from
|
||||||
Do not set `policy.enabled: true` outside that model — a zone-blind enablement
|
an unevaluated request. Denies do not reach the CA backend and produce no
|
||||||
is out of order under the ADR, not merely inadvisable.
|
certificate.
|
||||||
|
|
||||||
The sequence below is retained as the *mechanics*, for whoever enables the gate
|
## Rollout and rollback
|
||||||
for a zone once zones exist.
|
|
||||||
|
|
||||||
### 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.
|
Rollback is a versioned profile or registry rollback. Do not reintroduce
|
||||||
2. Smoke against a port-forward of the warn pin. The evidence is the **absence**
|
`policy.enabled: false`: that would erase per-zone evidence and recreate the
|
||||||
of `caller authentication warning` in its log, not the `allow` — warn serves
|
global control ADR-0009 supersedes.
|
||||||
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 1–5 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- `wiki/OpsWardenConfig.md` — full config reference
|
- `tenancy.yaml`
|
||||||
- `wiki/CredentialRouting.md`
|
- `docs/evidence/security-zone-admission-2026-08-22.md`
|
||||||
- `~/flex-auth/docs/ops-warden-policy-gate-handoff.md` — flex-auth handoff
|
- `wiki/OpsWardenConfig.md`
|
||||||
- `flex-auth/INTENT.md`
|
- `wiki/WorkloadSecurityPosture.md`
|
||||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
- `history/2026-08-19-flex-auth-caller-identity-evidence.md`
|
||||||
|
|
|
||||||
|
|
@ -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
|
assist can remove manual secret handling friction, while posture/maturity decides
|
||||||
whether the secret may flow at all.
|
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)
|
## Canon layering (where each part lands)
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ When `WARDEN_AGENT_ID` is set and the lane is `risk: high`, `warden access --fet
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export WARDEN_AGENT_ID=grok # or claude, codex
|
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 --out /tmp/nc.token
|
||||||
warden access railiance-backup-offsite-lane --no-policy --wrap
|
warden access railiance-backup-offsite-lane --wrap
|
||||||
warden access railiance-backup-offsite-lane --no-policy --fingerprint
|
warden access railiance-backup-offsite-lane --fingerprint
|
||||||
```
|
```
|
||||||
|
|
||||||
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.
|
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,10 @@ Config (host/port/env names only):
|
||||||
2. Fetch via sanctioned transport (never paste into chat):
|
2. Fetch via sanctioned transport (never paste into chat):
|
||||||
|
|
||||||
```bash
|
```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
|
# primary field is IMAP_USERNAME; for password use --field after template support
|
||||||
# or:
|
# 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
|
env IMAP_USERNAME=… # prefer secrets-engine / dual-field exec when wired
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ Design: `binky-control/integrations/qonto-mcp.md`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# lengths / presence only when debugging
|
# 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";
|
sh -c 'export QONTO_API_KEY="$API_KEY" QONTO_ORGANIZATION_ID="$API_USER";
|
||||||
# then: qonto-mcp-server or curl thirdparty with Authorization login:key
|
# then: qonto-mcp-server or curl thirdparty with Authorization login:key
|
||||||
:'
|
:'
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,11 @@ bridge up state-hub-railiance01
|
||||||
- [ ] Tunnel establishes without static cert file on disk
|
- [ ] Tunnel establishes without static cert file on disk
|
||||||
- [ ] Re-run `bridge up` after cert TTL expires — `cert_command` re-issues automatically
|
- [ ] 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`
|
Confirm `signatures.log` records `policy_zone`, `policy_failure_mode`,
|
||||||
on tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
|
`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/OpsWardenConfig.md` — cert_command example
|
||||||
- `wiki/playbooks/operator-openbao-token-hygiene.md`
|
- `wiki/playbooks/operator-openbao-token-hygiene.md`
|
||||||
- `wiki/AuditTrail.md` — query recent signs via `warden activity`
|
- `wiki/AuditTrail.md` — query recent signs via `warden activity`
|
||||||
- `warden route show ops-bridge-tunnel --json`
|
- `warden route show ops-bridge-tunnel --json`
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preferred: file or exec injection via warden
|
# 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:
|
# 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):
|
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):
|
3. **Proxy via warden access** (catalog active + resolvable):
|
||||||
|
|
||||||
```bash
|
```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.
|
# Primary field is NC_WEBDAV_TOKEN. AGE_PRIVATE_KEY is recovery escrow only.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
3. **Or proxy via warden access** (same `bao kv get`, audited metadata only):
|
||||||
```bash
|
```bash
|
||||||
warden route show reuse-surface-hub-write-token --json
|
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):
|
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`
|
Rotation: `railiance-platform/docs/reuse-surface-runtime-secrets-rotation-runbook.md`
|
||||||
(OpenBao patch → ESO `force-sync` → hub rollout → `make reuse-forgejo-webhook` when
|
(OpenBao patch → ESO `force-sync` → hub rollout → `make reuse-forgejo-webhook` when
|
||||||
the webhook HMAC changes → `make reuse-webhook-smoke`). Lifecycle:
|
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).
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ First worked lane: `tenants/binky/company-email/imap` (CCR-2026-0007).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bao login -method=oidc -path=netkingdom role=<lane>-workload-kv-read
|
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
|
# agents: never raw --fetch stream; WARDEN_AGENT_ID + risk=high → exit 7
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
**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):
|
you, holds nothing). Field-verified flags (whynot-design, @whynot/design@0.4.0):
|
||||||
```bash
|
```bash
|
||||||
# --exec needs the env-var name; --no-policy is required while the gate is advisory
|
# --exec needs the env-var name. The zone-aware policy gate always runs first.
|
||||||
# (policy.enabled=false), else the call exits 4.
|
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
|
||||||
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN \
|
|
||||||
--exec -- npm publish
|
--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
|
On either path the value transits to you (or the child env) and never enters
|
||||||
ops-warden's memory, disk, or audit log.
|
ops-warden's memory, disk, or audit log.
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ type: workplan
|
||||||
title: "Adopt security zones as a consumer — retire the global policy.enabled"
|
title: "Adopt security zones as a consumer — retire the global policy.enabled"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: ops-warden
|
repo: ops-warden
|
||||||
status: proposed
|
status: finished
|
||||||
owner: ops-warden
|
owner: ops-warden
|
||||||
topic_slug: netkingdom
|
topic_slug: netkingdom
|
||||||
planning_priority: P1
|
planning_priority: P1
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- WARDEN-WP-0031
|
- WARDEN-WP-0031
|
||||||
created: "2026-08-19"
|
created: "2026-08-19"
|
||||||
updated: "2026-08-19"
|
updated: "2026-08-22"
|
||||||
state_hub_workstream_id: "f1b6cdcb-5e16-48ab-a72b-339e909e92f0"
|
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
|
```task
|
||||||
id: WARDEN-WP-0032-T02
|
id: WARDEN-WP-0032-T02
|
||||||
status: wait
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "08735f17-fe7e-4a6a-9b71-f350f98c30b1"
|
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
|
`metadata`, `labels` and `attributes` already flatten into the rego input, so the
|
||||||
compiler can emit membership today.
|
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
|
```task
|
||||||
id: WARDEN-WP-0032-T03
|
id: WARDEN-WP-0032-T03
|
||||||
status: wait
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "fd642377-8d64-473c-b684-f8f1a842c223"
|
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
|
*accuracy, not altitude*. Declaring a stricter zone than can be evidenced is the
|
||||||
failure mode that looks like progress.
|
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
|
```task
|
||||||
id: WARDEN-WP-0032-T04
|
id: WARDEN-WP-0032-T04
|
||||||
status: wait
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
|
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`,
|
owning it — a superseding record, never an in-place edit. Update `SCOPE.md`,
|
||||||
`wiki/WorkloadSecurityPosture.md`, and `wiki/PolicyGatedSigning.md` with it.
|
`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
|
```task
|
||||||
id: WARDEN-WP-0032-T05
|
id: WARDEN-WP-0032-T05
|
||||||
status: done
|
status: done
|
||||||
|
|
@ -241,7 +285,7 @@ policy covers these paths (T06).
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: WARDEN-WP-0032-T06
|
id: WARDEN-WP-0032-T06
|
||||||
status: progress
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
|
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
|
tokens — a write against railiance-platform's OpenBao. Exposure stays theoretical
|
||||||
to the same degree as before.
|
to the same degree as before.
|
||||||
|
|
||||||
**Still open:** whether the maturity-derived default replaces the `ungraded`
|
**Platform half closed 2026-08-22.** `RAILIANCE-WP-0022` consumed the generated
|
||||||
sentinel entirely (waits on `ZONE-WP-0001-T03`).
|
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
|
## Related
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue