gate-house ruled the v0.8 assent round (GH-DEC-2026-011, net-kingdom@64394e9): ask 1 declined, ask 2 adopted. Ask 1's refusal is accepted without reservation and the reason is better than the ask -- a sanctioned transitional fail_open is indistinguishable at runtime from the stance the rule forbids, and would make the rule optional at the only moment it costs anything. Ask 2 gave §13.1 a Coverage column with this repo's figures as its first entries. Since we asked for the column, we owe it accuracy: scripts/report_coverage.py measures both populations from the artifacts the runtime uses (reusing the workload-join build rather than re-deriving it), and a test asserts pep-stance.yaml's published block equals what it measures. A hand-counted number in a register that explicitly does not recompute it decays silently, and a stale figure beside a marked cell is worse than the blank the other four rows carry. pep-stance.yaml marks the unknown cell inline as a declared gap -- assent, the measured reason for not flipping, the declined ask, WARDEN-WP-0040 as route -- and a second test keeps it marked while it is fail_open, failing when it is flipped. standard_version stays 0.7 because that is what binds; v0.8 is proposed, so it gains standard_version_reviewed rather than pre-adopting. Separately, gate-house corrected GH-DEC-2026-008: the claim/decision digest comparison it originally required is unimplementable and a fail-closed consumer obeying it would have denied permanently. We had never copied the wording, so nothing to unwind -- but everything they have sent about this lane was living in an inbox thread, a bad home for a correction that only matters when someone finally wires the consume. Now wiki/ApprovalConsumption.md, leading with "nothing is wired", carrying the corrected target and the attribution gap that digest matching does not discharge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EPuTc18FjU5WFqoSEKH3C Assistant: claude-code Assistant-Model: opus Assistant-Process: 1276224@bnt-lap001 Assistant-Session: 426ec497-e1c4-4dd3-b417-dfce1ca1dbc3
209 lines
7 KiB
Markdown
209 lines
7 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
|
|
|
|
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.
|
|
|
|
## 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`
|