# Zone-aware policy-gated signing 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. 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. Binding decisions: `ADR-0009` (current) and `ADR-0006` (superseded rationale). ## Request path ```text warden sign -> inventory, principal, actor-type, and TTL checks -> resource id ssh-cert: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 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 `scripts/build_flex_auth_registry.py` compiles inventory actor resources. Each actor carries an explicit `zone_subject`: ```yaml actors: agt-state-hub-bridge: type: agt principals: [agt-task-bridge] ttl_hours: 24 zone_subject: applicability: applicable workload_id: ops-bridge-tunnel ``` The compiler consumes zone-engine's resolved view when available and emits resource attributes: ```json { "workload_id": "ops-bridge-tunnel", "security_zone": "z2-continuity", "security_zone_admission": "satisfied", "security_zone_revision": "sha256:..." } ``` 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. `trust_zone: platform` was a dormant, unrelated field and is retired. It must not coexist with `security_zone` as a competing membership source. Build the snapshot: ```bash 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 ``` Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not to a guessed zone. ## PEP failure modes The initial build profile accepted by ops-warden is: | 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. 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: command command: - kubectl - create - token - ops-warden - -n - ops-warden - --audience - flex-auth - --duration - 10m ``` `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. ## Caller identity An HTTP 401 or 403 from the policy service refuses the operation under every outage profile. It is an explicit caller-authentication or system-binding refusal, not evaluator unavailability. Resolve the admitted caller and resource contract; do not retry with a different resource owner or disable caller enforcement. 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 python3 scripts/check_policy_caller_identity.py \ --url http://127.0.0.1:19090 ``` 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. ## Audit evidence Successful signing records: - `policy_decision_id` when flex-auth returned one; - `policy_zone`; - `policy_failure_mode`; - `policy_outcome` (`allow`, `audit_only`, or `fail_open`). 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. ## Rollout and rollback 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. 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 - `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`