ops-warden/wiki/PolicyGatedSigning.md
tegwick 7ce58ae638
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: adopt security zones and explicit workload refs
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:36:37 +02:00

6.3 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

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

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.

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