ops-warden/wiki/PolicyGatedSigning.md
tegwick 0a331413a2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Send a caller identity to flex-auth so policy.enabled can flip
flex-auth's flex-auth-ops-warden pin (FLEX-WP-0016) TokenReviews the caller and
binds resource.system: ops-warden to system:serviceaccount:ops-warden:ops-warden.
policy.py posted /v1/check with no Authorization header, so the pin logs
"caller authentication warning" and can only run callerAuth.mode: warn — which,
under ADHOC-2026-08-17-T01, is exactly what blocks policy.enabled: true.

- policy.caller_auth (none | file | env | command) + src/warden/caller_identity.py:
  token resolved per call, never cached, written, or logged (ADR-0002)
- both check_sign_policy and check_fetch_policy attach the bearer header; an
  unobtainable token fails closed rather than retrying anonymously
- scripts/check_policy_caller_identity.py: read-only gate, prints length and a
  truncated fingerprint only, distinguishes 401 (audience/binding) from 403
- example config: caller_auth block, and flex_auth_url corrected — it pointed at
  flex-auth.flex-auth.svc, a Service that does not exist
- WARDEN-WP-0031, PolicyGatedSigning caller-identity section and flip sequence

Default stays mode: none, so behaviour is unchanged until an operator opts in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:08:34 +02:00

12 KiB
Raw Blame History

Policy-Gated SSH Signing

Date: 2026-06-23
Status: implemented (opt-in) — WARDEN-WP-0007; policy package confirmed FLEX-WP-0006

By default warden sign authorizes via inventory allow-list and TTL policy only. When policy.enabled: true in warden.yaml, ops-warden calls flex-auth before signing and records the decision id in signatures.log.


Flow

warden sign <actor> --pubkey <path>
        |
        v
Load actor from inventory (type, principals, ttl)
        |
        v
policy.enabled?
  no  -> skip
  yes -> flex-auth POST /v1/check
        |
        +-- DENY / unreachable (fail_closed) -> CAError
        |
        v ALLOW
CABackend.sign()  (local or OpenBao SSH engine)
        |
        v
Append signatures.log (+ policy_decision_id when set)

The same gate runs for warden issue (local backend only).


flex-auth request shape

Field Source
subject.id WARDEN_POLICY_SUBJECT env var, or actor name
subject.type Actor type (adm / agt / atm)
tenant policy.tenant (default tenant:platform)
resource.id ssh-cert:actor/<actor-name>
resource.type ssh-certificate
action sign
context.principals From inventory
context.actor_type adm | agt | atm
context.pubkey_fingerprint SHA256 of pubkey text
context.ttl_hours Requested TTL

flex-auth must return effect: allow and an id (or request_id) on allow. Deny responses include a reason surfaced in the CLI error.


Configuration

# warden.yaml — policy gate (opt-in, default off)
policy:
  enabled: false
  flex_auth_url: http://127.0.0.1:8080
  fail_closed: true
  tenant: tenant:platform
  subject_env: WARDEN_POLICY_SUBJECT
  system: ops-warden
  caller_auth:            # how ops-warden identifies itself — see "Caller identity"
    mode: none
Key Default Description
enabled false When true, call flex-auth before every sign/issue
flex_auth_url http://127.0.0.1:8080 flex-auth base URL
fail_closed true Deny sign when flex-auth is unreachable or returns HTTP error
tenant tenant:platform Tenant sent in subject and resource
subject_env WARDEN_POLICY_SUBJECT Env var for IAM subject id override
system ops-warden Resource system identifier
caller_auth.mode none none | file | env | command — source of the Authorization bearer token flex-auth TokenReviews (Caller identity)

Set WARDEN_POLICY_SUBJECT to the caller's IAM profile sub when available. If unset, the actor name is used as subject id.


Versioning

Version Gate Status
v1 Inventory + TTL max Shipped
v2 flex-auth opt-in via policy.enabled Shipped (WP-0007)
v2.1 Identity claims required for adm signs Planned
v3 Tenant-scoped policies per tenant:* Planned

What stays in inventory

  • Actor registration (name, type, default principals, default TTL)
  • Host reference documentation
  • Scorecard local checks

flex-auth decides whether this sign request is allowed now; inventory defines what the actor is allowed to request.


flex-auth policy package (FLEX-WP-0006)

