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

196 lines
6.3 KiB
Markdown

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