ops-warden/wiki/PolicyGatedSigning.md
tegwick b6b9b95616 WARDEN-WP-0040-T02: compile activity-core's backup-daily declaration
Commit zone-engine's resolved view of activity-core's backup-daily
declaration and rebuild the flex-auth snapshot from it. atm-backup-daily
now resolves to z2-protected declared, admission unsatisfied (M1 below
the M2 floor), and effective zone unknown. Signing-target coverage is
unchanged at 0/3. Also regenerate the high-risk-paths artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 327285@bnt-lap001
Assistant-Session: d6532cd1-7cd5-417b-baac-b841c9103a6b
2026-10-09 00:13:47 +02:00

7.4 KiB

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

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 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:

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:

{
  "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:

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

The committed snapshot is built from examples/inventory.seed.yaml with --zone-resolutions registry/flex-auth/zone-resolutions.json, which is zone-engine's tools/resolve_zones.py output over owner declarations (today: activity-core declarations/backup-daily/tenancy.yaml). Run the resolver from the estate root so source paths stay relative.

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:

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:

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.

Approval consumption (not wired)

This lane does not yet consume an approval artifact. The corrections already received about how it must validate when it does — two artifacts never taken from each other, the corrected binding.approval_binding_digest comparison target, and the attribution gap that digest matching does not discharge — are held in wiki/ApprovalConsumption.md so they are not lost between now and then.

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