flex-auth owns the ssh-certificate / sign policy package. ops-warden consumes it via POST /v1/check when policy.enabled: true.

Handoff (canonical): ~/flex-auth/docs/ops-warden-policy-gate-handoff.md

Asset flex-auth path
Policy package examples/ops-warden/policy_package.md
Allow/deny fixtures examples/ops-warden/policy_fixtures.yaml
Registry snapshot examples/ops-warden/registry_snapshot.json
Subject manifest examples/ops-warden/subject_manifest.yaml
Resource manifest examples/ops-warden/resource_manifest.yaml

Tenant and subject bindings

Field Value
Tenant tenant:platform (policy.tenant)
Resource system ops-warden (policy.system)
Resource type ssh-certificate
Action sign
Resource id ssh-cert:actor/<actor-name>
Actor type Example flex-auth subject ops-warden inventory name pattern
adm platform-steward adm-*
agt ci-deploy-agent agt-*
atm backup-automation atm-*

Subject id sent to flex-auth: WARDEN_POLICY_SUBJECT when set, otherwise the inventory actor name. flex-auth may also allow iam:<actor-name> when listed in allowed_subjects on the resource.

Principals and TTL: Taken from the sign request (inventory defaults). flex-auth denies when principals are empty/disallowed or TTL exceeds max_ttl_hours on the registered resource.

Fixture coverage (flex-auth)

Allow: fixture:ops-warden-adm-sign-allow, fixture:ops-warden-agt-sign-allow, fixture:ops-warden-atm-sign-allow.

Deny: fixture:ops-warden-unknown-subject-deny, fixture:ops-warden-actor-type-mismatch-deny, fixture:ops-warden-ttl-above-max-deny, fixture:ops-warden-disallowed-principal-deny, fixture:ops-warden-missing-fingerprint-deny.

Local smoke

# flex-auth (from ~/flex-auth)
flex-auth serve --addr 127.0.0.1:8080 \
  --registry examples/ops-warden/registry_snapshot.json \
  --policy examples/ops-warden/policy_package.md \
  --log /tmp/flex-auth-ops-warden-decisions.jsonl

# warden.yaml — policy.enabled: true, flex_auth_url pointing at flex-auth
# Use an actor registered in the flex-auth registry (example fixtures use
# template names; production needs a registry slice for real inventory actors).

Local end-to-end evidence: history/2026-06-23-flex-auth-policy-gate-local-smoke.md.

Production registry from inventory

Build a flex-auth registry snapshot that mirrors inventory.yaml actors:

python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml \
  -o registry/flex-auth/production_registry_snapshot.json
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json

Re-run after adding or changing actors. Deploy the snapshot to the production flex-auth runtime together with ~/flex-auth/examples/ops-warden/policy_package.md.

Smoke (non-secret):

./scripts/policy_gate_production_smoke.sh
# OpenBao-backed — preferred: credential broker (no manual VAULT_TOKEN):
cd ~/railiance-platform && make credential-exec-ops-warden-smoke
# Manual fallback when broker unavailable:
SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh

Evidence: history/2026-06-23-flex-auth-policy-gate-production-smoke.md.


Caller identity (WARDEN-WP-0031)

flex-auth authenticates the caller before it evaluates the request. The in-cluster pin flex-auth-ops-warden (FLEX-WP-0016) passes the bearer token to a Kubernetes TokenReview and requires the principal system:serviceaccount:ops-warden:ops-warden for any request carrying resource.system: ops-warden.

Until ops-warden sends that header the pin logs

caller authentication warning: caller is not authenticated

and can only run callerAuth.mode: warn. Per ADHOC-2026-08-17-T01, policy.enabled must not flip while /v1/check still answers unauthenticated callers — so the missing header is what blocks the flip, and it is ops-warden's to fix, not flex-auth's.

Configure a token source

policy:
  caller_auth:
    mode: none          # none | file | env | command
    token_path: /var/run/secrets/flex-auth/token   # mode: file
    token_env: WARDEN_POLICY_CALLER_TOKEN          # mode: env
    command: kubectl create token ops-warden -n ops-warden --audience flex-auth --duration 10m
    audience: flex-auth
