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
|
||||
need to the subsystem that owns it.** SSH signing is **production-verified** on
|
||||
|
|
@ -48,14 +48,13 @@ the read-only conformance checker `scripts/check_secret_posture_conformance.py`
|
|||
and the dev-tier contract-double library `warden.doubles` (T4). Canon landing in
|
||||
net-kingdom / info-tech-canon is owner-driven (tracked via coordination messages, T5).
|
||||
|
||||
**Policy gate** is shipped on the caller side (WP-0007) with production registry
|
||||
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
|
||||
policy package (FLEX-WP-0006) and now runs the in-cluster pin
|
||||
`flex-auth-ops-warden` (FLEX-WP-0007 finished; FLEX-WP-0016). `policy.enabled`
|
||||
remains **false**: that pin is in `callerAuth.mode: warn` because ops-warden sent
|
||||
no caller identity. WP-0031 ships the calling side (`policy.caller_auth` +
|
||||
`scripts/check_policy_caller_identity.py`); the flip waits on an operator picking
|
||||
a token source and flex-auth then enforcing.
|
||||
**The policy gate is zone-aware.** The caller-identity path is production proven
|
||||
and the flex-auth pin enforces caller authentication. WP-0032 adopted
|
||||
`security-zones_v0.1`: the repo-wide `policy.enabled` and `policy.fail_closed`
|
||||
settings are retired, target workload membership compiles into flex-auth resource
|
||||
attributes, and ops-warden selects dependency failure behavior from the target
|
||||
zone. Unknown membership is explicit and uses the versioned build profile.
|
||||
Ops-warden itself declares `z1-operational` in `tenancy.yaml`.
|
||||
|
||||
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
|
||||
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
|
||||
|
|
@ -82,7 +81,7 @@ for warden-sign) but is used by 2 of 24 lanes. See
|
|||
**INTENT alignment:** SSH issuance mission met in production. All ops-warden workplans
|
||||
through WP-0029 are finished except WP-0027 (`backlog`) and WP-0030 (`proposed`).
|
||||
Remaining distance is in other repos' lanes: ops-bridge running the cert_command pilot
|
||||
cutover, flex-auth runtime deployment (FLEX-WP-0007, unblocks `policy.enabled: true`),
|
||||
cutover, flex-auth publishing the zone-aware pre-sign stance package,
|
||||
the owner-driven WP-0015 canon landing, and — newly named — the missing owner front
|
||||
doors that keep ops-warden holding interim lanes (secrets-engine, tenant-engine).
|
||||
|
||||
|
|
@ -153,9 +152,10 @@ superseding ADR, never an in-place edit.
|
|||
| `ADR-0003` | Cover gaps, but never silently own them |
|
||||
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
|
||||
| `ADR-0005` | Implement one lane narrowly, route everything else |
|
||||
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (`policy.enabled` stays off until WP-0032) |
|
||||
| `ADR-0006` | Superseded: enforcement is zone-scoped, never a global flag |
|
||||
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
|
||||
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
|
||||
| `ADR-0009` | Adopt security-zones v0.1 and compile explicit workload membership; PEP failure mode is per zone |
|
||||
|
||||
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
|
||||
credential-management standard, the-custodian's ADR-001 workplan convention — are
|
||||
|
|
@ -188,7 +188,8 @@ for the rest.
|
|||
- `cert_command`: `warden sign <actor> --pubkey <path>` → cert on stdout
|
||||
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
|
||||
- `warden status`, cleanup, scorecard, signatures log
|
||||
- Opt-in flex-auth policy gate (`policy.enabled`, `policy_decision_id` in log)
|
||||
- Zone-aware flex-auth policy gate (`policy_decision_id`, zone, failure mode, and
|
||||
outcome in the signing audit; no repo-wide enable switch)
|
||||
- Production flex-auth registry builder (`scripts/build_flex_auth_registry.py`,
|
||||
`registry/flex-auth/production_registry_snapshot.json`)
|
||||
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
|
||||
|
|
@ -247,7 +248,7 @@ for the rest.
|
|||
| --- | --- |
|
||||
| WP-0001–0005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
|
||||
| WP-0006 | Credential routing, security map, inventory patterns, OpenBao checklist |
|
||||
| WP-0007 | Opt-in flex-auth policy gate (`policy.enabled`) |
|
||||
| WP-0007 | Original opt-in flex-auth policy gate (global switch retired by WP-0032) |
|
||||
| WP-0008 | Production sign verification, stewardship closeout, archive hygiene |
|
||||
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
|
||||
| WP-0010 | Access routing charter + pointer catalog |
|
||||
|
|
@ -280,7 +281,7 @@ for the rest.
|
|||
| WP | Status | Focus |
|
||||
| --- | --- | --- |
|
||||
| WP-0027 | `backlog` | Tamper-resistant credential governance + mass rotation/lockdown (Strand B follow-on to WP-0026) |
|
||||
| WP-0032 | `proposed` | Adopt security zones as a consumer — retire the global `policy.enabled` (model owned by `zone-engine`) |
|
||||
| WP-0032 | `finished` | Security zones adopted — global switch retired, explicit workload references compiled, and owner policy live |
|
||||
| WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate |
|
||||
|
||||
Remaining production distance is also in other repos' lanes (see Known gaps).
|
||||
|
|
@ -356,10 +357,10 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
|
|||
- **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived).
|
||||
WP-0031 shipped the calling identity and flex-auth's pin now runs
|
||||
`callerAuth.mode: enforce` (FLEX-WP-0016) — the gate is **ready and verified**
|
||||
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). `policy.enabled`
|
||||
remains **false by decision**, not by blocker: `ADR-0006` scopes enforcement to
|
||||
security zones, which `zone-engine` defines (`ZONE-WP-0001`). Re-check with
|
||||
`scripts/check_policy_caller_identity.py`
|
||||
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). WP-0032 and
|
||||
`ADR-0009` retired the global switch: the compiled target workload selects the
|
||||
zone, flex-auth owns stance, and ops-warden applies the zone's PEP failure mode.
|
||||
Re-check caller identity with `scripts/check_policy_caller_identity.py`.
|
||||
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
|
||||
conformance checker, dev doubles); canon landing owner-driven
|
||||
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| workplan | WARDEN-WP-0032 | proposed | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| workplan | WARDEN-WP-0033 | active | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
|
||||
| task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md |
|
||||
|
|
@ -117,11 +117,12 @@
|
|||
| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T02 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T03 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T04 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T02 | progress | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T06 | progress | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ type: adr
|
|||
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
status: superseded
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-19"
|
||||
updated: "2026-08-19"
|
||||
updated: "2026-08-22"
|
||||
last_reviewed: "2026-08-19"
|
||||
review_interval: 6m
|
||||
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
successor: "ops-warden-adr-0009"
|
||||
---
|
||||
|
||||
# ADR-0006 — Enforcement is zone-scoped, never a global flag
|
||||
|
|
|
|||
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-0004` | High-risk lanes refuse raw value streaming to agent sessions | ops-warden, and any agent runtime calling `warden access` |
|
||||
| `ADR-0005` | Implement one lane narrowly, route everything else | ops-warden |
|
||||
| `ADR-0006` | Enforcement is zone-scoped, never a global flag | ops-warden |
|
||||
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (**superseded by ADR-0009**) | ops-warden |
|
||||
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden |
|
||||
| `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden |
|
||||
| `ADR-0009` | Adopt security-zones v0.1; compile explicit membership and select PEP failure mode per zone | ops-warden |
|
||||
|
|
|
|||
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
|
||||
ttl_hours: 24
|
||||
description: "ops-bridge tunnel agent for state-hub"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: ops-bridge-tunnel
|
||||
|
||||
agt-codex-interhub-bootstrap:
|
||||
type: agt
|
||||
|
|
@ -17,6 +20,9 @@ actors:
|
|||
- agt-interhub-bootstrap
|
||||
ttl_hours: 2
|
||||
description: "Short-lived agent access for attended Inter-Hub bootstrap"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: codex-interhub-bootstrap
|
||||
|
||||
adm-example:
|
||||
type: adm
|
||||
|
|
@ -24,6 +30,9 @@ actors:
|
|||
- adm-full
|
||||
ttl_hours: 48
|
||||
description: "Example human operator — replace with per-person adm-* actors"
|
||||
zone_subject:
|
||||
applicability: not-applicable
|
||||
reason: human operator retains native actor identity
|
||||
|
||||
atm-backup-daily:
|
||||
type: atm
|
||||
|
|
@ -31,6 +40,9 @@ actors:
|
|||
- atm-backup-daily
|
||||
ttl_hours: 8
|
||||
description: "Example nightly automation actor"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: backup-daily
|
||||
|
||||
hosts:
|
||||
example-host:
|
||||
|
|
@ -38,4 +50,4 @@ hosts:
|
|||
agt:
|
||||
- agt-task-bridge
|
||||
atm:
|
||||
- atm-backup-daily
|
||||
- atm-backup-daily
|
||||
|
|
|
|||
|
|
@ -15,18 +15,24 @@ vault:
|
|||
inventory_path: ~/.config/warden/inventory.yaml
|
||||
state_dir: ~/.local/state/warden
|
||||
|
||||
# Opt-in flex-auth gate — enable only when flex-auth is reachable at flex_auth_url.
|
||||
# Zone-aware flex-auth gate. Missing target membership is the explicit unknown
|
||||
# profile; there is no repo-wide enable switch.
|
||||
# Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory).
|
||||
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
|
||||
policy:
|
||||
# Keep false until the flex-auth ops-warden pin runs callerAuth.mode: enforce.
|
||||
# Gate: python scripts/check_policy_caller_identity.py (WARDEN-WP-0031)
|
||||
enabled: false
|
||||
# The in-cluster pin for ops-warden's signing policy (FLEX-WP-0016). A bare
|
||||
# flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it
|
||||
# through a port-forward or tunnel and point this at that local address.
|
||||
flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||
fail_closed: true
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open
|
||||
not-applicable: fail_closed
|
||||
tenant: tenant:platform
|
||||
subject_env: WARDEN_POLICY_SUBJECT
|
||||
system: ops-warden
|
||||
|
|
@ -44,4 +50,4 @@ policy:
|
|||
# command: kubectl create token ops-warden -n ops-warden
|
||||
# --audience flex-auth --duration 10m
|
||||
token_env: WARDEN_POLICY_CALLER_TOKEN
|
||||
audience: flex-auth
|
||||
audience: flex-auth
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@
|
|||
],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||
"security_zone_standard": "security-zones_v0.1",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@
|
|||
"ssh-signing",
|
||||
"adm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "adm-example",
|
||||
|
|
@ -78,7 +77,10 @@
|
|||
"allowed_principals": [
|
||||
"adm-full"
|
||||
],
|
||||
"max_ttl_hours": 48
|
||||
"max_ttl_hours": 48,
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "not-applicable",
|
||||
"security_zone_reason": "human operator retains native actor identity"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -88,7 +90,6 @@
|
|||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-codex-interhub-bootstrap",
|
||||
|
|
@ -100,7 +101,11 @@
|
|||
"allowed_principals": [
|
||||
"agt-interhub-bootstrap"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
"max_ttl_hours": 2,
|
||||
"workload_id": "codex-interhub-bootstrap",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -110,7 +115,6 @@
|
|||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-state-hub-bridge",
|
||||
|
|
@ -122,7 +126,11 @@
|
|||
"allowed_principals": [
|
||||
"agt-task-bridge"
|
||||
],
|
||||
"max_ttl_hours": 24
|
||||
"max_ttl_hours": 24,
|
||||
"workload_id": "ops-bridge-tunnel",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -132,7 +140,6 @@
|
|||
"ssh-signing",
|
||||
"atm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "atm-backup-daily",
|
||||
|
|
@ -144,7 +151,11 @@
|
|||
"allowed_principals": [
|
||||
"atm-backup-daily"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
"max_ttl_hours": 8,
|
||||
"workload_id": "backup-daily",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
# declares it, and is null where the field set has not been established --
|
||||
# null means unknown, never 'one field'.
|
||||
|
||||
generated_at: "2026-08-21T11:28:33Z"
|
||||
generated_at: "2026-08-22T12:59:27Z"
|
||||
source: ops-warden/registry/routing/catalog.yaml
|
||||
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
|
||||
catalog_revision_date: "2026-08-21T13:26:10+02:00"
|
||||
catalog_dirty: false
|
||||
catalog_dirty: true
|
||||
high_risk_lane_count: 19
|
||||
concrete_path_count: 14
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@
|
|||
# canon_ref upstream net-kingdom doc the wiki section tracks
|
||||
# reviewed date this pointer was last checked against canon (YYYY-MM-DD)
|
||||
# status active (surfaced by default) | draft (hidden unless --all)
|
||||
# workload_ref explicit workload applicability and authoritative join.
|
||||
# Managed deployables use rapp_id + name (+ optional deployable);
|
||||
# operational workloads use name + declaration_ref; unresolved
|
||||
# applicable lanes carry unknown_reason; native non-workload
|
||||
# subjects carry not-applicable + reason. Never infer from paths.
|
||||
# steps ONLY when warden_executes: true
|
||||
# cert_command ONLY when warden_executes: true
|
||||
# delegation WP-0030 register. mode: native | interim | permanent.
|
||||
|
|
@ -39,6 +44,9 @@ entries:
|
|||
title: Short-lived SSH certificate for host / ops reachability
|
||||
# Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Generic certificate-signing lane; each actor resource must resolve its own target workload."
|
||||
need_keywords: [ssh, certificate, cert, host, access, sign, adm, agt, atm, reachability, ops]
|
||||
owner_repo: ops-warden
|
||||
subsystem: ops-warden
|
||||
|
|
@ -61,6 +69,10 @@ entries:
|
|||
title: Scoped OpenBao token for ops-warden SSH signing (warden-sign)
|
||||
# A scoped VAULT_TOKEN is a credential in its own right. Graded on what the value is, not on whether ops-warden currently proxies it (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
name: ops-warden
|
||||
declaration_ref: tenancy.yaml
|
||||
need_keywords: [vault_token, vault, token, warden-sign, warden, ops-warden, signing, sign, smoke, flex-auth, credential, broker, lease, openbao, ssh, production]
|
||||
owner_repo: railiance-platform
|
||||
subsystem: OpenBao credential broker
|
||||
|
|
@ -97,6 +109,9 @@ entries:
|
|||
title: API key, DB credential, or dynamic lease
|
||||
# Wildcard lane over platform/workloads/<domain>/<workload>/<bundle>: its ceiling is the most dangerous bundle it can resolve to (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Generic credential-path pattern; concrete lanes carry the workload reference."
|
||||
need_keywords: [api, key, secret, database, db, password, token, lease, openbao, vault, kv, dynamic, credential, npm, npm_auth_token, registry]
|
||||
owner_repo: railiance-platform
|
||||
subsystem: OpenBao
|
||||
|
|
@ -133,6 +148,9 @@ entries:
|
|||
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
|
||||
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "whynot-design has not published an authoritative workload identity declaration."
|
||||
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package]
|
||||
owner_repo: railiance-platform
|
||||
subsystem: OpenBao
|
||||
|
|
@ -177,6 +195,9 @@ entries:
|
|||
title: Authorization decision — may this actor perform this action
|
||||
# Returns an authorization decision; no credential flows (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Generic authorization action; the governed resource supplies workload identity."
|
||||
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision]
|
||||
owner_repo: flex-auth
|
||||
subsystem: flex-auth
|
||||
|
|
@ -195,6 +216,9 @@ entries:
|
|||
title: Interactive login, OIDC token, or MFA
|
||||
# Interactive browser OIDC: a login flow, not a KV read. No stored value is fetched, and warden access already excludes is_login from raw-value streaming (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Interactive login action; caller identity is native context, not a workload target."
|
||||
need_keywords: [login, oidc, identity, mfa, token, jwt, sso, keycloak, key-cape, iam, claims, authenticate, signin]
|
||||
owner_repo: key-cape
|
||||
subsystem: key-cape / Keycloak
|
||||
|
|
@ -222,6 +246,9 @@ entries:
|
|||
title: SSH tunnel or port forward
|
||||
# Routes to ops-bridge and supplies a cert_command; no secret value flows (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "ops-bridge has not published the operational tunnel workload declaration."
|
||||
need_keywords: [tunnel, port, forward, bridge, ops-bridge, reverse, transport, ssh-tunnel, cert_command]
|
||||
owner_repo: ops-bridge
|
||||
subsystem: ops-bridge
|
||||
|
|
@ -240,6 +267,9 @@ entries:
|
|||
title: Host SSH principal file or force-command deployment
|
||||
# Principal-file deployment via Ansible; no secret value flows (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Host principal-file deployment is an infrastructure action, not a workload."
|
||||
need_keywords: [principal, auth_principals, force-command, host, sshd, hardening, railiance-infra, ansible]
|
||||
owner_repo: railiance-infra
|
||||
subsystem: railiance-infra
|
||||
|
|
@ -258,6 +288,9 @@ entries:
|
|||
title: Inter-Hub bootstrap SSH envelope
|
||||
# Graded high conservatively: ops-warden could not establish from the lane definition that no key material moves in the envelope. Regrade with evidence, do not assume down (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The inter-hub bootstrap execution unit has no authoritative workload declaration."
|
||||
need_keywords: [inter-hub, interhub, bootstrap, ops-hub, agt-interhub-bootstrap, envelope, force-command, CUST-WP-0049]
|
||||
owner_repo: ops-warden
|
||||
subsystem: ops-warden + railiance-infra
|
||||
|
|
@ -276,6 +309,9 @@ entries:
|
|||
title: activity-core IssueSink → issue-core REST emission
|
||||
# Emission routing only — the API key is a separate lane (WARDEN-WP-0032-T05).
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "activity-core has not published an authoritative workload identity declaration."
|
||||
need_keywords: [activity-core, issue-sink, issue-core, emission, issue_core_url, issue_core_api_key, tasks, ingest, rest, issuesink]
|
||||
owner_repo: activity-core
|
||||
subsystem: activity-core + issue-core
|
||||
|
|
@ -301,6 +337,11 @@ entries:
|
|||
# A Forgejo backend token is not recovered by rotating an ingestion key.
|
||||
# Found by secrets-engine reviewing SECRETS-WP-0006 -- not by us.
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
rapp_id: rapp-issue-core
|
||||
name: issue-core
|
||||
deployable: issue-core
|
||||
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN] # CCR-2026-0002
|
||||
need_keywords: [issue-core, ingestion, api, key, openbao, issue_core_api_key, eso, external-secrets]
|
||||
owner_repo: railiance-platform
|
||||
|
|
@ -349,6 +390,9 @@ entries:
|
|||
# rotation. Disclosure lets an attacker forge webhook deliveries into the
|
||||
# federation hub, which rotating the write token alone does not undo.
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "reuse-surface has not published an authoritative workload identity declaration."
|
||||
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET] # CCR-2026-0005
|
||||
need_keywords: [reuse-surface, reuse_surface, hub, register, federation, write, token, bearer, REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET, reuse.coulomb.social]
|
||||
owner_repo: railiance-platform
|
||||
|
|
@ -404,6 +448,9 @@ entries:
|
|||
verified: owner-confirmed
|
||||
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "llm-connect has not published an authoritative workload identity declaration."
|
||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0003 / RAILIANCE-WP-0010
|
||||
# (promoted 2026-07-02): policy workload-kv-read-llm-connect-provider-secrets and k8s
|
||||
# auth role external-secrets-activity-core applied; ExternalSecret
|
||||
|
|
@ -445,6 +492,9 @@ entries:
|
|||
verified: unverified
|
||||
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The backup execution unit has no authoritative workload identity declaration."
|
||||
# CCR-2026-0004: policy + OIDC role applied; values provisioned 2026-07-07.
|
||||
# Capabilities-safe re-verify 2026-07-16 (WP-0026 T07): lane-policy token
|
||||
# capabilities=read on data path; default-policy and agent-high-risk-boundary = deny;
|
||||
|
|
@ -491,6 +541,9 @@ entries:
|
|||
verified: owner-confirmed
|
||||
# High-risk: site-admin PAT (WP-0026 T04).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The Forgejo administration workload has no authoritative workload identity declaration."
|
||||
# CCR-2026-0006: approved by platform-operator 2026-07-12; policy
|
||||
# workload-kv-read-forgejo-admin + OIDC role forgejo-admin-workload-kv-read live on
|
||||
# bao.coulomb.social; PAT attended-minted and stored under field API_TOKEN at
|
||||
|
|
@ -533,6 +586,9 @@ entries:
|
|||
reviewed: "2026-08-11"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The Binky email consumer has no authoritative workload identity declaration."
|
||||
# CCR-2026-0007: tenants/ mount + policy + OIDC role applied; founder provisioned
|
||||
# values via UI (version ≥2, not placeholder). Capabilities-safe verify 2026-07-17:
|
||||
# lane-policy read; default deny. Host: imap.ionos.de:993 (binky-control config).
|
||||
|
|
@ -568,6 +624,9 @@ entries:
|
|||
reviewed: "2026-08-11"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The Binky Qonto MCP consumer has no authoritative workload identity declaration."
|
||||
# CCR-2026-0008: policy + OIDC role applied; secret at tenants/binky/qonto-api
|
||||
# (fields API_KEY, API_USER). Map to QONTO_API_KEY / QONTO_ORGANIZATION_ID for
|
||||
# qonto-mcp-server. First read-only pull 2026-07-21 (BINKY-WP-0005-T05).
|
||||
|
|
@ -604,6 +663,11 @@ entries:
|
|||
reviewed: "2026-08-17"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
rapp_id: rapp-qonto
|
||||
name: qonto
|
||||
deployable: rapp-qonto
|
||||
auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange"
|
||||
path_template: "platform/workloads/rapp-qonto/keycape-client"
|
||||
fetch_command: "bao kv get -field=client_secret platform/workloads/rapp-qonto/keycape-client"
|
||||
|
|
@ -635,6 +699,9 @@ entries:
|
|||
reviewed: "2026-08-11"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "agent-harness has not published an authoritative workload identity declaration."
|
||||
# Provisioned 2026-07-17 on railiance01: ed25519 keypair on host, OpenBao copy at
|
||||
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
|
||||
# coulomb/executor-sandbox (title agent-harness-railiance01). Git push verified.
|
||||
|
|
@ -671,6 +738,9 @@ entries:
|
|||
reviewed: "2026-08-15"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "agent-harness has not published an authoritative workload identity declaration."
|
||||
# Provisioned 2026-07-17: role agent-harness-binky-mail bound to existing policy
|
||||
# workload-kv-read-binky-company-email-imap; role_id/secret_id delivered to
|
||||
# railiance01 ~/.local/agent-harness/approle-binky-mail (0600). Positive IMAP field
|
||||
|
|
@ -696,6 +766,9 @@ entries:
|
|||
title: Object-storage STS / temporary S3 credentials
|
||||
# Temporary S3 credentials are still credentials (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Generic STS credential-vending pattern; concrete consumers carry workload references."
|
||||
need_keywords: [s3, sts, object-storage, minio, artifact-store, temporary, credentials, bucket, vending]
|
||||
owner_repo: net-kingdom
|
||||
subsystem: flex-auth + OpenBao + artifact-store
|
||||
|
|
@ -726,6 +799,9 @@ entries:
|
|||
reviewed: "2026-08-15"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: not-applicable
|
||||
reason: "Generic database credential-vending pattern; concrete consumers carry workload references."
|
||||
exec_capable: false
|
||||
|
||||
- id: rein-openweights-openrouter-approle
|
||||
|
|
@ -745,6 +821,9 @@ entries:
|
|||
reviewed: "2026-08-15"
|
||||
verified: unverified
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "rein-openweights has not published an authoritative workload identity declaration."
|
||||
# Built 2026-07-27 by ops-mason (MASON-WP-0001-T05), approved by Bernd
|
||||
# Worsch 2026-07-27. Policy + AppRole live; reins/ KV v2 mount created
|
||||
# (no existing mount fit without widening scope beyond what was
|
||||
|
|
@ -805,6 +884,9 @@ entries:
|
|||
# railiance-apps; user-engine is consumer-only and claims no lane here.
|
||||
consumers: [user-engine]
|
||||
risk: standard
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "coulomb-social has not published an authoritative workload identity declaration."
|
||||
# K8s assembly is the live handoff today (same pattern as vergage-teilnahme-env).
|
||||
# OpenBao KV platform/workloads/coulomb/coulomb-social/runtime-env is the
|
||||
# future custody home — CCR not yet applied; resolvable via operator script.
|
||||
|
|
@ -827,6 +909,9 @@ entries:
|
|||
title: audit-core sender registry (write and operator-read tokens)
|
||||
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "audit-core has not published an authoritative workload identity declaration."
|
||||
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS]
|
||||
owner_repo: ops-mason
|
||||
subsystem: OpenBao + audit-core
|
||||
|
|
@ -864,6 +949,9 @@ entries:
|
|||
reviewed: "2026-08-21"
|
||||
verified: owner-confirmed
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "email-connect has not published an authoritative workload identity declaration."
|
||||
# CCR-2026-0010 approved 2026-08-12; applied same day (EMAIL-WP-0004-T03):
|
||||
# policies external-secrets-email-connect + workload-kv-read-email-connect-transactional,
|
||||
# KV platform/workloads/email-connect/transactional v1, ESO token Secret,
|
||||
|
|
@ -906,6 +994,9 @@ entries:
|
|||
reviewed: "2026-08-15"
|
||||
verified: unverified
|
||||
risk: high
|
||||
workload_ref:
|
||||
applicability: applicable
|
||||
unknown_reason: "The attended Scaleway bootstrap execution unit has no authoritative workload identity declaration."
|
||||
# CCR-2026-0011. Values via founder paste-once or local tfvars ingest.
|
||||
# Not the Barman runtime key (platform-pg-backup-s3).
|
||||
auth_method: "caller's own OpenBao token (founder / operator workstation)"
|
||||
|
|
|
|||
|
|
@ -55,12 +55,79 @@ def _caring_descriptor(actor_type: str, resource_id: str) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
||||
def _resolved_by_workload(zone_resolutions: dict[str, Any] | None) -> dict[str, Any]:
|
||||
records = (zone_resolutions or {}).get("records") or []
|
||||
resolved: dict[str, Any] = {}
|
||||
for record in records:
|
||||
workload_id = str(record.get("workload_id") or "")
|
||||
if not workload_id:
|
||||
continue
|
||||
if workload_id in resolved:
|
||||
raise ValueError(f"duplicate security-zone resolution for {workload_id!r}")
|
||||
resolved[workload_id] = record
|
||||
return resolved
|
||||
|
||||
|
||||
def _zone_attributes(
|
||||
actor: str,
|
||||
entry: dict[str, Any],
|
||||
resolutions: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
subject = entry.get("zone_subject")
|
||||
if not isinstance(subject, dict):
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "catalog_applicability_absent",
|
||||
}
|
||||
applicability = subject.get("applicability")
|
||||
if applicability == "not-applicable":
|
||||
reason = str(subject.get("reason") or "").strip()
|
||||
if not reason:
|
||||
raise ValueError(f"{actor}.zone_subject.reason is required")
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "not-applicable",
|
||||
"security_zone_reason": reason,
|
||||
}
|
||||
if applicability != "applicable":
|
||||
raise ValueError(
|
||||
f"{actor}.zone_subject.applicability must be applicable or not-applicable"
|
||||
)
|
||||
workload_id = str(subject.get("workload_id") or "").strip()
|
||||
if not workload_id:
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_reference_absent",
|
||||
}
|
||||
record = resolutions.get(workload_id)
|
||||
if record is None:
|
||||
return {
|
||||
"workload_id": workload_id,
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent",
|
||||
}
|
||||
return {
|
||||
"workload_id": workload_id,
|
||||
"security_zone": str(record.get("effective_zone") or "unknown"),
|
||||
"security_zone_declared": record.get("declared_zone"),
|
||||
"security_zone_admission": str(record.get("admission") or "unknown"),
|
||||
"security_zone_reason": str(record.get("admission_reason") or "unknown"),
|
||||
"security_zone_revision": record.get("membership_revision"),
|
||||
}
|
||||
|
||||
|
||||
def build_registry(
|
||||
inventory: dict[str, Any], zone_resolutions: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
actors: dict[str, Any] = inventory.get("actors") or {}
|
||||
resources: list[dict[str, Any]] = []
|
||||
subjects: list[dict[str, Any]] = []
|
||||
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
|
||||
relationships: list[dict[str, Any]] = []
|
||||
resolutions = _resolved_by_workload(zone_resolutions)
|
||||
|
||||
for name, entry in sorted(actors.items()):
|
||||
actor_type = str(entry["type"])
|
||||
|
|
@ -74,7 +141,6 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"id": resource_id,
|
||||
"type": "ssh-certificate",
|
||||
"labels": ["ssh-signing", actor_type],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": name,
|
||||
|
|
@ -82,6 +148,7 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"allowed_subjects": [name, f"iam:{name}"],
|
||||
"allowed_principals": principals,
|
||||
"max_ttl_hours": ttl_hours,
|
||||
**_zone_attributes(name, entry, resolutions),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -156,8 +223,8 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"caring_profiles": ["caring-0.4.0-rc2"],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||
"security_zone_standard": "security-zones_v0.1",
|
||||
"tenant": "tenant:platform",
|
||||
},
|
||||
}
|
||||
|
|
@ -186,14 +253,24 @@ def main() -> None:
|
|||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
|
||||
parser.add_argument("-o", "--output", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--zone-resolutions",
|
||||
type=Path,
|
||||
help="zone-engine resolved-view JSON; absent references remain unknown",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
inventory = yaml.safe_load(args.inventory.read_text()) or {}
|
||||
registry = build_registry(inventory)
|
||||
zone_resolutions = (
|
||||
json.loads(args.zone_resolutions.read_text())
|
||||
if args.zone_resolutions is not None
|
||||
else None
|
||||
)
|
||||
registry = build_registry(inventory, zone_resolutions)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(registry, indent=2) + "\n")
|
||||
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Readiness gate for the flex-auth ``policy.enabled`` flip (WARDEN-WP-0031).
|
||||
"""Readiness gate for the zone-aware flex-auth caller identity.
|
||||
|
||||
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
|
||||
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
|
||||
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
|
||||
but a caller that sends no ``Authorization`` header only produces a
|
||||
``caller authentication warning`` and is still served. That pin cannot move to
|
||||
``enforce`` — and therefore ``policy.enabled: true`` cannot be set — until
|
||||
ops-warden's calling side actually presents a token.
|
||||
``enforce`` until ops-warden's calling side actually presents a token. The
|
||||
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
|
||||
|
||||
This script asserts the calling side *without* flipping anything:
|
||||
|
||||
|
|
@ -57,9 +57,7 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
|
|||
return [("fail", "warden.yaml", str(e))]
|
||||
|
||||
policy = cfg.policy
|
||||
checks.append(
|
||||
("ok", "warden.yaml", f"loaded; policy.enabled={str(policy.enabled).lower()}")
|
||||
)
|
||||
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
|
||||
|
||||
mode = policy.caller_auth.mode
|
||||
if mode == "none":
|
||||
|
|
@ -84,12 +82,12 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
|
|||
)
|
||||
|
||||
target = url or policy.flex_auth_url
|
||||
if url is None and not policy.enabled:
|
||||
if target is None:
|
||||
checks.append(
|
||||
(
|
||||
"skip",
|
||||
"live /v1/check",
|
||||
f"policy.enabled=false; pass --url to smoke {target} anyway",
|
||||
"policy.flex_auth_url is absent; pass --url to run the live smoke",
|
||||
)
|
||||
)
|
||||
return checks
|
||||
|
|
@ -188,13 +186,13 @@ def main() -> int:
|
|||
if failed:
|
||||
print(
|
||||
f"\nNOT READY — {len(failed)} check(s) failed. "
|
||||
"Do not ask flex-auth to enforce, and do not set policy.enabled: true."
|
||||
"Do not ask flex-auth to enforce caller authentication."
|
||||
)
|
||||
return 1
|
||||
print(
|
||||
"\nREADY — the calling side presents an identity. Next: tell flex-auth to set "
|
||||
"callerAuth.mode: enforce on flex-auth-ops-warden, re-run this check, then set "
|
||||
"policy.enabled: true with fail_closed: true."
|
||||
"callerAuth.mode: enforce on flex-auth-ops-warden and re-run this check. "
|
||||
"Zone-specific PEP failure modes already replace the retired global switches."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -136,8 +136,13 @@ def main() -> int:
|
|||
if args.check:
|
||||
current = args.out.read_text() if args.out.exists() else ""
|
||||
# generated_at always differs; compare everything else.
|
||||
strip = lambda t: "\n".join(l for l in t.splitlines() if not l.startswith("generated_at:"))
|
||||
if strip(current) != strip(content):
|
||||
def strip_generated_at(text: str) -> str:
|
||||
return "\n".join(
|
||||
line for line in text.splitlines()
|
||||
if not line.startswith("generated_at:")
|
||||
)
|
||||
|
||||
if strip_generated_at(current) != strip_generated_at(content):
|
||||
print(f"STALE: {args.out} does not match the catalog. Re-run without --check.")
|
||||
return 1
|
||||
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")
|
||||
|
|
|
|||
|
|
@ -66,9 +66,8 @@ ca_key: $SMOKE_DIR/ca_key
|
|||
state_dir: $SMOKE_DIR/state
|
||||
inventory_path: $INVENTORY
|
||||
policy:
|
||||
enabled: true
|
||||
flex_auth_url: http://$ADDR
|
||||
fail_closed: true
|
||||
zone_registry_path: $REGISTRY
|
||||
tenant: tenant:platform
|
||||
system: ops-warden
|
||||
EOF
|
||||
|
|
@ -106,9 +105,8 @@ vault:
|
|||
inventory_path: $INVENTORY
|
||||
state_dir: $SMOKE_DIR/state-vault
|
||||
policy:
|
||||
enabled: true
|
||||
flex_auth_url: http://$ADDR
|
||||
fail_closed: true
|
||||
zone_registry_path: $REGISTRY
|
||||
tenant: tenant:platform
|
||||
system: ops-warden
|
||||
EOF
|
||||
|
|
@ -118,4 +116,4 @@ EOF
|
|||
python3 -c "import json,sys; e=json.loads(sys.argv[1]); assert e.get('backend')=='vault' and e.get('policy_decision_id'); print('vault policy_decision_id:', e['policy_decision_id'])" "$VAULT_LINE"
|
||||
fi
|
||||
|
||||
echo "OK — production registry policy gate smoke passed"
|
||||
echo "OK — production registry policy gate smoke passed"
|
||||
|
|
|
|||
234
scripts/report_workload_join.py
Executable file → Normal file
234
scripts/report_workload_join.py
Executable file → Normal file
|
|
@ -1,128 +1,156 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Report the lane -> workload join, for the security-zone model (ZONE-WP-0001-T03).
|
||||
"""Report explicit lane -> workload resolution for security-zones_v0.1.
|
||||
|
||||
A security zone's subject is the **workload**, not the lane — policy is about the
|
||||
running thing and whoever answers for it. ops-warden's catalog is a credential
|
||||
surface, so on its own it cannot name that subject. The other side of the join
|
||||
lives in `rapp-*/declarations/rapp.yaml`, which declares `workload_identity`
|
||||
along with `data_classification` and `criticality`; ops-warden's
|
||||
`registry/policy/security-posture.yaml` then maps classification to a minimum
|
||||
maturity via `dataclass_floor`.
|
||||
The catalog owner declares whether each lane is workload-applicable. Managed
|
||||
deployables use the exact Repo Manager v1 ``(rapp_id, name, deployable?)``
|
||||
reference. Independently governed operational workloads use ``name`` plus an
|
||||
owner declaration reference. Unknown and not-applicable are explicit results.
|
||||
|
||||
This script computes the join and reports its coverage. It asserts nothing it
|
||||
cannot derive: a lane whose workload cannot be established is reported as
|
||||
unmatched rather than guessed, because the unmatched set is the informative
|
||||
output — those are lanes existing for something that is not a declared workload.
|
||||
|
||||
Read-only. Touches no secret value and no live system.
|
||||
|
||||
Usage:
|
||||
python scripts/report_workload_join.py [--rapp-root ~] [--json]
|
||||
This script never parses a credential path, consults ``owner_repo`` as an
|
||||
identity hint, or substitutes a repository name. It reads declarations only.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
_SRC = Path(__file__).resolve().parent.parent / "src"
|
||||
if _SRC.is_dir() and str(_SRC) not in sys.path:
|
||||
sys.path.insert(0, str(_SRC))
|
||||
|
||||
import yaml # noqa: E402
|
||||
import yaml
|
||||
|
||||
|
||||
def load_rapp_workloads(root: Path) -> Dict[str, Dict[str, Any]]:
|
||||
"""workload name -> declaration, from every rapp-*/declarations/rapp.yaml."""
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
def load_rapp_workloads(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Exact Repo Manager v1 key -> authoritative declaration projection."""
|
||||
out: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
|
||||
try:
|
||||
data = yaml.safe_load(decl.read_text()) or {}
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
identity = data.get("workload_identity") or {}
|
||||
rapp_id = data.get("rapp_id")
|
||||
name = identity.get("name")
|
||||
if not name:
|
||||
if not rapp_id or not name:
|
||||
continue
|
||||
out[str(name)] = {
|
||||
"rapp_id": data.get("rapp_id"),
|
||||
deployables = []
|
||||
for member in (data.get("composition") or {}).get("member_repos") or []:
|
||||
deployables.extend(str(value) for value in member.get("deployables") or [])
|
||||
out[(str(rapp_id), str(name))] = {
|
||||
"source": str(decl),
|
||||
"deployables": sorted(set(deployables)),
|
||||
"data_classification": data.get("data_classification"),
|
||||
"criticality": data.get("criticality"),
|
||||
"readiness_state": data.get("readiness_state"),
|
||||
"bound_reefs": data.get("bound_reefs"),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def candidate_workloads(path_template: Optional[str]) -> List[str]:
|
||||
"""Workload names a path could plausibly name — never a single guess.
|
||||
|
||||
The convention is `platform/workloads/<domain>/<workload>/<bundle>`, but it
|
||||
is not applied consistently: `platform/workloads/rapp-qonto/keycape-client`
|
||||
has one level fewer, so position alone cannot say which segment is the
|
||||
workload. Both are offered and matched against declarations.
|
||||
"""
|
||||
if not path_template or "<" in path_template:
|
||||
return []
|
||||
parts = [p for p in path_template.split("/") if p]
|
||||
if len(parts) >= 4 and parts[1] == "workloads":
|
||||
return [parts[3], parts[2]]
|
||||
if parts and parts[0] == "tenants" and len(parts) >= 3:
|
||||
return [parts[2], parts[1]]
|
||||
return []
|
||||
def _direct_declaration_path(
|
||||
declaration_ref: str, *, catalog_path: Path, estate_root: Path
|
||||
) -> Path:
|
||||
ref = Path(declaration_ref)
|
||||
if ref.is_absolute():
|
||||
return ref
|
||||
local = catalog_path.resolve().parents[2] / ref
|
||||
return local if local.exists() else estate_root / ref
|
||||
|
||||
|
||||
def build(catalog_path: Path, rapp_root: Path) -> Dict[str, Any]:
|
||||
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
|
||||
declared = load_rapp_workloads(rapp_root)
|
||||
floor = (
|
||||
yaml.safe_load(
|
||||
(catalog_path.parent.parent / "policy" / "security-posture.yaml").read_text()
|
||||
def _resolve_direct(
|
||||
ref: dict[str, Any], *, catalog_path: Path, estate_root: Path
|
||||
) -> tuple[dict[str, Any] | None, str | None]:
|
||||
source = _direct_declaration_path(
|
||||
str(ref["declaration_ref"]), catalog_path=catalog_path, estate_root=estate_root
|
||||
)
|
||||
if not source.exists():
|
||||
return None, f"declaration not found: {source}"
|
||||
try:
|
||||
declaration = yaml.safe_load(source.read_text()) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
return None, f"invalid declaration YAML: {exc}"
|
||||
identity = declaration.get("workload_identity") or {}
|
||||
if identity.get("name") != ref.get("name"):
|
||||
return None, (
|
||||
f"declared workload_identity.name={identity.get('name')!r}, "
|
||||
f"expected {ref.get('name')!r}"
|
||||
)
|
||||
or {}
|
||||
).get("dataclass_floor", {})
|
||||
context = (declaration.get("zones") or {}).get("context", {})
|
||||
return {
|
||||
"source": str(source),
|
||||
"data_classification": context.get("data_classification"),
|
||||
"criticality": context.get("criticality"),
|
||||
"maturity": context.get("maturity"),
|
||||
"declared_zone": (declaration.get("zones") or {}).get("membership"),
|
||||
}, None
|
||||
|
||||
matched, unmatched, no_path = [], [], []
|
||||
|
||||
def build(catalog_path: Path, estate_root: Path) -> dict[str, Any]:
|
||||
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
|
||||
managed = load_rapp_workloads(estate_root)
|
||||
posture_path = catalog_path.parent.parent / "policy" / "security-posture.yaml"
|
||||
floor = (yaml.safe_load(posture_path.read_text()) or {}).get("dataclass_floor", {})
|
||||
|
||||
resolved: list[dict[str, Any]] = []
|
||||
unknown: list[dict[str, Any]] = []
|
||||
not_applicable: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
lane = entry.get("id")
|
||||
cands = candidate_workloads(entry.get("path_template"))
|
||||
if not cands:
|
||||
no_path.append({"lane": lane, "risk": entry.get("risk")})
|
||||
lane = str(entry.get("id"))
|
||||
ref = entry.get("workload_ref") or {}
|
||||
applicability = ref.get("applicability")
|
||||
if applicability == "not-applicable":
|
||||
not_applicable.append({"lane": lane, "reason": ref.get("reason")})
|
||||
continue
|
||||
hit = next((c for c in cands if c in declared), None)
|
||||
if hit is None:
|
||||
unmatched.append(
|
||||
{"lane": lane, "risk": entry.get("risk"), "candidates": cands}
|
||||
if applicability != "applicable":
|
||||
unknown.append({"lane": lane, "reason": "applicability missing or invalid"})
|
||||
continue
|
||||
if ref.get("unknown_reason"):
|
||||
unknown.append({"lane": lane, "reason": ref["unknown_reason"]})
|
||||
continue
|
||||
|
||||
projection: dict[str, Any] | None
|
||||
error: str | None = None
|
||||
if ref.get("rapp_id"):
|
||||
key = (str(ref.get("rapp_id")), str(ref.get("name")))
|
||||
projection = managed.get(key)
|
||||
if projection is None:
|
||||
error = f"Repo Manager reference does not resolve: {key[0]}/{key[1]}"
|
||||
elif ref.get("deployable") and ref["deployable"] not in projection["deployables"]:
|
||||
error = f"deployable {ref['deployable']!r} is not declared by {key[0]}/{key[1]}"
|
||||
else:
|
||||
projection, error = _resolve_direct(
|
||||
ref, catalog_path=catalog_path, estate_root=estate_root
|
||||
)
|
||||
if error or projection is None:
|
||||
unknown.append({"lane": lane, "reason": error or "reference unresolved"})
|
||||
continue
|
||||
decl = declared[hit]
|
||||
cls = decl.get("data_classification")
|
||||
matched.append(
|
||||
|
||||
classification = projection.get("data_classification")
|
||||
resolved.append(
|
||||
{
|
||||
"lane": lane,
|
||||
"workload": hit,
|
||||
"risk": entry.get("risk"),
|
||||
"data_classification": cls,
|
||||
"criticality": decl.get("criticality"),
|
||||
"min_maturity": floor.get(cls),
|
||||
"unmapped_classification": bool(cls) and cls not in floor,
|
||||
"workload_ref": ref,
|
||||
"source": projection.get("source"),
|
||||
"data_classification": classification,
|
||||
"criticality": projection.get("criticality"),
|
||||
"maturity": projection.get("maturity") or floor.get(classification),
|
||||
"declared_zone": projection.get("declared_zone"),
|
||||
"unmapped_classification": bool(classification) and classification not in floor,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"declared_workloads": declared,
|
||||
"matched": matched,
|
||||
"unmatched": unmatched,
|
||||
"no_path": no_path,
|
||||
"dataclass_floor": floor,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"resolved": resolved,
|
||||
"unknown": unknown,
|
||||
"not_applicable": not_applicable,
|
||||
"ok": len(resolved) + len(unknown) + len(not_applicable) == len(entries),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--rapp-root", type=Path, default=Path.home())
|
||||
parser.add_argument(
|
||||
"--estate-root", "--rapp-root", dest="estate_root", type=Path, default=Path.home()
|
||||
)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
parser.add_argument(
|
||||
"--catalog",
|
||||
|
|
@ -133,40 +161,24 @@ def main() -> int:
|
|||
/ "catalog.yaml",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
report = build(args.catalog, Path(os.path.expanduser(str(args.rapp_root))))
|
||||
report = build(args.catalog, args.estate_root)
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
return 0
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
print("lane -> workload join\n")
|
||||
print(f" declared workloads (rapp-*): {len(report['declared_workloads'])}")
|
||||
print(f" lanes matched to a workload: {len(report['matched'])}")
|
||||
print(f" lanes unmatched: {len(report['unmatched'])}")
|
||||
print(f" lanes with no usable path: {len(report['no_path'])}\n")
|
||||
|
||||
if report["matched"]:
|
||||
print("MATCHED")
|
||||
for m in report["matched"]:
|
||||
flag = " <-- classification unmapped by dataclass_floor" if m[
|
||||
"unmapped_classification"
|
||||
] else ""
|
||||
print(
|
||||
f" {m['lane']:34} -> {str(m['workload']):16} "
|
||||
f"{str(m['data_classification']):13} crit={str(m['criticality']):9} "
|
||||
f"min={str(m['min_maturity']):4} risk={m['risk']}{flag}"
|
||||
)
|
||||
if report["unmatched"]:
|
||||
print("\nUNMATCHED — a lane exists for something no rapp declares")
|
||||
for u in report["unmatched"]:
|
||||
print(
|
||||
f" {u['lane']:34} risk={str(u['risk']):9} "
|
||||
f"candidates={', '.join(u['candidates'])}"
|
||||
)
|
||||
if report["no_path"]:
|
||||
print("\nNO USABLE PATH — not a KV lane, or a pattern rather than an address")
|
||||
print(" " + ", ".join(n["lane"] for n in report["no_path"]))
|
||||
return 0
|
||||
print("explicit lane -> workload resolution\n")
|
||||
print(f" resolved: {len(report['resolved'])}")
|
||||
print(f" unknown: {len(report['unknown'])}")
|
||||
print(f" not-applicable: {len(report['not_applicable'])}\n")
|
||||
for row in report["resolved"]:
|
||||
ref = row["workload_ref"]
|
||||
prefix = f"{ref.get('rapp_id')}/" if ref.get("rapp_id") else ""
|
||||
print(f"RESOLVED {row['lane']:34} -> {prefix}{ref.get('name')}")
|
||||
for row in report["unknown"]:
|
||||
print(f"UNKNOWN {row['lane']:34} {row['reason']}")
|
||||
for row in report["not_applicable"]:
|
||||
print(f"NOT-APPLICABLE {row['lane']:34} {row['reason']}")
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -71,6 +71,6 @@ def policy_gate_status() -> str:
|
|||
cfg = load_config()
|
||||
except ConfigError:
|
||||
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
||||
if cfg.policy.enabled:
|
||||
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
|
||||
if cfg.policy.flex_auth_url:
|
||||
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import os
|
|||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
_AUDIT_FILENAME = "audit.jsonl"
|
||||
_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
|
@ -215,7 +215,6 @@ def collect_activity(
|
|||
since = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
events = read_events(state_dir, since=since, kinds=kinds)
|
||||
if include_legacy:
|
||||
legacy_kinds = kinds or {"sign", "access", "worker"}
|
||||
if not kinds or "sign" in kinds:
|
||||
events.extend(_legacy_sign_events(state_dir, since))
|
||||
if not kinds or "access" in kinds:
|
||||
|
|
@ -278,4 +277,4 @@ def fetch_hub_notes(*, days: int = 7, hub_url: Optional[str] = None) -> list[dic
|
|||
"summary": summary,
|
||||
}
|
||||
)
|
||||
return notes
|
||||
return notes
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ def _append_signature_log(
|
|||
}
|
||||
if spec.policy_decision_id:
|
||||
entry["policy_decision_id"] = spec.policy_decision_id
|
||||
if spec.policy_zone:
|
||||
entry["policy_zone"] = spec.policy_zone
|
||||
if spec.policy_failure_mode:
|
||||
entry["policy_failure_mode"] = spec.policy_failure_mode
|
||||
if spec.policy_outcome:
|
||||
entry["policy_outcome"] = spec.policy_outcome
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
with (state_dir / "signatures.log").open("a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
|
@ -76,6 +82,9 @@ def _append_signature_log(
|
|||
actor_type=spec.actor_type.value,
|
||||
backend=backend,
|
||||
ttl_hours=spec.ttl_hours,
|
||||
policy_zone=spec.policy_zone,
|
||||
policy_failure_mode=spec.policy_failure_mode,
|
||||
policy_outcome=spec.policy_outcome,
|
||||
)
|
||||
except Exception:
|
||||
pass # audit must not block signing
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ before it evaluates the request: `Authorization: Bearer <token>` is passed to a
|
|||
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
|
||||
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
|
||||
that header, the pin logs `caller authentication warning` and can only run in
|
||||
`warn` mode — which is why `policy.enabled` cannot flip.
|
||||
`warn` mode — which is why enforcing caller authentication is a separate gate.
|
||||
|
||||
This module resolves the token at call time and hands it straight to the request.
|
||||
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from warden.config import ConfigError, WardenConfig, load_config
|
|||
from warden.policy import check_sign_policy
|
||||
from warden.inventory import ActorEntry, InventoryError, PrincipalsInventory, load_inventory, save_inventory
|
||||
from warden.models import ActorType, CertSpec, DEFAULT_TTL_HOURS, validate_actor_name
|
||||
from warden.routing.catalog import blocker_stale_days
|
||||
from warden.scorecard import run_scorecard
|
||||
|
||||
app = typer.Typer(
|
||||
|
|
@ -120,7 +121,7 @@ def _get_ca(cfg: WardenConfig):
|
|||
|
||||
|
||||
def _apply_policy_gate(cfg: WardenConfig, spec: CertSpec) -> None:
|
||||
"""Run flex-auth check when policy.enabled; sets spec.policy_decision_id."""
|
||||
"""Run the zone-aware flex-auth check; record any returned decision id."""
|
||||
decision_id = check_sign_policy(cfg.policy, spec)
|
||||
if decision_id:
|
||||
spec.policy_decision_id = decision_id
|
||||
|
|
@ -644,6 +645,7 @@ def _entry_summary(entry) -> dict:
|
|||
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
|
||||
"risk": entry.risk,
|
||||
"high_risk": entry.is_high_risk,
|
||||
"workload_ref": entry.workload_ref.to_dict(),
|
||||
# Renewal guidance (WP-0026 T06) — advisory, no secret values. `has_rotation`
|
||||
# lets a caller gate before asking for the full block via `warden rotate-guide`.
|
||||
"has_rotation": entry.has_rotation,
|
||||
|
|
@ -771,9 +773,6 @@ def route_list(
|
|||
)
|
||||
|
||||
|
||||
from warden.routing.catalog import blocker_stale_days
|
||||
|
||||
|
||||
def _gap_is_stale(entry, delegation, reviewed: str, stale_days) -> bool:
|
||||
"""An interim lane needs attention on either of two independent grounds.
|
||||
|
||||
|
|
@ -1255,6 +1254,14 @@ def _access_proxy(
|
|||
"token stays in the caller's own store.[/dim]"
|
||||
)
|
||||
else:
|
||||
if no_policy:
|
||||
err.print(
|
||||
"[red]--no-policy is retired[/red]: security-zones_v0.1 selects "
|
||||
"the policy stance and failure mode. Remove the flag; an unresolved "
|
||||
"workload uses the explicit unknown-zone profile."
|
||||
)
|
||||
raise typer.Exit(2)
|
||||
|
||||
# G1 — caller identity. ops-warden adds no token of its own.
|
||||
if not caller_auth_present():
|
||||
err.print(
|
||||
|
|
@ -1263,24 +1270,20 @@ def _access_proxy(
|
|||
)
|
||||
raise typer.Exit(3)
|
||||
|
||||
# G3 — policy gate before fetch.
|
||||
if cfg.policy.enabled:
|
||||
try:
|
||||
decision_id = check_fetch_policy(
|
||||
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
|
||||
)
|
||||
except CAError as e:
|
||||
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
|
||||
raise typer.Exit(4)
|
||||
err.print(f"[green]flex-auth allow[/green] (decision {decision_id}).")
|
||||
elif not no_policy:
|
||||
err.print(
|
||||
"[yellow]flex-auth gate is not enforced[/yellow] (policy.enabled=false). "
|
||||
"Re-run with [bold]--no-policy[/bold] to proxy ungated, or enable the gate."
|
||||
# G3 — the zone-aware policy gate always runs before fetch.
|
||||
try:
|
||||
decision_id = check_fetch_policy(
|
||||
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
|
||||
)
|
||||
except CAError as e:
|
||||
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
|
||||
raise typer.Exit(4)
|
||||
if decision_id:
|
||||
err.print(f"[green]flex-auth decision[/green] ({decision_id}).")
|
||||
else:
|
||||
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
|
||||
err.print(
|
||||
"[yellow]flex-auth unavailable; unknown-zone fail_open applied[/yellow]."
|
||||
)
|
||||
|
||||
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
|
||||
# share the resolved fetch command.
|
||||
|
|
@ -1434,7 +1437,10 @@ def access(
|
|||
] = False,
|
||||
no_policy: Annotated[
|
||||
bool,
|
||||
typer.Option("--no-policy", help="Acknowledge proxying when the flex-auth gate is not enforced"),
|
||||
typer.Option(
|
||||
"--no-policy",
|
||||
help="Retired compatibility flag; zone-aware policy evaluation cannot be bypassed",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Operator front door: how to obtain any credential, gated and audited.
|
||||
|
|
|
|||
|
|
@ -45,9 +45,19 @@ class CallerAuthConfig:
|
|||
|
||||
@dataclass
|
||||
class PolicyConfig:
|
||||
enabled: bool = False
|
||||
flex_auth_url: str = "http://127.0.0.1:8080"
|
||||
fail_closed: bool = True
|
||||
flex_auth_url: Optional[str] = None
|
||||
zone_registry_path: Optional[Path] = None
|
||||
failure_modes: Dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
"z0-experimental": "fail_open",
|
||||
"z1-operational": "fail_open",
|
||||
"z2-protected": "fail_open",
|
||||
"z2-continuity": "fail_open",
|
||||
"z3-critical": "fail_closed",
|
||||
"unknown": "fail_open",
|
||||
"not-applicable": "fail_closed",
|
||||
}
|
||||
)
|
||||
tenant: str = "tenant:platform"
|
||||
subject_env: str = "WARDEN_POLICY_SUBJECT"
|
||||
system: str = "ops-warden"
|
||||
|
|
@ -148,6 +158,13 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
)
|
||||
|
||||
policy_raw = raw.get("policy") or {}
|
||||
retired = sorted({"enabled", "fail_closed"}.intersection(policy_raw))
|
||||
if retired:
|
||||
raise ConfigError(
|
||||
"retired policy setting(s) "
|
||||
+ ", ".join(f"policy.{key}" for key in retired)
|
||||
+ "; security-zones_v0.1 now selects stance and failure mode"
|
||||
)
|
||||
caller_raw = policy_raw.get("caller_auth") or {}
|
||||
caller_command = caller_raw.get("command")
|
||||
if isinstance(caller_command, str):
|
||||
|
|
@ -175,10 +192,33 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
|
||||
if caller_cfg.mode == "command" and not caller_cfg.command:
|
||||
raise ConfigError("policy.caller_auth.command is required for mode: command")
|
||||
failure_modes = PolicyConfig().failure_modes
|
||||
configured_failure_modes = policy_raw.get("failure_modes") or {}
|
||||
if not isinstance(configured_failure_modes, dict):
|
||||
raise ConfigError("policy.failure_modes must be a mapping")
|
||||
failure_modes.update(
|
||||
{str(zone): str(mode) for zone, mode in configured_failure_modes.items()}
|
||||
)
|
||||
invalid_modes = {
|
||||
zone: mode
|
||||
for zone, mode in failure_modes.items()
|
||||
if mode not in {"fail_open", "fail_closed"}
|
||||
}
|
||||
if invalid_modes:
|
||||
raise ConfigError(
|
||||
"policy.failure_modes values must be fail_open or fail_closed: "
|
||||
f"{invalid_modes}"
|
||||
)
|
||||
zone_registry_path = policy_raw.get("zone_registry_path")
|
||||
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
|
||||
policy_cfg = PolicyConfig(
|
||||
enabled=bool(policy_raw.get("enabled", False)),
|
||||
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
|
||||
fail_closed=bool(policy_raw.get("fail_closed", True)),
|
||||
flex_auth_url=flex_auth_url,
|
||||
zone_registry_path=(
|
||||
Path(os.path.expanduser(str(zone_registry_path)))
|
||||
if zone_registry_path
|
||||
else None
|
||||
),
|
||||
failure_modes=failure_modes,
|
||||
tenant=str(policy_raw.get("tenant", "tenant:platform")),
|
||||
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
|
||||
system=str(policy_raw.get("system", "ops-warden")),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ class CertSpec:
|
|||
principals: List[str]
|
||||
identity: str = "" # defaults to actor_name if empty
|
||||
policy_decision_id: Optional[str] = None
|
||||
policy_zone: Optional[str] = None
|
||||
policy_failure_mode: Optional[str] = None
|
||||
policy_outcome: Optional[str] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.identity:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
|
||||
"""Zone-aware flex-auth policy gates for OpsWarden."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -20,21 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
|
|||
return f"sha256:{digest}"
|
||||
|
||||
|
||||
def _caller_headers(cfg: PolicyConfig) -> dict[str, str]:
|
||||
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
|
||||
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
|
||||
|
||||
When the token cannot be obtained we refuse the call under ``fail_closed``
|
||||
When the token cannot be obtained we refuse the call under the selected
|
||||
zone's ``fail_closed`` behavior
|
||||
rather than silently falling back to an unauthenticated request — an
|
||||
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
||||
"""
|
||||
try:
|
||||
return caller_auth_headers(cfg.caller_auth)
|
||||
except CallerIdentityError as e:
|
||||
if cfg.fail_closed:
|
||||
if fail_closed:
|
||||
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
||||
return {}
|
||||
|
||||
|
||||
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
|
||||
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
|
||||
if cfg.zone_registry_path is None:
|
||||
return "unknown"
|
||||
try:
|
||||
registry = json.loads(cfg.zone_registry_path.read_text())
|
||||
resources = registry["resource_manifests"][0]["resources"]
|
||||
resource = next(item for item in resources if item.get("id") == resource_id)
|
||||
attributes = resource.get("attributes") or {}
|
||||
if attributes.get("security_zone_admission") == "not-applicable":
|
||||
return "not-applicable"
|
||||
zone = str(attributes.get("security_zone") or "unknown")
|
||||
return zone if zone in cfg.failure_modes else "unknown"
|
||||
except (OSError, ValueError, KeyError, StopIteration, TypeError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
|
||||
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
|
||||
|
||||
|
||||
def _evaluator_failure(
|
||||
message: str,
|
||||
*,
|
||||
fail_closed: bool,
|
||||
cause: Exception | None = None,
|
||||
spec: CertSpec | None = None,
|
||||
) -> None:
|
||||
if fail_closed:
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_closed"
|
||||
if cause is None:
|
||||
raise CAError(message)
|
||||
raise CAError(message) from cause
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_open"
|
||||
|
||||
|
||||
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
||||
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
|
||||
|
||||
|
|
@ -42,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
|||
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
||||
"""Call flex-auth /v1/check before signing.
|
||||
|
||||
Returns decision id when policy is enabled and effect is allow.
|
||||
Returns None when policy is disabled.
|
||||
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
|
||||
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
|
||||
Evaluator failures use the PEP-owned failure mode for the target workload's
|
||||
compiled zone; absent resolution is the explicit ``unknown`` profile.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
resource_id = f"ssh-cert:actor/{spec.actor_name}"
|
||||
zone = _resource_zone(cfg, resource_id)
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
spec.policy_zone = zone
|
||||
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
f"flex-auth URL is not configured for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
|
||||
|
|
@ -76,37 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||
f"for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
|
||||
f"(fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
spec.policy_outcome = "deny"
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
||||
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
spec.policy_outcome = effect
|
||||
return str(decision_id)
|
||||
|
||||
|
||||
|
|
@ -116,13 +183,17 @@ def check_fetch_policy(
|
|||
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
|
||||
|
||||
The action is ``read`` on a ``secret`` resource owned by another subsystem —
|
||||
ops-warden is the conduit, not the owner. Returns the decision id on allow,
|
||||
None when policy is disabled, and raises CAError on deny (or on an unreachable
|
||||
flex-auth when fail_closed). No secret value is ever part of this request.
|
||||
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
zone = "unknown"
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
"flex-auth URL is not configured for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
|
||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||
request = {
|
||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
||||
|
|
@ -137,33 +208,44 @@ def check_fetch_policy(
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
"flex-auth returned a non-JSON decision for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
return str(decision_id)
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
return str(decision_id)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from warden.routing.catalog import (
|
|||
find_catalog_path,
|
||||
load_catalog,
|
||||
)
|
||||
from warden.routing.models import Delegation, RouteEntry
|
||||
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
|
||||
|
||||
__all__ = [
|
||||
"Catalog",
|
||||
|
|
@ -20,6 +20,7 @@ __all__ = [
|
|||
"CatalogFreshness",
|
||||
"Delegation",
|
||||
"RouteEntry",
|
||||
"WorkloadReference",
|
||||
"find_catalog_path",
|
||||
"load_catalog",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ import yaml
|
|||
from warden.routing.models import (
|
||||
VALID_DELEGATION_MODES,
|
||||
VALID_RISK,
|
||||
VALID_WORKLOAD_APPLICABILITY,
|
||||
Delegation,
|
||||
RotationGuide,
|
||||
RouteEntry,
|
||||
WorkloadReference,
|
||||
)
|
||||
|
||||
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
||||
|
|
@ -64,6 +66,7 @@ _REQUIRED_FIELDS = (
|
|||
"canon_ref",
|
||||
"reviewed",
|
||||
"status",
|
||||
"workload_ref",
|
||||
)
|
||||
_VALID_STATUS = ("active", "draft")
|
||||
_VALID_LANES = ("secret", "login")
|
||||
|
|
@ -516,6 +519,82 @@ def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation
|
|||
)
|
||||
|
||||
|
||||
def _parse_workload_ref(entry_id: str, raw: object) -> WorkloadReference:
|
||||
"""Parse an explicit workload join without attempting identity inference."""
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref must be a mapping; every lane must "
|
||||
"declare applicable or not-applicable"
|
||||
)
|
||||
|
||||
applicability = str(raw.get("applicability", "")).strip()
|
||||
if applicability not in VALID_WORKLOAD_APPLICABILITY:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.applicability {applicability!r} invalid "
|
||||
f"(expected one of {VALID_WORKLOAD_APPLICABILITY})"
|
||||
)
|
||||
|
||||
def optional(name: str) -> Optional[str]:
|
||||
value = raw.get(name)
|
||||
return str(value).strip() if value is not None and str(value).strip() else None
|
||||
|
||||
ref = WorkloadReference(
|
||||
applicability=applicability,
|
||||
rapp_id=optional("rapp_id"),
|
||||
name=optional("name"),
|
||||
deployable=optional("deployable"),
|
||||
declaration_ref=optional("declaration_ref"),
|
||||
reason=optional("reason"),
|
||||
unknown_reason=optional("unknown_reason"),
|
||||
)
|
||||
|
||||
target_fields = (ref.rapp_id, ref.name, ref.deployable, ref.declaration_ref)
|
||||
if applicability == "not-applicable":
|
||||
if not ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.reason is required for not-applicable"
|
||||
)
|
||||
if any(target_fields) or ref.unknown_reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} not-applicable workload_ref must not carry a "
|
||||
"workload target or unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if ref.unknown_reason:
|
||||
if any(target_fields) or ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} unknown workload_ref must carry only "
|
||||
"applicability and unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if not ref.name:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref requires name or "
|
||||
"unknown_reason"
|
||||
)
|
||||
if ref.rapp_id:
|
||||
if ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} managed workload_ref must not also carry "
|
||||
"declaration_ref"
|
||||
)
|
||||
elif not ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} operational workload_ref requires declaration_ref"
|
||||
)
|
||||
if ref.deployable and not ref.rapp_id:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.deployable requires rapp_id"
|
||||
)
|
||||
if ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref must not carry reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
|
||||
def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry #{index} is not a mapping")
|
||||
|
|
@ -576,12 +655,16 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
|
||||
)
|
||||
|
||||
risk = str(raw.get("risk", "standard")).strip() or "standard"
|
||||
if risk not in VALID_RISK:
|
||||
risk_value = raw.get("risk")
|
||||
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
|
||||
risk = risk or "ungraded"
|
||||
if risk != "ungraded" and risk not in VALID_RISK:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
|
||||
)
|
||||
|
||||
workload_ref = _parse_workload_ref(entry_id, raw.get("workload_ref"))
|
||||
|
||||
return RouteEntry(
|
||||
id=entry_id,
|
||||
title=str(raw["title"]),
|
||||
|
|
@ -593,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
canon_ref=str(raw["canon_ref"]),
|
||||
reviewed=str(raw["reviewed"]),
|
||||
status=status,
|
||||
workload_ref=workload_ref,
|
||||
steps=[str(s) for s in steps],
|
||||
cert_command=str(cert_command) if cert_command else None,
|
||||
auth_method=handoff["auth_method"],
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ VALID_RISK = ("standard", "high")
|
|||
# interim — ops-warden covers a gap; intended_owner + blocked_on required
|
||||
# permanent — ops-warden is the designed owner of this front door (SSH only today)
|
||||
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
|
||||
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
|
||||
|
||||
IMPLICIT_DELEGATION_BLOCKED_ON = (
|
||||
"unclassified — no delegation block; treat as a question, not a settlement"
|
||||
|
|
@ -100,6 +101,46 @@ class Delegation:
|
|||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkloadReference:
|
||||
"""Authoritative workload join for a catalog lane (WARDEN-WP-0032).
|
||||
|
||||
Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple.
|
||||
Independently governed operational workloads use ``name`` plus an owner
|
||||
declaration reference. An applicable lane whose owner has not published an
|
||||
identity remains explicitly ``unknown``; it is never inferred from the
|
||||
credential path or repository name.
|
||||
"""
|
||||
|
||||
applicability: str # applicable | not-applicable
|
||||
rapp_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
deployable: Optional[str] = None
|
||||
declaration_ref: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
unknown_reason: Optional[str] = None
|
||||
|
||||
@property
|
||||
def resolution(self) -> str:
|
||||
if self.applicability == "not-applicable":
|
||||
return "not-applicable"
|
||||
if self.unknown_reason:
|
||||
return "unknown"
|
||||
return "resolved"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"applicability": self.applicability,
|
||||
"rapp_id": self.rapp_id,
|
||||
"name": self.name,
|
||||
"deployable": self.deployable,
|
||||
"declaration_ref": self.declaration_ref,
|
||||
"reason": self.reason,
|
||||
"unknown_reason": self.unknown_reason,
|
||||
"resolution": self.resolution,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteEntry:
|
||||
id: str
|
||||
|
|
@ -112,6 +153,8 @@ class RouteEntry:
|
|||
canon_ref: str
|
||||
reviewed: str
|
||||
status: str # "active" | "draft"
|
||||
# Explicit workload applicability and authoritative join. Never inferred.
|
||||
workload_ref: Optional[WorkloadReference] = None
|
||||
# SSH lane only — None/empty for routed (non-executed) needs.
|
||||
steps: List[str] = field(default_factory=list)
|
||||
cert_command: Optional[str] = None
|
||||
|
|
@ -169,6 +212,34 @@ class RouteEntry:
|
|||
"""
|
||||
return self.risk not in LOW_RISK_GRADES
|
||||
|
||||
def risk_for_zone(
|
||||
self,
|
||||
*,
|
||||
effective_zone: str = "unknown",
|
||||
admission: str = "unknown",
|
||||
synthetic_only: bool = False,
|
||||
) -> str:
|
||||
"""Resolve an absent grade using security-zones_v0.1 section 5.1.
|
||||
|
||||
Explicit grades always win. The sole lower default is a satisfied
|
||||
``z0-experimental`` workload proven synthetic-only. Every other zone,
|
||||
failed/unknown admission, and missing context fails safe to at least
|
||||
``high``; z3 reports ``critical`` (which the read boundary treats as
|
||||
high). Catalog CI still requires explicit grades, so this is the safe
|
||||
runtime behavior for malformed or newer external catalogs.
|
||||
"""
|
||||
if self.is_graded:
|
||||
return self.risk
|
||||
if (
|
||||
effective_zone == "z0-experimental"
|
||||
and admission == "satisfied"
|
||||
and synthetic_only
|
||||
):
|
||||
return "standard"
|
||||
if effective_zone == "z3-critical" and admission == "satisfied":
|
||||
return "critical"
|
||||
return "high"
|
||||
|
||||
@property
|
||||
def has_rotation(self) -> bool:
|
||||
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
||||
|
|
|
|||
37
tenancy.yaml
37
tenancy.yaml
|
|
@ -8,6 +8,17 @@ framework: netkingdom-tenancy-posture
|
|||
service: ops-warden
|
||||
role: ssh-certificate-authority
|
||||
|
||||
workload_identity:
|
||||
name: ops-warden
|
||||
kind: operational-control-plane
|
||||
responsible_repo: ops-warden
|
||||
identity_bindings:
|
||||
- scheme: kubernetes-service-account
|
||||
authority: railiance01
|
||||
subject: system:serviceaccount:ops-warden:ops-warden
|
||||
principal_type: service
|
||||
environment: prod
|
||||
|
||||
tenancy:
|
||||
current:
|
||||
I: 1
|
||||
|
|
@ -108,13 +119,25 @@ tenancy:
|
|||
is single-node with a shared control plane, and under Decision 4.6.1 that
|
||||
caps V for everything bound to it (see NK-WP-0027).
|
||||
|
||||
# zones: (reserved, tenancy-posture_v0.1 Decision 5.6)
|
||||
#
|
||||
# Deliberately absent. The security-zone standard is being drafted by
|
||||
# zone-engine (ZONE-WP-0001) and no zone vocabulary exists yet, so any value
|
||||
# here would be fabricated — the failure §6 exists to prevent. ops-warden is
|
||||
# the first intended consumer and will populate this key once the sibling
|
||||
# standard defines it (WARDEN-WP-0032-T03).
|
||||
zones:
|
||||
standard: security-zones_v0.1
|
||||
membership: z1-operational
|
||||
responsible_party: team:platform-security
|
||||
justification: >-
|
||||
The attended platform signing service has a bounded operational scope and
|
||||
internal metadata exposure. It has M1 evidence, but no SLO history, on-call
|
||||
rotation, or incident exercise that would support z2-protected.
|
||||
context:
|
||||
maturity: M1
|
||||
criticality: medium
|
||||
data_classification: internal
|
||||
evidence:
|
||||
- ref: docs/evidence/security-zone-admission-2026-08-22.md
|
||||
supports: [M1, platform-only-scope, basic-slo, data-handling-note]
|
||||
- ref: history/2026-08-19-flex-auth-caller-identity-evidence.md
|
||||
supports: [production-policy-path, authenticated-caller]
|
||||
reviewed: "2026-08-22"
|
||||
review_due: "2026-11-22"
|
||||
|
||||
evidence:
|
||||
A: >-
|
||||
|
|
|
|||
|
|
@ -84,13 +84,13 @@ def test_default_vault_token_env(tmp_path):
|
|||
assert cfg.vault.token_env == "VAULT_TOKEN"
|
||||
|
||||
|
||||
def test_policy_defaults_disabled(tmp_path):
|
||||
def test_policy_defaults_to_unknown_zone_profile(tmp_path):
|
||||
cfg_path = tmp_path / "warden.yaml"
|
||||
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
|
||||
cfg = load_config(cfg_path)
|
||||
assert cfg.policy.enabled is False
|
||||
assert cfg.policy.flex_auth_url == "http://127.0.0.1:8080"
|
||||
assert cfg.policy.fail_closed is True
|
||||
assert cfg.policy.flex_auth_url is None
|
||||
assert cfg.policy.failure_modes["unknown"] == "fail_open"
|
||||
assert cfg.policy.failure_modes["z3-critical"] == "fail_closed"
|
||||
|
||||
|
||||
def test_policy_block_parsed(tmp_path):
|
||||
|
|
@ -99,18 +99,30 @@ def test_policy_block_parsed(tmp_path):
|
|||
"backend": "local",
|
||||
"ca_key": str(tmp_path / "ca"),
|
||||
"policy": {
|
||||
"enabled": True,
|
||||
"flex_auth_url": "http://flex-auth:8080",
|
||||
"fail_closed": False,
|
||||
"zone_registry_path": str(tmp_path / "zones.json"),
|
||||
"failure_modes": {"z2-protected": "fail_closed"},
|
||||
"tenant": "tenant:coulomb",
|
||||
"subject_env": "MY_SUBJECT",
|
||||
"system": "warden-test",
|
||||
},
|
||||
})
|
||||
cfg = load_config(cfg_path)
|
||||
assert cfg.policy.enabled is True
|
||||
assert cfg.policy.flex_auth_url == "http://flex-auth:8080"
|
||||
assert cfg.policy.fail_closed is False
|
||||
assert cfg.policy.zone_registry_path == tmp_path / "zones.json"
|
||||
assert cfg.policy.failure_modes["z2-protected"] == "fail_closed"
|
||||
assert cfg.policy.tenant == "tenant:coulomb"
|
||||
assert cfg.policy.subject_env == "MY_SUBJECT"
|
||||
assert cfg.policy.system == "warden-test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retired", ["enabled", "fail_closed"])
|
||||
def test_retired_global_policy_switches_are_rejected(tmp_path, retired):
|
||||
cfg_path = tmp_path / "warden.yaml"
|
||||
write_yaml(cfg_path, {
|
||||
"backend": "local",
|
||||
"ca_key": str(tmp_path / "ca"),
|
||||
"policy": {retired: True},
|
||||
})
|
||||
with pytest.raises(ConfigError, match=f"policy.{retired}"):
|
||||
load_config(cfg_path)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
|
|
|
|||
|
|
@ -31,4 +31,53 @@ def test_build_registry_from_inventory_seed(tmp_path):
|
|||
)
|
||||
assert bridge["attributes"]["actor_type"] == "agt"
|
||||
assert bridge["attributes"]["max_ttl_hours"] == 24
|
||||
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
|
||||
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
|
||||
assert "trust_zone" not in bridge
|
||||
assert bridge["attributes"]["security_zone"] == "unknown"
|
||||
assert bridge["attributes"]["security_zone_admission"] == "unknown"
|
||||
assert bridge["attributes"]["workload_id"] == "ops-bridge-tunnel"
|
||||
|
||||
human = next(
|
||||
r
|
||||
for r in registry["resource_manifests"][0]["resources"]
|
||||
if r["id"] == "ssh-cert:actor/adm-example"
|
||||
)
|
||||
assert human["attributes"]["security_zone_admission"] == "not-applicable"
|
||||
|
||||
|
||||
def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
|
||||
zones = tmp_path / "zones.json"
|
||||
zones.write_text(json.dumps({
|
||||
"records": [{
|
||||
"workload_id": "ops-bridge-tunnel",
|
||||
"declared_zone": "z2-continuity",
|
||||
"admission": "satisfied",
|
||||
"admission_reason": "admission_floor_met",
|
||||
"effective_zone": "z2-continuity",
|
||||
"membership_revision": "sha256:zone-revision",
|
||||
}]
|
||||
}))
|
||||
out = tmp_path / "registry.json"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
str(INVENTORY),
|
||||
"--zone-resolutions",
|
||||
str(zones),
|
||||
"-o",
|
||||
str(out),
|
||||
],
|
||||
check=True,
|
||||
cwd=ROOT,
|
||||
)
|
||||
registry = json.loads(out.read_text())
|
||||
bridge = next(
|
||||
r
|
||||
for r in registry["resource_manifests"][0]["resources"]
|
||||
if r["id"] == "ssh-cert:actor/agt-state-hub-bridge"
|
||||
)
|
||||
attrs = bridge["attributes"]
|
||||
assert attrs["security_zone"] == "z2-continuity"
|
||||
assert attrs["security_zone_admission"] == "satisfied"
|
||||
assert attrs["security_zone_revision"] == "sha256:zone-revision"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import json
|
|||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.memory import activate, enabled, record_command_episode, status, store_path
|
||||
from warden.memory import activate, record_command_episode, status, store_path
|
||||
from warden.worker import RuleBrain, _plan_with_memory, build_plans
|
||||
|
||||
runner = CliRunner()
|
||||
|
|
@ -140,4 +140,4 @@ def test_route_find_implicitly_activates_memory_without_explicit_command(tmp_pat
|
|||
activation = ensure_memory_context(need="ssh tunnel", implicit=True)
|
||||
assert activation is not None
|
||||
assert activation.get("implicit") is True
|
||||
assert status()["episode_count"] >= 1
|
||||
assert status()["episode_count"] >= 1
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ def _spec(pubkey_path: Path) -> CertSpec:
|
|||
)
|
||||
|
||||
|
||||
def _zone_registry(tmp_path: Path, zone: str) -> Path:
|
||||
path = tmp_path / "registry.json"
|
||||
path.write_text(
|
||||
'{"resource_manifests":[{"resources":[{"id":'
|
||||
'"ssh-cert:actor/agt-state-hub-bridge","attributes":{'
|
||||
f'"security_zone":"{zone}","security_zone_admission":"satisfied"'
|
||||
'}}]}]}'
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def test_pubkey_fingerprint(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
|
|
@ -35,26 +46,32 @@ def test_pubkey_fingerprint(tmp_path):
|
|||
assert len(fp) == 7 + 64
|
||||
|
||||
|
||||
def test_disabled_returns_none(tmp_path):
|
||||
def test_unconfigured_evaluator_uses_unknown_fail_open_profile(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=False)
|
||||
assert check_sign_policy(cfg, _spec(pubkey)) is None
|
||||
cfg = PolicyConfig()
|
||||
spec = _spec(pubkey)
|
||||
assert check_sign_policy(cfg, spec) is None
|
||||
assert spec.policy_zone == "unknown"
|
||||
assert spec.policy_failure_mode == "fail_open"
|
||||
assert spec.policy_outcome == "fail_open"
|
||||
|
||||
|
||||
def test_allow_returns_decision_id(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, flex_auth_url="http://flex-auth.test")
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
spec = _spec(pubkey)
|
||||
with patch("warden.policy.httpx.post", return_value=mock_response) as post:
|
||||
result = check_sign_policy(cfg, _spec(pubkey))
|
||||
result = check_sign_policy(cfg, spec)
|
||||
|
||||
assert result == "dec-123"
|
||||
assert spec.policy_outcome == "allow"
|
||||
post.assert_called_once()
|
||||
call_kwargs = post.call_args
|
||||
assert call_kwargs[0][0] == "http://flex-auth.test/v1/check"
|
||||
|
|
@ -67,7 +84,7 @@ def test_allow_returns_decision_id(tmp_path):
|
|||
def test_deny_raises_ca_error(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
|
|
@ -84,7 +101,10 @@ def test_deny_raises_ca_error(tmp_path):
|
|||
def test_unreachable_fail_closed_raises(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=True)
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"warden.policy.httpx.post",
|
||||
|
|
@ -97,7 +117,7 @@ def test_unreachable_fail_closed_raises(tmp_path):
|
|||
def test_unreachable_fail_open_returns_none(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=False)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
with patch(
|
||||
"warden.policy.httpx.post",
|
||||
|
|
@ -109,7 +129,10 @@ def test_unreachable_fail_open_returns_none(tmp_path):
|
|||
def test_http_error_fail_closed_raises(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=True)
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
|
|
@ -123,7 +146,7 @@ def test_http_error_fail_closed_raises(tmp_path):
|
|||
|
||||
|
||||
def test_missing_pubkey_raises(tmp_path):
|
||||
cfg = PolicyConfig(enabled=True)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
spec = _spec(tmp_path / "missing.pub")
|
||||
with pytest.raises(CAError, match="Public key not found"):
|
||||
check_sign_policy(cfg, spec)
|
||||
|
|
@ -132,7 +155,10 @@ def test_missing_pubkey_raises(tmp_path):
|
|||
def test_subject_from_env(tmp_path, monkeypatch):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, subject_env="WARDEN_POLICY_SUBJECT")
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
subject_env="WARDEN_POLICY_SUBJECT",
|
||||
)
|
||||
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "iam:bernd")
|
||||
|
||||
mock_response = MagicMock()
|
||||
|
|
@ -212,7 +238,7 @@ def test_sign_policy_sends_authorization_header(tmp_path, monkeypatch):
|
|||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
|
||||
cfg = PolicyConfig(
|
||||
enabled=True,
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
|
||||
)
|
||||
spec = CertSpec(
|
||||
|
|
@ -250,8 +276,8 @@ def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
|
|||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
cfg = PolicyConfig(
|
||||
enabled=True,
|
||||
fail_closed=True,
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
|
||||
)
|
||||
spec = CertSpec(
|
||||
|
|
@ -263,3 +289,21 @@ def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
|
|||
)
|
||||
with pytest.raises(CAError, match="caller identity unavailable"):
|
||||
policy_mod.check_sign_policy(cfg, spec)
|
||||
|
||||
|
||||
def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
|
||||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
response = MagicMock()
|
||||
response.json.return_value = {
|
||||
"effect": "audit_only",
|
||||
"reason": "advisory_would_deny_disallowed_principal",
|
||||
"id": "decision:advisory",
|
||||
}
|
||||
response.raise_for_status = MagicMock()
|
||||
spec = _spec(pubkey)
|
||||
with patch("warden.policy.httpx.post", return_value=response):
|
||||
assert check_sign_policy(cfg, spec) == "decision:advisory"
|
||||
assert spec.policy_zone == "unknown"
|
||||
assert spec.policy_outcome == "audit_only"
|
||||
|
|
|
|||
|
|
@ -193,7 +193,6 @@ def _warden_yaml(tmp_path: Path) -> Path:
|
|||
(tmp_path / "ca").write_text("")
|
||||
cfg.write_text(
|
||||
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
|
||||
"policy:\n enabled: false\n"
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
|
@ -203,10 +202,11 @@ def _proxy_env(monkeypatch, tmp_path):
|
|||
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path)))
|
||||
|
||||
|
||||
def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
|
||||
def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
# subprocess must never run if the gate blocks first.
|
||||
# The unknown-zone profile proceeds when no evaluator is configured, then
|
||||
# the independent safe-transport boundary still refuses captured stdout.
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
|
||||
|
|
@ -216,8 +216,8 @@ def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
|
|||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch"],
|
||||
)
|
||||
assert r.exit_code == 4
|
||||
assert "not enforced" in r.stdout or "not enforced" in str(r.output)
|
||||
assert r.exit_code == 6
|
||||
assert "unknown-zone fail_open" in r.output
|
||||
|
||||
|
||||
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
||||
|
|
@ -228,11 +228,27 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
|||
r = runner.invoke(
|
||||
app,
|
||||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
|
||||
"--path", "platform/x/y/z", "--fetch"],
|
||||
)
|
||||
assert r.exit_code == 3
|
||||
|
||||
|
||||
def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite retired flag")),
|
||||
)
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
|
||||
)
|
||||
assert r.exit_code == 2
|
||||
assert "--no-policy is retired" in r.output
|
||||
|
||||
|
||||
# --- T4: login lane --------------------------------------------------------
|
||||
|
||||
def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path):
|
||||
|
|
@ -281,6 +297,7 @@ def test_invalid_lane_rejected(tmp_path):
|
|||
id="x", title="t", need_keywords=["k"], owner_repo="o", subsystem="s",
|
||||
warden_executes=False, wiki_ref="w", canon_ref="c", reviewed="2026-06-27",
|
||||
status="active", lane="bogus",
|
||||
workload_ref={"applicability": "not-applicable", "reason": "fixture"},
|
||||
)
|
||||
p = tmp_path / "c.yaml"
|
||||
p.write_text(yaml.dump({"version": 1, "entries": [entry]}))
|
||||
|
|
@ -361,7 +378,7 @@ def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
|
|||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")),
|
||||
)
|
||||
# CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout.
|
||||
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch", "--no-policy"])
|
||||
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"])
|
||||
assert r.exit_code == 6
|
||||
assert "sanctioned transport" in r.output.lower() or "refusing" in r.output.lower()
|
||||
|
||||
|
|
@ -378,7 +395,7 @@ def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_pat
|
|||
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "whynot-design-npm-publish", "--fingerprint", "--no-policy"],
|
||||
["access", "whynot-design-npm-publish", "--fingerprint"],
|
||||
)
|
||||
assert r.exit_code == 0
|
||||
assert "top-secret-token-value" not in r.output # value never shown
|
||||
|
|
@ -395,7 +412,7 @@ def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch):
|
|||
app,
|
||||
[
|
||||
"access", "railiance-backup-offsite-lane",
|
||||
"--fetch", "--no-policy", "--unsafe-stdout",
|
||||
"--fetch", "--unsafe-stdout",
|
||||
],
|
||||
)
|
||||
assert r.exit_code == 7, r.output
|
||||
|
|
@ -415,7 +432,7 @@ def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
|
|||
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "railiance-backup-offsite-lane", "--fingerprint", "--no-policy"],
|
||||
["access", "railiance-backup-offsite-lane", "--fingerprint"],
|
||||
)
|
||||
assert r.exit_code == 0, r.output
|
||||
assert "should-not-appear" not in r.output
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ No test here requires a live subsystem — routing is a read-only pointer layer.
|
|||
"""
|
||||
import json
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -11,10 +12,9 @@ import yaml
|
|||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from datetime import date
|
||||
|
||||
from warden.routing import CatalogError, load_catalog
|
||||
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
|
||||
from warden.scorecard import check_catalog_rotation_coverage
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
|
@ -40,6 +40,10 @@ SSH_ENTRY = {
|
|||
"canon_ref": "net-kingdom/docs/x.md",
|
||||
"reviewed": "2026-06-18",
|
||||
"status": "active",
|
||||
"workload_ref": {
|
||||
"applicability": "not-applicable",
|
||||
"reason": "generic certificate action",
|
||||
},
|
||||
"cert_command": "warden sign <actor> --pubkey <path>",
|
||||
"steps": ["confirm inventory", "sign"],
|
||||
}
|
||||
|
|
@ -55,6 +59,10 @@ ROUTED_ENTRY = {
|
|||
"canon_ref": "net-kingdom/docs/x.md",
|
||||
"reviewed": "2026-06-18",
|
||||
"status": "active",
|
||||
"workload_ref": {
|
||||
"applicability": "not-applicable",
|
||||
"reason": "generic credential pattern",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -76,6 +84,45 @@ def test_real_catalog_has_one_executed_lane():
|
|||
assert [e.id for e in executed] == ["ssh-cert-host-access"]
|
||||
|
||||
|
||||
def test_every_catalog_lane_declares_workload_applicability():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
assert all(entry.workload_ref is not None for entry in catalog.entries)
|
||||
assert {entry.workload_ref.resolution for entry in catalog.entries} == {
|
||||
"resolved", "unknown", "not-applicable"
|
||||
}
|
||||
|
||||
|
||||
def test_managed_and_operational_workload_references_parse():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
managed = catalog.get("issue-core-ingestion-api-key").workload_ref
|
||||
assert managed.resolution == "resolved"
|
||||
assert (managed.rapp_id, managed.name, managed.deployable) == (
|
||||
"rapp-issue-core", "issue-core", "issue-core"
|
||||
)
|
||||
operational = catalog.get("ops-warden-warden-sign-token").workload_ref
|
||||
assert operational.resolution == "resolved"
|
||||
assert operational.rapp_id is None
|
||||
assert operational.name == "ops-warden"
|
||||
assert operational.declaration_ref == "tenancy.yaml"
|
||||
|
||||
|
||||
def test_workload_reference_rejects_ambiguous_absence(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY)
|
||||
bad.pop("workload_ref")
|
||||
with pytest.raises(CatalogError, match="workload_ref"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_workload_reference_rejects_malformed_managed_target(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY)
|
||||
bad["workload_ref"] = {
|
||||
"applicability": "applicable",
|
||||
"rapp_id": "rapp-issue-core",
|
||||
}
|
||||
with pytest.raises(CatalogError, match="requires name"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_ops_warden_warden_sign_lane_has_native_exec():
|
||||
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
|
|
@ -454,9 +501,6 @@ def test_every_entry_has_reviewed_date():
|
|||
# Rotation / re-establishment guidance registry (WARDEN-WP-0026 T06)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from warden.scorecard import check_catalog_rotation_coverage
|
||||
|
||||
|
||||
def test_every_active_vending_lane_has_rotation_guidance():
|
||||
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
|
|
@ -774,6 +818,32 @@ def test_unrecognised_grade_is_treated_as_high():
|
|||
assert entry.is_graded is False
|
||||
|
||||
|
||||
def test_ungraded_risk_uses_maturity_derived_zone_default():
|
||||
entry = _bare_entry()
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z0-experimental",
|
||||
admission="satisfied",
|
||||
synthetic_only=True,
|
||||
) == "standard"
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z0-experimental",
|
||||
admission="unknown",
|
||||
synthetic_only=True,
|
||||
) == "high"
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z3-critical",
|
||||
admission="satisfied",
|
||||
) == "critical"
|
||||
assert entry.risk_for_zone(effective_zone="unknown") == "high"
|
||||
|
||||
|
||||
def test_explicit_risk_grade_always_wins_over_zone_default():
|
||||
entry = _bare_entry(risk="standard")
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z3-critical", admission="satisfied"
|
||||
) == "standard"
|
||||
|
||||
|
||||
def test_low_risk_vocabulary_is_explicit():
|
||||
for grade in ("standard", "low", "accepted"):
|
||||
entry = _bare_entry(risk=grade)
|
||||
|
|
@ -870,6 +940,11 @@ def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env):
|
|||
assert result.exit_code == 3
|
||||
rows = json.loads(result.stdout)
|
||||
assert any(r["stale"] for r in rows)
|
||||
# A lane can be stale on age or on never having been verified; both must be
|
||||
# expressible, or asked-and-waiting silently passes the gate.
|
||||
assert any(r["stale"] and r["days_since_review"] == 0 for r in rows)
|
||||
# A freshly reviewed lane can still be stale because it was never verified;
|
||||
# asked-and-waiting must not silently pass the gate as the calendar moves.
|
||||
assert any(
|
||||
r["stale"]
|
||||
and r["days_since_review"] <= 1
|
||||
and r["verified"] == "asked-and-waiting"
|
||||
for r in rows
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
|
|
|
|||
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
|
||||
-> logs clean of unauthenticated callers
|
||||
-> flex-auth fail-closed
|
||||
-> policy.enabled: true (ops-warden side, FLEX-WP-0007)
|
||||
-> zone-specific enforce stance (flex-auth policy package)
|
||||
```
|
||||
|
||||
`policy.enabled` must not flip anywhere while `/v1/check` still answers
|
||||
unauthenticated callers.
|
||||
An enforce stance must not be assigned while `/v1/check` still answers
|
||||
unauthenticated callers. ops-warden has no global enable switch or gate bypass;
|
||||
it applies the compiled zone stance and its local per-zone failure mode.
|
||||
|
||||
**Division of the call:** the mechanism above is an architecture recommendation and
|
||||
ops-warden's to make. Accepting the pod-spec change and the rollout timing are the
|
||||
|
|
@ -179,4 +180,4 @@ and automation work — not platform-admin equivalents on hosts.
|
|||
- `wiki/AccessRouting.md` — issue-vs-route role and boundary
|
||||
- `wiki/CredentialRouting.md`
|
||||
- `wiki/PolicyGatedSigning.md` (future flex-auth hook)
|
||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ prevent, and duplicates OpenBao.
|
|||
| --- | --- | --- |
|
||||
| **G1** | **Caller identity, never warden's** | The proxy runs the owner's tool with the caller's own environment; ops-warden injects no token of its own. Secret lanes require the caller to already hold a credential (`caller_auth_present`), else they fail with the auth pointer. |
|
||||
| **G2** | **Transit only — no persistence/logging of values** | Sanctioned transports keep the value off stdout: `--out` writes it to a mode-0600 file, `--exec` injects it into a child process env, `--wrap` returns a single-use OpenBao wrapping token (not the value). Raw `--fetch` to stdout is refused for captured/piped output (`--unsafe-stdout` overrides for a human terminal). warden never writes the value to disk or log; the audit record is **metadata only**. (WP-0026 T02) |
|
||||
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before any secret-lane fetch. With `policy.enabled: false` the proxy refuses unless `--no-policy` is given to acknowledge proxying ungated. |
|
||||
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before every secret-lane fetch. Zone membership selects stance and local failure mode; an unresolved workload uses the explicit `unknown` profile. The retired `--no-policy` flag is rejected, so this gate has no CLI bypass. |
|
||||
|
||||
The catalog side enforces a fourth, upstream guard: **handoff fields are templates,
|
||||
never values.** `_assert_no_secret_material` rejects any known token prefix or
|
||||
|
|
|
|||
|
|
@ -36,11 +36,19 @@ ca_key: ~/.ssh/ops-ca-user
|
|||
inventory_path: ~/.config/warden/inventory.yaml
|
||||
state_dir: ~/.local/state/warden
|
||||
|
||||
# Optional flex-auth gate (default off — see wiki/PolicyGatedSigning.md)
|
||||
# Zone-aware flex-auth gate. With no URL, the explicit unknown/build profile
|
||||
# fails open and records that no evaluator decision was available.
|
||||
policy:
|
||||
enabled: false
|
||||
flex_auth_url: http://127.0.0.1:8080
|
||||
fail_closed: true
|
||||
zone_registry_path: /path/to/compiled-flex-auth-registry.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open
|
||||
not-applicable: fail_closed
|
||||
```
|
||||
|
||||
### Bootstrapping the local CA key
|
||||
|
|
@ -85,11 +93,10 @@ vault:
|
|||
inventory_path: ~/.config/warden/inventory.yaml
|
||||
state_dir: ~/.local/state/warden
|
||||
|
||||
# Enable after flex-auth ssh-certificate policies are deployed:
|
||||
# Configure after flex-auth ssh-certificate policies are deployed:
|
||||
# policy:
|
||||
# enabled: true
|
||||
# flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
|
||||
# fail_closed: true
|
||||
# flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||
# zone_registry_path: /etc/warden/production_registry_snapshot.json
|
||||
```
|
||||
|
||||
### Example — in-cluster caller (pod or trusted host)
|
||||
|
|
@ -235,22 +242,36 @@ hosts:
|
|||
|
||||
---
|
||||
|
||||
## Policy gate (flex-auth, opt-in)
|
||||
## Policy gate (flex-auth, zone-aware)
|
||||
|
||||
When `policy.enabled: true`, `warden sign` and `warden issue` call flex-auth
|
||||
`POST /v1/check` before signing. Deny or unreachable (with `fail_closed: true`)
|
||||
blocks issuance. Allowed decisions store `policy_decision_id` in `signatures.log`.
|
||||
`warden sign` and `warden issue` evaluate flex-auth whenever a URL is configured.
|
||||
The target resource's compiled `security_zone` selects the local PEP failure
|
||||
mode. A rendered deny always blocks; evaluator failure blocks or proceeds per
|
||||
zone. Signing records `policy_decision_id` when present plus `policy_zone`,
|
||||
`policy_failure_mode`, and `policy_outcome`.
|
||||
|
||||
```yaml
|
||||
policy:
|
||||
enabled: false # default — no behavior change
|
||||
flex_auth_url: http://127.0.0.1:8080
|
||||
fail_closed: true # deny when flex-auth unreachable
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open
|
||||
not-applicable: fail_closed
|
||||
tenant: tenant:platform
|
||||
subject_env: WARDEN_POLICY_SUBJECT
|
||||
system: ops-warden
|
||||
```
|
||||
|
||||
`policy.enabled` and the global `policy.fail_closed` are retired; configuration
|
||||
loading rejects them with a migration error. Stance is owned by flex-auth's
|
||||
versioned policy package, not this block. The failure-mode map is PEP behavior
|
||||
for an unavailable or invalid evaluator.
|
||||
|
||||
Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
|
||||
|
||||
---
|
||||
|
|
@ -261,7 +282,7 @@ Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
|
|||
|----------|---------|-------------|
|
||||
| `WARDEN_CONFIG` | `~/.config/warden/warden.yaml` | Config file path |
|
||||
| `VAULT_TOKEN` | — | API token for `backend: vault` (OpenBao or Vault; name configurable via `vault.token_env`) |
|
||||
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks (when `policy.enabled`) |
|
||||
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -284,4 +305,4 @@ tunnels:
|
|||
`ops-bridge` runs `cert_command` before each SSH launch, captures stdout as the cert,
|
||||
and passes it alongside the private key via `ssh -i <key> -i <cert>`.
|
||||
See `wiki/CertCommandInterface.md` for the full contract and
|
||||
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.
|
||||
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.
|
||||
|
|
|
|||
|
|
@ -1,347 +1,196 @@
|
|||
# Policy-Gated SSH Signing
|
||||
# Zone-aware policy-gated signing
|
||||
|
||||
Date: 2026-06-23
|
||||
Status: **implemented (opt-in)** — WARDEN-WP-0007; policy package confirmed FLEX-WP-0006
|
||||
Ops-warden asks flex-auth for a decision before SSH certificate issuance. The
|
||||
gate is resource-scoped through security-zone membership; there is no repo-wide
|
||||
enable switch.
|
||||
|
||||
By default `warden sign` authorizes via **inventory allow-list** and TTL policy
|
||||
only. When `policy.enabled: true` in `warden.yaml`, ops-warden calls flex-auth
|
||||
before signing and records the decision id in `signatures.log`.
|
||||
Authority stays split:
|
||||
|
||||
---
|
||||
- flex-auth owns the versioned pre-sign stance (`enforced`, `advisory`, or
|
||||
`exempt`) and returns the decision;
|
||||
- ops-warden owns what the PEP does when flex-auth is unavailable or invalid;
|
||||
- the workload owner declares identity and zone membership;
|
||||
- zone-engine owns `security-zones_v0.1` admission and resolution semantics.
|
||||
|
||||
## Flow
|
||||
Binding decisions: `ADR-0009` (current) and `ADR-0006` (superseded rationale).
|
||||
|
||||
## Request path
|
||||
|
||||
```text
|
||||
warden sign <actor> --pubkey <path>
|
||||
|
|
||||
v
|
||||
Load actor from inventory (type, principals, ttl)
|
||||
|
|
||||
v
|
||||
policy.enabled?
|
||||
no -> skip
|
||||
yes -> flex-auth POST /v1/check
|
||||
|
|
||||
+-- DENY / unreachable (fail_closed) -> CAError
|
||||
|
|
||||
v ALLOW
|
||||
CABackend.sign() (local or OpenBao SSH engine)
|
||||
|
|
||||
v
|
||||
Append signatures.log (+ policy_decision_id when set)
|
||||
warden sign <actor>
|
||||
-> inventory, principal, actor-type, and TTL checks
|
||||
-> resource id ssh-cert:actor/<actor>
|
||||
-> read compiled security_zone for that resource
|
||||
-> POST flex-auth /v1/check with authenticated caller identity
|
||||
allow -> sign; record decision and zone evidence
|
||||
audit_only -> sign; record advisory decision and zone evidence
|
||||
deny -> refuse before the CA backend
|
||||
unavailable/invalid
|
||||
-> apply that zone's PEP failure mode
|
||||
-> record fail_open when issuance proceeds
|
||||
```
|
||||
|
||||
The same gate runs for `warden issue` (local backend only).
|
||||
The request contains actor id/type, requested principals, TTL, and a SHA-256
|
||||
fingerprint of the public key. It never contains a private key or secret value.
|
||||
|
||||
---
|
||||
## Compiled membership
|
||||
|
||||
## flex-auth request shape
|
||||
|
||||
| Field | Source |
|
||||
| --- | --- |
|
||||
| `subject.id` | `WARDEN_POLICY_SUBJECT` env var, or actor name |
|
||||
| `subject.type` | Actor type (`adm` / `agt` / `atm`) |
|
||||
| `tenant` | `policy.tenant` (default `tenant:platform`) |
|
||||
| `resource.id` | `ssh-cert:actor/<actor-name>` |
|
||||
| `resource.type` | `ssh-certificate` |
|
||||
| `action` | `sign` |
|
||||
| `context.principals` | From inventory |
|
||||
| `context.actor_type` | adm \| agt \| atm |
|
||||
| `context.pubkey_fingerprint` | SHA256 of pubkey text |
|
||||
| `context.ttl_hours` | Requested TTL |
|
||||
|
||||
flex-auth must return `effect: allow` and an `id` (or `request_id`) on allow.
|
||||
Deny responses include a `reason` surfaced in the CLI error.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
`scripts/build_flex_auth_registry.py` compiles inventory actor resources. Each
|
||||
actor carries an explicit `zone_subject`:
|
||||
|
||||
```yaml
|
||||
# warden.yaml — policy gate (opt-in, default off)
|
||||
policy:
|
||||
enabled: false
|
||||
flex_auth_url: http://127.0.0.1:8080
|
||||
fail_closed: true
|
||||
tenant: tenant:platform
|
||||
subject_env: WARDEN_POLICY_SUBJECT
|
||||
system: ops-warden
|
||||
caller_auth: # how ops-warden identifies itself — see "Caller identity"
|
||||
mode: none
|
||||
actors:
|
||||
agt-state-hub-bridge:
|
||||
type: agt
|
||||
principals: [agt-task-bridge]
|
||||
ttl_hours: 24
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: ops-bridge-tunnel
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | `false` | When `true`, call flex-auth before every sign/issue |
|
||||
| `flex_auth_url` | `http://127.0.0.1:8080` | flex-auth base URL |
|
||||
| `fail_closed` | `true` | Deny sign when flex-auth is unreachable or returns HTTP error |
|
||||
| `tenant` | `tenant:platform` | Tenant sent in subject and resource |
|
||||
| `subject_env` | `WARDEN_POLICY_SUBJECT` | Env var for IAM subject id override |
|
||||
| `system` | `ops-warden` | Resource system identifier |
|
||||
| `caller_auth.mode` | `none` | `none` \| `file` \| `env` \| `command` — source of the `Authorization` bearer token flex-auth TokenReviews ([Caller identity](#caller-identity-warden-wp-0031)) |
|
||||
The compiler consumes zone-engine's resolved view when available and emits
|
||||
resource attributes:
|
||||
|
||||
Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available.
|
||||
If unset, the actor name is used as subject id.
|
||||
|
||||
---
|
||||
|
||||
## Versioning
|
||||
|
||||
| Version | Gate | Status |
|
||||
| --- | --- | --- |
|
||||
| **v1** | Inventory + TTL max | Shipped |
|
||||
| **v2** | flex-auth opt-in via `policy.enabled` | Shipped (WP-0007) |
|
||||
| **v2.1** | Identity claims required for `adm` signs | Planned |
|
||||
| **v3** | Tenant-scoped policies per `tenant:*` | Planned |
|
||||
|
||||
---
|
||||
|
||||
## What stays in inventory
|
||||
|
||||
- Actor registration (name, type, default principals, default TTL)
|
||||
- Host reference documentation
|
||||
- Scorecard local checks
|
||||
|
||||
flex-auth decides **whether this sign request is allowed now**; inventory
|
||||
defines **what the actor is allowed to request**.
|
||||
|
||||
---
|
||||
|
||||
## flex-auth policy package (FLEX-WP-0006)
|
||||
|
||||
flex-auth owns the `ssh-certificate` / `sign` policy package. ops-warden consumes
|
||||
it via `POST /v1/check` when `policy.enabled: true`.
|
||||
|
||||
**Handoff (canonical):** `~/flex-auth/docs/ops-warden-policy-gate-handoff.md`
|
||||
|
||||
| Asset | flex-auth path |
|
||||
| --- | --- |
|
||||
| Policy package | `examples/ops-warden/policy_package.md` |
|
||||
| Allow/deny fixtures | `examples/ops-warden/policy_fixtures.yaml` |
|
||||
| Registry snapshot | `examples/ops-warden/registry_snapshot.json` |
|
||||
| Subject manifest | `examples/ops-warden/subject_manifest.yaml` |
|
||||
| Resource manifest | `examples/ops-warden/resource_manifest.yaml` |
|
||||
|
||||
### Tenant and subject bindings
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Tenant | `tenant:platform` (`policy.tenant`) |
|
||||
| Resource system | `ops-warden` (`policy.system`) |
|
||||
| Resource type | `ssh-certificate` |
|
||||
| Action | `sign` |
|
||||
| Resource id | `ssh-cert:actor/<actor-name>` |
|
||||
|
||||
| Actor type | Example flex-auth subject | ops-warden inventory name pattern |
|
||||
| --- | --- | --- |
|
||||
| `adm` | `platform-steward` | `adm-*` |
|
||||
| `agt` | `ci-deploy-agent` | `agt-*` |
|
||||
| `atm` | `backup-automation` | `atm-*` |
|
||||
|
||||
**Subject id sent to flex-auth:** `WARDEN_POLICY_SUBJECT` when set, otherwise the
|
||||
inventory actor name. flex-auth may also allow `iam:<actor-name>` when listed in
|
||||
`allowed_subjects` on the resource.
|
||||
|
||||
**Principals and TTL:** Taken from the sign request (inventory defaults). flex-auth
|
||||
denies when principals are empty/disallowed or TTL exceeds `max_ttl_hours` on the
|
||||
registered resource.
|
||||
|
||||
### Fixture coverage (flex-auth)
|
||||
|
||||
Allow: `fixture:ops-warden-adm-sign-allow`, `fixture:ops-warden-agt-sign-allow`,
|
||||
`fixture:ops-warden-atm-sign-allow`.
|
||||
|
||||
Deny: `fixture:ops-warden-unknown-subject-deny`,
|
||||
`fixture:ops-warden-actor-type-mismatch-deny`, `fixture:ops-warden-ttl-above-max-deny`,
|
||||
`fixture:ops-warden-disallowed-principal-deny`,
|
||||
`fixture:ops-warden-missing-fingerprint-deny`.
|
||||
|
||||
### Local smoke
|
||||
|
||||
```bash
|
||||
# flex-auth (from ~/flex-auth)
|
||||
flex-auth serve --addr 127.0.0.1:8080 \
|
||||
--registry examples/ops-warden/registry_snapshot.json \
|
||||
--policy examples/ops-warden/policy_package.md \
|
||||
--log /tmp/flex-auth-ops-warden-decisions.jsonl
|
||||
|
||||
# warden.yaml — policy.enabled: true, flex_auth_url pointing at flex-auth
|
||||
# Use an actor registered in the flex-auth registry (example fixtures use
|
||||
# template names; production needs a registry slice for real inventory actors).
|
||||
```json
|
||||
{
|
||||
"workload_id": "ops-bridge-tunnel",
|
||||
"security_zone": "z2-continuity",
|
||||
"security_zone_admission": "satisfied",
|
||||
"security_zone_revision": "sha256:..."
|
||||
}
|
||||
```
|
||||
|
||||
Local end-to-end evidence: `history/2026-06-23-flex-auth-policy-gate-local-smoke.md`.
|
||||
If the workload reference or resolved membership is absent, the resource says
|
||||
`security_zone: unknown` with a reason. A native non-workload actor/action says
|
||||
`security_zone_admission: not-applicable`. The compiler never parses a path or
|
||||
repository name to repair missing identity.
|
||||
|
||||
### Production registry from inventory
|
||||
`trust_zone: platform` was a dormant, unrelated field and is retired. It must
|
||||
not coexist with `security_zone` as a competing membership source.
|
||||
|
||||
Build a flex-auth registry snapshot that mirrors `inventory.yaml` actors:
|
||||
Build the snapshot:
|
||||
|
||||
```bash
|
||||
python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml \
|
||||
python3 scripts/build_flex_auth_registry.py \
|
||||
~/.config/warden/inventory.yaml \
|
||||
--zone-resolutions /path/to/zone-resolved-view.json \
|
||||
-o registry/flex-auth/production_registry_snapshot.json
|
||||
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json
|
||||
```
|
||||
|
||||
Re-run after adding or changing actors. Deploy the snapshot to the production
|
||||
flex-auth runtime together with `~/flex-auth/examples/ops-warden/policy_package.md`.
|
||||
Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not
|
||||
to a guessed zone.
|
||||
|
||||
Smoke (non-secret):
|
||||
## PEP failure modes
|
||||
|
||||
```bash
|
||||
./scripts/policy_gate_production_smoke.sh
|
||||
# OpenBao-backed — preferred: credential broker (no manual VAULT_TOKEN):
|
||||
cd ~/railiance-platform && make credential-exec-ops-warden-smoke
|
||||
# Manual fallback when broker unavailable:
|
||||
SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh
|
||||
```
|
||||
The initial build profile accepted by ops-warden is:
|
||||
|
||||
Evidence: `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`.
|
||||
| Zone/result | Dependency failure |
|
||||
| --- | --- |
|
||||
| `z0-experimental` | `fail_open` |
|
||||
| `z1-operational` | `fail_open` |
|
||||
| `z2-protected` | `fail_open` |
|
||||
| `z2-continuity` | `fail_open` |
|
||||
| `z3-critical` | `fail_closed` |
|
||||
| `unknown` | `fail_open` under the versioned build profile |
|
||||
| `not-applicable` | `fail_closed` for this pre-sign PEP |
|
||||
|
||||
---
|
||||
These are dependency failure modes, not policy stance. A rendered deny always
|
||||
blocks. The `unknown` row does not grant membership or an exception; it is the
|
||||
explicit build-stage treatment until authoritative declarations land.
|
||||
|
||||
## Caller identity (WARDEN-WP-0031)
|
||||
|
||||
flex-auth authenticates the **caller** before it evaluates the request. The
|
||||
in-cluster pin `flex-auth-ops-warden` (FLEX-WP-0016) passes the bearer token to a
|
||||
Kubernetes TokenReview and requires the principal
|
||||
`system:serviceaccount:ops-warden:ops-warden` for any request carrying
|
||||
`resource.system: ops-warden`.
|
||||
|
||||
Until ops-warden sends that header the pin logs
|
||||
|
||||
```
|
||||
caller authentication warning: caller is not authenticated
|
||||
```
|
||||
|
||||
and can only run `callerAuth.mode: warn`. Per ADHOC-2026-08-17-T01,
|
||||
`policy.enabled` must not flip while `/v1/check` still answers unauthenticated
|
||||
callers — so **the missing header is what blocks the flip**, and it is
|
||||
ops-warden's to fix, not flex-auth's.
|
||||
|
||||
### Configure a token source
|
||||
Configuration:
|
||||
|
||||
```yaml
|
||||
policy:
|
||||
flex_auth_url: http://127.0.0.1:19090
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open
|
||||
not-applicable: fail_closed
|
||||
caller_auth:
|
||||
mode: none # none | file | env | command
|
||||
token_path: /var/run/secrets/flex-auth/token # mode: file
|
||||
token_env: WARDEN_POLICY_CALLER_TOKEN # mode: env
|
||||
command: kubectl create token ops-warden -n ops-warden --audience flex-auth --duration 10m
|
||||
audience: flex-auth
|
||||
mode: command
|
||||
command:
|
||||
- kubectl
|
||||
- create
|
||||
- token
|
||||
- ops-warden
|
||||
- -n
|
||||
- ops-warden
|
||||
- --audience
|
||||
- flex-auth
|
||||
- --duration
|
||||
- 10m
|
||||
```
|
||||
|
||||
| Mode | Use it when |
|
||||
| --- | --- |
|
||||
| `none` | Default. No header — pre-FLEX-WP-0016 behaviour, only viable while the pin is in `warn` |
|
||||
| `file` | In-cluster PEP with a projected, audience-bound ServiceAccount token |
|
||||
| `env` | The token is already in the environment (CI, a wrapper) |
|
||||
| `command` | Workstation `warden sign` — mint a short-lived bound token per call |
|
||||
`policy.enabled` and the global `policy.fail_closed` are retired. The loader
|
||||
rejects either key so old and new controls cannot coexist as two sources of
|
||||
truth.
|
||||
|
||||
A workstation is not a ServiceAccount, which is why `command` exists. The token is
|
||||
read, sent, and dropped: never cached, written, or logged (ADR-0002). If a token
|
||||
is configured but cannot be obtained, the sign is **refused** under
|
||||
`fail_closed` — falling back to an anonymous call would defeat the gate.
|
||||
## Caller identity
|
||||
|
||||
### Readiness gate
|
||||
The production flex-auth pin authenticates ops-warden with Kubernetes
|
||||
TokenReview and binds `resource.system: ops-warden` to
|
||||
`system:serviceaccount:ops-warden:ops-warden`. Supported token sources are:
|
||||
|
||||
- `file` — projected ServiceAccount token for an in-cluster PEP;
|
||||
- `command` — short-lived `kubectl create token` on a workstation;
|
||||
- `env` — attended fallback;
|
||||
- `none` — no identity header; only useful for an intentionally unauthenticated
|
||||
development evaluator.
|
||||
|
||||
Tokens are resolved per call, never cached, logged, or echoed. Under a
|
||||
fail-closed zone, an unavailable caller token blocks. Under a fail-open zone it
|
||||
becomes a recorded evaluator failure; ops-warden never retries anonymously with
|
||||
a secret copied into its own state.
|
||||
|
||||
Re-establish the value-safe caller proof:
|
||||
|
||||
```bash
|
||||
python scripts/check_policy_caller_identity.py # offline
|
||||
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090 # port-forward of the warn pin
|
||||
python3 scripts/check_policy_caller_identity.py \
|
||||
--url http://127.0.0.1:19090
|
||||
```
|
||||
|
||||
Exit 0 ready / 1 not ready / 2 bad input. It prints the token's length and a
|
||||
truncated fingerprint, never the value, so its output is safe to paste into a
|
||||
handoff message. A live `401` means the token was sent but rejected (audience or
|
||||
binding); `403` means it authenticated but may not represent `system: ops-warden`.
|
||||
Expected evidence is HTTP 200 with a decision id and anonymous HTTP 401 on the
|
||||
enforcing pin. The script reports only token length and a truncated fingerprint.
|
||||
|
||||
### Enablement is deferred by decision (ADR-0006)
|
||||
## Audit evidence
|
||||
|
||||
`policy.enabled` is **false, and that is a decision rather than a blocker.** The
|
||||
gate is ready: flex-auth's pin runs `callerAuth.mode: enforce`, the readiness
|
||||
gate exits 0 against it, and an anonymous `/v1/check` returns 401.
|
||||
Successful signing records:
|
||||
|
||||
It is not enabled because `policy.enabled` is one boolean over the whole repo,
|
||||
and with `fail_closed: true` it makes flex-auth a hard dependency of every
|
||||
`warden sign` — including the certs the ops-bridge tunnels depend on, one of
|
||||
which carries the policy call. Uniform enforcement across an estate under active
|
||||
refactor hardens the access needed to perform the refactor.
|
||||
- `policy_decision_id` when flex-auth returned one;
|
||||
- `policy_zone`;
|
||||
- `policy_failure_mode`;
|
||||
- `policy_outcome` (`allow`, `audit_only`, or `fail_open`).
|
||||
|
||||
`ADR-0006` scopes enforcement to security zones; `WARDEN-WP-0032` defines them.
|
||||
Do not set `policy.enabled: true` outside that model — a zone-blind enablement
|
||||
is out of order under the ADR, not merely inadvisable.
|
||||
A fail-open result must therefore be visible rather than indistinguishable from
|
||||
an unevaluated request. Denies do not reach the CA backend and produce no
|
||||
certificate.
|
||||
|
||||
The sequence below is retained as the *mechanics*, for whoever enables the gate
|
||||
for a zone once zones exist.
|
||||
## Rollout and rollback
|
||||
|
||||
### Flip sequence
|
||||
1. Validate `tenancy.yaml` and the workload declarations referenced by actor
|
||||
`zone_subject` entries.
|
||||
2. Compile the registry and inspect unknown/not-applicable results.
|
||||
3. Run `scripts/check_policy_caller_identity.py` against the enforcing pin.
|
||||
4. Deploy the same compiled registry revision and matching flex-auth policy
|
||||
package.
|
||||
5. Smoke an allow/advisory path, an enforced deny, and evaluator loss for one
|
||||
fail-open and one fail-closed zone.
|
||||
|
||||
1. Configure `caller_auth`; `check_policy_caller_identity.py` exits 0 offline.
|
||||
2. Smoke against a port-forward of the warn pin. The evidence is the **absence**
|
||||
of `caller authentication warning` in its log, not the `allow` — warn serves
|
||||
unauthenticated callers too, so an allow alone proves nothing.
|
||||
3. Ask flex-auth to set `callerAuth.mode: enforce` (their FLEX-WP-0016 T03).
|
||||
4. Re-run the gate against the enforcing pin.
|
||||
5. Only then: `policy.enabled: true`, `fail_closed: true`.
|
||||
|
||||
Reversing 3 and 5 401s every `warden sign`.
|
||||
|
||||
---
|
||||
|
||||
## Production rollout
|
||||
|
||||
**Keep `policy.enabled: false` until flex-auth is reachable** at `policy.flex_auth_url`
|
||||
with `fail_closed: true`, unreachable flex-auth blocks all signs.
|
||||
|
||||
### Operator checklist
|
||||
|
||||
| Step | Owner | Action |
|
||||
| --- | --- | --- |
|
||||
| 1 | flex-auth | Deploy runtime; confirm `curl <flex_auth_url>/healthz` → 200 (**FLEX-WP-0007**) |
|
||||
| 2 | flex-auth | Load production registry + policy package (`~/flex-auth/examples/ops-warden/`) |
|
||||
| 3 | ops-warden | Regenerate registry from inventory: `scripts/build_flex_auth_registry.py` |
|
||||
| 4 | ops-warden | Local smoke: `./scripts/policy_gate_production_smoke.sh` |
|
||||
| 5 | operator | Vault smoke: `make credential-exec-ops-warden-smoke` in `railiance-platform` (or manual `SMOKE_VAULT=1` fallback) |
|
||||
| 6 | operator | Set `policy.flex_auth_url` in `~/.config/warden/warden.yaml` — the pin is `flex-auth-ops-warden.flex-auth.svc.cluster.local:8080`, reached from a workstation via port-forward or tunnel |
|
||||
| 6a | ops-warden | Configure `policy.caller_auth`; `scripts/check_policy_caller_identity.py` exits 0 (see **Caller identity**) |
|
||||
| 6b | flex-auth | Set `callerAuth.mode: enforce` on the ops-warden pin (FLEX-WP-0016 T03) |
|
||||
| 7 | operator | Set `policy.enabled: true`; keep `fail_closed: true` |
|
||||
| 8 | operator | Allow smoke: `warden sign <actor>` — `signatures.log` has `policy_decision_id` |
|
||||
| 9 | operator | Deny smoke: e.g. `--ttl` above max — CLI shows flex-auth `reason`, no cert |
|
||||
|
||||
Cross-repo references:
|
||||
|
||||
- `~/flex-auth/workplans/FLEX-WP-0007-ops-warden-policy-gate-production-deployment.md`
|
||||
- `history/2026-06-23-flex-auth-production-pickup-suggestion.md`
|
||||
- `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`
|
||||
|
||||
### Summary
|
||||
|
||||
1. Deploy the flex-auth registry and policy package to the production flex-auth
|
||||
runtime — **not** only the example fixtures.
|
||||
2. Set `policy.flex_auth_url` to the production flex-auth base URL.
|
||||
3. Enable `policy.enabled: true` only after steps 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.
|
||||
|
||||
---
|
||||
Rollback is a versioned profile or registry rollback. Do not reintroduce
|
||||
`policy.enabled: false`: that would erase per-zone evidence and recreate the
|
||||
global control ADR-0009 supersedes.
|
||||
|
||||
## See also
|
||||
|
||||
- `wiki/OpsWardenConfig.md` — full config reference
|
||||
- `wiki/CredentialRouting.md`
|
||||
- `~/flex-auth/docs/ops-warden-policy-gate-handoff.md` — flex-auth handoff
|
||||
- `flex-auth/INTENT.md`
|
||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
||||
- `tenancy.yaml`
|
||||
- `docs/evidence/security-zone-admission-2026-08-22.md`
|
||||
- `wiki/OpsWardenConfig.md`
|
||||
- `wiki/WorkloadSecurityPosture.md`
|
||||
- `history/2026-08-19-flex-auth-caller-identity-evidence.md`
|
||||
|
|
|
|||
|
|
@ -111,6 +111,28 @@ This is the practical bridge from WARDEN-WP-0014 (`warden access`) to WP-0015: a
|
|||
assist can remove manual secret handling friction, while posture/maturity decides
|
||||
whether the secret may flow at all.
|
||||
|
||||
## Security-zone consumer contract
|
||||
|
||||
Security zones are now the prescriptive sibling of this descriptive posture
|
||||
model. `zone-engine` owns `security-zones_v0.1`; ops-warden follows it as a
|
||||
consumer under `ADR-0009`.
|
||||
|
||||
- Workload maturity and data classification are admission evidence, not zone
|
||||
names and not control stance.
|
||||
- The workload owner declares authoritative `workload_identity` and `zones:` in
|
||||
the same `tenancy.yaml` service entry.
|
||||
- Credential lanes and actor resources reference that workload explicitly.
|
||||
Managed deployables use Repo Manager's exact `(rapp_id, name, deployable?)`
|
||||
tuple; independent operational workloads reference their owner declaration.
|
||||
- Missing identity, membership, or evidence resolves `unknown`. No compiler may
|
||||
infer it from a path, repository owner, actor class, or environment.
|
||||
- flex-auth owns pre-sign stance; ops-warden owns PEP dependency failure behavior,
|
||||
the agent read boundary, and `warden plan` escalation behavior.
|
||||
|
||||
Ops-warden declares `z1-operational` with M1/internal/medium evidence. That is an
|
||||
accuracy statement, not a target: M2 remains unavailable until SLO history,
|
||||
on-call, and incident/recovery evidence exist.
|
||||
|
||||
---
|
||||
|
||||
## Canon layering (where each part lands)
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ When `WARDEN_AGENT_ID` is set and the lane is `risk: high`, `warden access --fet
|
|||
|
||||
```bash
|
||||
export WARDEN_AGENT_ID=grok # or claude, codex
|
||||
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
|
||||
warden access railiance-backup-offsite-lane --no-policy --wrap
|
||||
warden access railiance-backup-offsite-lane --no-policy --fingerprint
|
||||
warden access railiance-backup-offsite-lane --out /tmp/nc.token
|
||||
warden access railiance-backup-offsite-lane --wrap
|
||||
warden access railiance-backup-offsite-lane --fingerprint
|
||||
```
|
||||
|
||||
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ Config (host/port/env names only):
|
|||
2. Fetch via sanctioned transport (never paste into chat):
|
||||
|
||||
```bash
|
||||
warden access binky-company-email-imap --all --no-policy --out /tmp/imap.user
|
||||
warden access binky-company-email-imap --all --out /tmp/imap.user
|
||||
# primary field is IMAP_USERNAME; for password use --field after template support
|
||||
# or:
|
||||
warden access binky-company-email-imap --all --no-policy --exec -- \
|
||||
warden access binky-company-email-imap --all --exec -- \
|
||||
env IMAP_USERNAME=… # prefer secrets-engine / dual-field exec when wired
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ Design: `binky-control/integrations/qonto-mcp.md`
|
|||
|
||||
```bash
|
||||
# lengths / presence only when debugging
|
||||
warden access binky-qonto-api --all --no-policy --exec -- \
|
||||
warden access binky-qonto-api --all --exec -- \
|
||||
sh -c 'export QONTO_API_KEY="$API_KEY" QONTO_ORGANIZATION_ID="$API_USER";
|
||||
# then: qonto-mcp-server or curl thirdparty with Authorization login:key
|
||||
:'
|
||||
|
|
|
|||
|
|
@ -122,10 +122,11 @@ bridge up state-hub-railiance01
|
|||
- [ ] Tunnel establishes without static cert file on disk
|
||||
- [ ] Re-run `bridge up` after cert TTL expires — `cert_command` re-issues automatically
|
||||
|
||||
### 5. Policy gate (optional, after FLEX-WP-0007)
|
||||
### 5. Zone-aware policy evidence
|
||||
|
||||
When `policy.enabled: true`, confirm `signatures.log` includes `policy_decision_id`
|
||||
on tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
|
||||
Confirm `signatures.log` records `policy_zone`, `policy_failure_mode`,
|
||||
`policy_outcome`, and `policy_decision_id` when flex-auth returns a decision on
|
||||
tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -177,4 +178,4 @@ starting cutover (WARDEN-WP-0023).
|
|||
- `wiki/OpsWardenConfig.md` — cert_command example
|
||||
- `wiki/playbooks/operator-openbao-token-hygiene.md`
|
||||
- `wiki/AuditTrail.md` — query recent signs via `warden activity`
|
||||
- `warden route show ops-bridge-tunnel --json`
|
||||
- `warden route show ops-bridge-tunnel --json`
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
|
||||
```bash
|
||||
# Preferred: file or exec injection via warden
|
||||
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
|
||||
warden access railiance-backup-offsite-lane --out /tmp/nc.token
|
||||
# or:
|
||||
warden access railiance-backup-offsite-lane --no-policy --exec -- env | grep -v .
|
||||
warden access railiance-backup-offsite-lane --exec -- env | grep -v .
|
||||
```
|
||||
|
||||
If you must use raw bao in an interactive human shell (not an agent session):
|
||||
|
|
@ -54,7 +54,7 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
3. **Proxy via warden access** (catalog active + resolvable):
|
||||
|
||||
```bash
|
||||
warden access railiance-backup-offsite-lane --no-policy --fetch --out /tmp/nc.token
|
||||
warden access railiance-backup-offsite-lane --fetch --out /tmp/nc.token
|
||||
# Primary field is NC_WEBDAV_TOKEN. AGE_PRIVATE_KEY is recovery escrow only.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ Never paste values into chat, State Hub, workplans, or Git.
|
|||
3. **Or proxy via warden access** (same `bao kv get`, audited metadata only):
|
||||
```bash
|
||||
warden route show reuse-surface-hub-write-token --json
|
||||
warden access reuse-surface-hub-write-token --no-policy --fetch
|
||||
warden access reuse-surface-hub-write-token --fetch
|
||||
```
|
||||
|
||||
4. **Webhook HMAC** (same path, second field — must match Forgejo org webhook):
|
||||
|
|
@ -87,4 +87,4 @@ Never paste values into chat, State Hub, workplans, or Git.
|
|||
Rotation: `railiance-platform/docs/reuse-surface-runtime-secrets-rotation-runbook.md`
|
||||
(OpenBao patch → ESO `force-sync` → hub rollout → `make reuse-forgejo-webhook` when
|
||||
the webhook HMAC changes → `make reuse-webhook-smoke`). Lifecycle:
|
||||
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).
|
||||
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ First worked lane: `tenants/binky/company-email/imap` (CCR-2026-0007).
|
|||
|
||||
```bash
|
||||
bao login -method=oidc -path=netkingdom role=<lane>-workload-kv-read
|
||||
warden access <catalog-id> --no-policy --out /tmp/secret.file # mode 0600
|
||||
warden access <catalog-id> --out /tmp/secret.file # mode 0600
|
||||
# agents: never raw --fetch stream; WARDEN_AGENT_ID + risk=high → exit 7
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -48,11 +48,10 @@ this token** — it is the access front door: `warden access` proxies the read f
|
|||
**ops-warden transparent fallback** — same lane via the `warden access` proxy (fetches as
|
||||
you, holds nothing). Field-verified flags (whynot-design, @whynot/design@0.4.0):
|
||||
```bash
|
||||
# --exec needs the env-var name; --no-policy is required while the gate is advisory
|
||||
# (policy.enabled=false), else the call exits 4.
|
||||
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN \
|
||||
# --exec needs the env-var name. The zone-aware policy gate always runs first.
|
||||
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
|
||||
--exec -- npm publish
|
||||
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN --fetch
|
||||
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch
|
||||
```
|
||||
On either path the value transits to you (or the child env) and never enters
|
||||
ops-warden's memory, disk, or audit log.
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ type: workplan
|
|||
title: "Adopt security zones as a consumer — retire the global policy.enabled"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: ops-warden
|
||||
topic_slug: netkingdom
|
||||
planning_priority: P1
|
||||
depends_on_workplans:
|
||||
- WARDEN-WP-0031
|
||||
created: "2026-08-19"
|
||||
updated: "2026-08-19"
|
||||
updated: "2026-08-22"
|
||||
state_hub_workstream_id: "f1b6cdcb-5e16-48ab-a72b-339e909e92f0"
|
||||
---
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ this repo's own input rather than out of supplying a new one.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T02
|
||||
status: wait
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "08735f17-fe7e-4a6a-9b71-f350f98c30b1"
|
||||
```
|
||||
|
|
@ -144,9 +144,39 @@ Good news for T03's cost: **no flex-auth registry schema change is required.**
|
|||
`metadata`, `labels` and `attributes` already flatten into the rego input, so the
|
||||
compiler can emit membership today.
|
||||
|
||||
**Consumer implementation landed in the working tree 2026-08-22.** The config
|
||||
loader rejects the retired `policy.enabled` and global `policy.fail_closed`
|
||||
keys. `scripts/build_flex_auth_registry.py` now emits the normative
|
||||
`workload_id`, `security_zone`, `security_zone_declared`,
|
||||
`security_zone_admission`, `security_zone_reason`, and
|
||||
`security_zone_revision` attributes; the dormant `trust_zone` field is gone.
|
||||
The PEP reads compiled resource membership and applies its local per-zone
|
||||
failure mode. Sign and audit records retain the zone, selected failure mode,
|
||||
outcome, and decision id.
|
||||
|
||||
The legacy `--no-policy` proxy switch remains only as a rejected compatibility
|
||||
flag. It can no longer bypass the gate: unresolved credential targets take the
|
||||
explicit `unknown` profile and its configured failure mode.
|
||||
|
||||
**Done 2026-08-22.** flex-auth package v2 (commit `e521e7b`) supplies total
|
||||
stance over every v0.1 zone plus `unknown`, preserves native enforcement for
|
||||
`not-applicable`, and returns `audit_only` for advisory decisions. The required
|
||||
live caller check passed through the existing tunnel with command-mode caller
|
||||
identity and decision `decision:f3f7c88f9585582a`; the check used a temporary
|
||||
migrated copy because the operator's persistent config still carries the two
|
||||
retired keys. Full repo tests pass and sign/audit evidence records zone,
|
||||
failure mode, outcome, and decision id.
|
||||
|
||||
The required readiness recheck was run on 2026-08-22 after `warden plan`
|
||||
returned `autonomous`. It correctly returned **NOT READY** before obtaining or
|
||||
printing any token because the active operator `warden.yaml` still contains
|
||||
the two retired global keys. Operator configuration migration is therefore an
|
||||
explicit remaining acceptance step, followed by the value-safe live
|
||||
`/v1/check` smoke. Do not ask flex-auth to enforce caller authentication yet.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T03
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "fd642377-8d64-473c-b684-f8f1a842c223"
|
||||
```
|
||||
|
|
@ -183,9 +213,16 @@ carries membership. Carry the conformance rule:
|
|||
*accuracy, not altitude*. Declaring a stricter zone than can be evidenced is the
|
||||
failure mode that looks like progress.
|
||||
|
||||
**Done 2026-08-22.** `tenancy.yaml` now declares ops-warden as an independently
|
||||
governed operational execution unit with its Kubernetes service-account
|
||||
binding, and admits it to `z1-operational` at `M1`, `medium`, `internal`.
|
||||
`docs/evidence/security-zone-admission-2026-08-22.md` records why this is the
|
||||
highest evidenced admission rather than an aspirational `M2`. The declaration
|
||||
passes net-kingdom's current `tenancy-posture_v0.1` validator.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T04
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
|
||||
```
|
||||
|
|
@ -195,6 +232,13 @@ state_hub_task_id: "1af3d26e-ce21-46ca-a56c-486d52b76dcb"
|
|||
owning it — a superseding record, never an in-place edit. Update `SCOPE.md`,
|
||||
`wiki/WorkloadSecurityPosture.md`, and `wiki/PolicyGatedSigning.md` with it.
|
||||
|
||||
**Done 2026-08-22.** ADR-0006 is marked superseded without rewriting its
|
||||
decision. ADR-0009 accepts `security-zones_v0.1` as a consumer, records the
|
||||
PDP/PEP split and unknown handling, and is indexed as its successor. `SCOPE.md`,
|
||||
`wiki/WorkloadSecurityPosture.md`, `wiki/PolicyGatedSigning.md`, configuration
|
||||
guidance, access guidance, and affected playbooks now describe the zone-aware
|
||||
control and do not instruct operators to use a global bypass.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T05
|
||||
status: done
|
||||
|
|
@ -241,7 +285,7 @@ policy covers these paths (T06).
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T06
|
||||
status: progress
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
|
||||
```
|
||||
|
|
@ -330,8 +374,53 @@ with a `workload-kv-read-*` policy. Confirming that means minting or inspecting
|
|||
tokens — a write against railiance-platform's OpenBao. Exposure stays theoretical
|
||||
to the same degree as before.
|
||||
|
||||
**Still open:** whether the maturity-derived default replaces the `ungraded`
|
||||
sentinel entirely (waits on `ZONE-WP-0001-T03`).
|
||||
**Platform half closed 2026-08-22.** `RAILIANCE-WP-0022` consumed the generated
|
||||
high-risk path input and deployed the completed deny policy. The platform-owned
|
||||
invariants report **19 high-risk lanes, 14 concrete entries, 0 uncovered**. A
|
||||
dedicated `coding-agent-railiance-platform` AppRole then proved deny-wins with
|
||||
exactly `agent-high-risk-boundary` plus one workload read policy: data access was
|
||||
denied while metadata remained readable; its single-use test identity was
|
||||
self-revoked and no test tokens remained. Decision `f0955252` records the
|
||||
operational identity. No KV value was read.
|
||||
|
||||
The local invariant agrees against the platform policy file: 19 high-risk lanes,
|
||||
14 concrete entries covered, 5 without a concrete address, 0 uncovered. This
|
||||
session could not independently read the deployed policy, so live-state evidence
|
||||
is the platform-owned readback above rather than a claim inferred from the local
|
||||
file.
|
||||
|
||||
**Done 2026-08-22.** The settled model makes the two layers complementary:
|
||||
catalog CI continues to require an explicit `risk` field so omission cannot
|
||||
silently weaken the agent boundary, while `RouteEntry.risk_for_zone` implements
|
||||
the normative maturity-derived fallback for consumers evaluating unresolved or
|
||||
future inputs (`z0` + satisfied + synthetic-only → `standard`; satisfied `z3`
|
||||
→ `critical`; everything else → `high`). An explicit catalog grade always wins.
|
||||
The platform deny policy and its live proof above close the direct-OpenBao half.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T07
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "6b8c8181-978c-4b0a-ac7b-2cc9ad57b3ba"
|
||||
```
|
||||
|
||||
**Adopt Repo Manager's workload-reference owner interface.** Accepted with one
|
||||
documented amendment against repo-manager revision `890f3b0`: managed
|
||||
deployables use the exact `(rapp_id, workload_identity.name)` pair and optional
|
||||
deployable name; an independently governed operational execution unit may use
|
||||
an exact local `tenancy.yaml` declaration. Native actions, actors, grants,
|
||||
patterns, and non-workload resources are explicitly `not-applicable`; unresolved
|
||||
workload-applicable references remain `unknown`. No path, owner name, or
|
||||
repository-name inference is allowed.
|
||||
|
||||
Every one of the 27 catalog lanes now carries an explicit `workload_ref`: 3
|
||||
resolved (2 exact managed RAPP references and ops-warden's direct operational
|
||||
declaration), 17 unknown with reasons, and 7 not-applicable with reasons.
|
||||
`scripts/report_workload_join.py` resolves only those explicit references,
|
||||
Repo Manager validates the interface and resolves both managed tuples exactly,
|
||||
and regression tests reject malformed or inferred joins. This incorporates
|
||||
net-kingdom's draft-12 correction received 2026-08-22: local tenancy declaration
|
||||
is not an escape hatch for a managed running deployable.
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue