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
7 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, orexempt) 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.1admission 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
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-livedkubectl create tokenon 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_idwhen flex-auth returned one;policy_zone;policy_failure_mode;policy_outcome(allow,audit_only, orfail_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
- Validate
tenancy.yamland the workload declarations referenced by actorzone_subjectentries. - Compile the registry and inspect unknown/not-applicable results.
- Run
scripts/check_policy_caller_identity.pyagainst the enforcing pin. - Deploy the same compiled registry revision and matching flex-auth policy package.
- 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.yamldocs/evidence/security-zone-admission-2026-08-22.mdwiki/OpsWardenConfig.mdwiki/WorkloadSecurityPosture.mdhistory/2026-08-19-flex-auth-caller-identity-evidence.md