Mode Use it when
none Default. No header — pre-FLEX-WP-0016 behaviour, only viable while the pin is in warn
file In-cluster PEP with a projected, audience-bound ServiceAccount token
env The token is already in the environment (CI, a wrapper)
command Workstation warden sign — mint a short-lived bound token per call

A workstation is not a ServiceAccount, which is why command exists. The token is read, sent, and dropped: never cached, written, or logged (ADR-0002). If a token is configured but cannot be obtained, the sign is refused under fail_closed — falling back to an anonymous call would defeat the gate.

Readiness gate

python scripts/check_policy_caller_identity.py                     # offline
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090   # port-forward of the warn pin

Exit 0 ready / 1 not ready / 2 bad input. It prints the token's length and a truncated fingerprint, never the value, so its output is safe to paste into a handoff message. A live 401 means the token was sent but rejected (audience or binding); 403 means it authenticated but may not represent system: ops-warden.

Flip sequence

  1. Configure caller_auth; check_policy_caller_identity.py exits 0 offline.
  2. Smoke against a port-forward of the warn pin. The evidence is the absence of caller authentication warning in its log, not the allow — warn serves unauthenticated callers too, so an allow alone proves nothing.
  3. Ask flex-auth to set callerAuth.mode: enforce (their FLEX-WP-0016 T03).
  4. Re-run the gate against the enforcing pin.
  5. Only then: policy.enabled: true, fail_closed: true.

Reversing 3 and 5 401s every warden sign.


Production rollout

Keep policy.enabled: false until flex-auth is reachable at policy.flex_auth_url with fail_closed: true, unreachable flex-auth blocks all signs.

Operator checklist

Step Owner Action
1 flex-auth Deploy runtime; confirm curl <flex_auth_url>/healthz → 200 (FLEX-WP-0007)
2 flex-auth Load production registry + policy package (~/flex-auth/examples/ops-warden/)
3 ops-warden Regenerate registry from inventory: scripts/build_flex_auth_registry.py
4 ops-warden Local smoke: ./scripts/policy_gate_production_smoke.sh
5 operator Vault smoke: make credential-exec-ops-warden-smoke in railiance-platform (or manual SMOKE_VAULT=1 fallback)
6 operator Set policy.flex_auth_url in ~/.config/warden/warden.yaml — the pin is flex-auth-ops-warden.flex-auth.svc.cluster.local:8080, reached from a workstation via port-forward or tunnel
6a ops-warden Configure policy.caller_auth; scripts/check_policy_caller_identity.py exits 0 (see Caller identity)
6b flex-auth Set callerAuth.mode: enforce on the ops-warden pin (FLEX-WP-0016 T03)
7 operator Set policy.enabled: true; keep fail_closed: true
8 operator Allow smoke: warden sign <actor>signatures.log has policy_decision_id
9 operator Deny smoke: e.g. --ttl above max — CLI shows flex-auth reason, no cert

Cross-repo references:

  • ~/flex-auth/workplans/FLEX-WP-0007-ops-warden-policy-gate-production-deployment.md
  • history/2026-06-23-flex-auth-production-pickup-suggestion.md
  • history/2026-06-23-flex-auth-policy-gate-production-smoke.md

Summary

  1. Deploy the flex-auth registry and policy package to the production flex-auth runtime — not only the example fixtures.
  2. Set policy.flex_auth_url to the production flex-auth base URL.
  3. Enable policy.enabled: true only after steps 15 pass.
  4. Keep fail_closed: true unless an explicit break-glass procedure exists.
  5. Smoke allow and deny paths; preserve non-secret evidence only.

Rollback

If signs are blocked after enabling the gate:

  1. Set policy.enabled: false in warden.yaml (inventory + TTL gate only).
  2. Confirm warden sign succeeds without flex-auth.
  3. File a State Hub note to flex-auth with non-secret symptoms (HTTP status, fail_closed behaviour, actor name).
  4. Re-enable only after flex-auth runtime and registry are verified.

Evidence fields for the flip: flex-auth health URL, smoke script exit codes, warden activity --kind sign --json showing policy_decision_id on allow path.


See also

  • wiki/OpsWardenConfig.md — full config reference
  • wiki/CredentialRouting.md
  • ~/flex-auth/docs/ops-warden-policy-gate-handoff.md — flex-auth handoff
  • flex-auth/INTENT.md
  • net-kingdom/docs/platform-identity-security-architecture.md