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>
12 KiB
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
- Configure
caller_auth;check_policy_caller_identity.pyexits 0 offline. - Smoke against a port-forward of the warn pin. The evidence is the absence
of
caller authentication warningin its log, not theallow— warn serves unauthenticated callers too, so an allow alone proves nothing. - Ask flex-auth to set
callerAuth.mode: enforce(their FLEX-WP-0016 T03). - Re-run the gate against the enforcing pin.
- 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.mdhistory/2026-06-23-flex-auth-production-pickup-suggestion.mdhistory/2026-06-23-flex-auth-policy-gate-production-smoke.md
Summary
- Deploy the flex-auth registry and policy package to the production flex-auth runtime — not only the example fixtures.
- Set
policy.flex_auth_urlto the production flex-auth base URL. - Enable
policy.enabled: trueonly after steps 1–5 pass. - Keep
fail_closed: trueunless an explicit break-glass procedure exists. - Smoke allow and deny paths; preserve non-secret evidence only.
Rollback
If signs are blocked after enabling the gate:
- Set
policy.enabled: falseinwarden.yaml(inventory + TTL gate only). - Confirm
warden signsucceeds without flex-auth. - File a State Hub note to
flex-authwith non-secret symptoms (HTTP status,fail_closedbehaviour, actor name). - 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 referencewiki/CredentialRouting.md~/flex-auth/docs/ops-warden-policy-gate-handoff.md— flex-auth handoffflex-auth/INTENT.mdnet-kingdom/docs/platform-identity-security-architecture.md