ops-warden/wiki/OpsWardenConfig.md
tegwick 7d4381e887
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
WARDEN: separate fetch evaluator URL for secret-read checks (CUST-WP-0075)
check_fetch_policy now posts only to policy.fetch_flex_auth_url (access-engine
pin flex-auth-ops-warden-fetch, workstation 127.0.0.1:19091). Signing keeps
policy.flex_auth_url. Unset or unreachable fetch evaluator fails closed with a
message naming it; no fallback to the signing pin or to allow. warden access
advisory output names the fetch evaluator. Recorded under WARDEN-WP-0039-T03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3775@bnt-lap001
Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
2026-10-03 09:18:54 +02:00

12 KiB

OpsWarden Configuration Reference

Config file: ~/.config/warden/warden.yaml (override with WARDEN_CONFIG env var)


Backend overview

Backend Config value Use when
Local CA backend: local Labs, CI, air-gapped dev, hosts without platform secrets access
Platform CA backend: vault Production and shared ops environments

Platform standard: Railiance S3 uses OpenBao as the runtime platform secrets service (RAIL-PL-WP-0002 in railiance-platform). OpenBao exposes a Vault-compatible HTTP API, so ops-warden keeps the config keys backend: vault and the vault: block — no separate OpenBao backend name is required. The same config works against OpenBao or HashiCorp Vault if you point vault.addr at either service.

ops-warden signs SSH certificates only. It does not deploy OpenBao, manage unseal keys, or store long-lived API secrets. Cluster bootstrap and custody live in railiance-platform and NetKingdom docs.


Local backend (lab / offline)

# Uses ssh-keygen -s with a CA private key on disk.
backend: local

# Path to the CA private key. Keep this file mode 600 and never commit it.
ca_key: ~/.ssh/ops-ca-user

inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden

# Zone-aware flex-auth gate. With no signing URL, the explicit unknown/build
# profile fails open for signing and records that no evaluator decision was
# available. Fetch checks need fetch_flex_auth_url and fail closed without it.
policy:
  flex_auth_url: http://127.0.0.1:8080
  fetch_flex_auth_url: http://127.0.0.1:8081
  zone_registry_path: /path/to/compiled-flex-auth-registry.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

Bootstrapping the local CA key

# Generate CA keypair once (offline, secure location)
ssh-keygen -t ed25519 -f ~/.ssh/ops-ca-user -C "Ops SSH User CA (2026)" -N ""
chmod 600 ~/.ssh/ops-ca-user
chmod 644 ~/.ssh/ops-ca-user.pub

# Distribute ops-ca-user.pub to every host:
#   TrustedUserCAKeys /etc/ssh/ca/ca_user.pub  (in sshd_config)
# See railiance-infra bootstrap-ssh-ca.yml playbook.

OpenBao / Vault-compatible backend (production)

Use this backend against the platform OpenBao instance or any other SSH secrets engine that implements the Vault signing API (POST /v1/<mount>/sign/<role>).

Example — Railiance01 (browser / operator workstation)

backend: vault

vault:
  # OpenBao UI/API (KeyCape OIDC). Prefer short-lived tokens from policy, not root.
  addr: https://bao.coulomb.social

  mount: ssh

  role_map:
    adm: adm-role
    agt: agt-role
    atm: atm-role

  # OpenBao accepts the same X-Vault-Token header name as Vault.
  token_env: VAULT_TOKEN

inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden

# Configure after flex-auth ssh-certificate policies are deployed:
# policy:
#   flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
#   fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080
#   zone_registry_path: /etc/warden/production_registry_snapshot.json

Example — in-cluster caller (pod or trusted host)

backend: vault

vault:
  addr: http://openbao.openbao.svc.cluster.local:8200
  mount: ssh
  role_map:
    adm: adm-role
    agt: agt-role
    atm: atm-role
  token_env: VAULT_TOKEN

Choose the addr that matches where warden runs: operators on a laptop use the external HTTPS endpoint; workloads inside the cluster use the internal service URL. See railiance-platform/docs/openbao.md for deployment and access paths.

Authentication

Preferred: use the railiance-platform credential broker so VAULT_TOKEN is injected only into the child process (no manual export):

cd ~/railiance-platform
scripts/credential.py exec --grant ops-warden/warden-sign --ttl 15m -- \
  warden sign <actor> --pubkey <path>

warden route show ops-warden-warden-sign-token · wiki/playbooks/ops-warden-warden-sign-token.md.

Manual fallback — export a scoped token for the current shell only:

export VAULT_TOKEN="<short-lived-warden-sign-token>"

warden reads the env var named in vault.token_env (default VAULT_TOKEN). OpenBao uses the same header; you do not need a separate BAO_TOKEN unless you configure token_env that way.

See wiki/playbooks/operator-openbao-token-hygiene.md for hygiene rules, OIDC routing, and HTTP 403 recovery.

On failure, warden sign suggests falling back to --backend local only for lab recovery — not as a production substitute.

SSH secrets engine setup (OpenBao)

Run once per environment after OpenBao is initialized and unsealed. Adjust TTL limits to match ActorType policy in wiki/AccessManagementDirective.md (adm 48 h, agt 24 h, atm 8 h).

# OpenBao CLI (bao) — preferred on Railiance
bao secrets enable ssh

bao write ssh/roles/agt-role \
    key_type=ca \
    allowed_users="*" \
    allow_user_certificates=true \
    default_user="agt" \
    ttl=24h max_ttl=24h

bao write ssh/roles/adm-role \
    key_type=ca \
    allowed_users="*" \
    allow_user_certificates=true \
    default_user="adm" \
    ttl=48h max_ttl=48h

bao write ssh/roles/atm-role \
    key_type=ca \
    allowed_users="*" \
    allow_user_certificates=true \
    default_user="atm" \
    ttl=8h max_ttl=8h

HashiCorp Vault uses the same paths with the vault CLI:

vault secrets enable ssh
vault write ssh/roles/agt-role key_type=ca ...  # same role parameters

Mount path defaults to ssh; override with vault.mount in warden.yaml if your engine lives elsewhere.

Platform references

Topic Location
OpenBao deploy, unseal, OIDC admin railiance-platform/docs/openbao.md
Host CA trust and principals railiance-infra Ansible playbooks
Signing contract for callers wiki/CertCommandInterface.md

Principals inventory (inventory.yaml)

actors:
  # Actor name must carry the prefix matching its type:
  #   adm-*  for adm, agt-*  for agt, atm-*  for atm
  agt-state-hub-bridge:
    type: agt
    # Principals embedded in the cert; matched against /etc/ssh/auth_principals/%u
    principals:
      - agt-task-bridge
    # Certificate TTL in hours. Defaults: adm=48, agt=24, atm=8
    ttl_hours: 24
    description: "ops-bridge tunnel agent for state-hub"

  adm-bernd:
    type: adm
    principals:
      - adm-full
    ttl_hours: 48

  atm-backup-daily:
    type: atm
    principals:
      - atm-backup-daily
    ttl_hours: 8
    description: "nightly backup automation"

hosts:
  # Optional: documents which principals are allowed on each host.
  # Not enforced by warden; used for reference and future tooling.
  railiance01:
    allowed_principals:
      agt:
        - agt-task-bridge
      atm:
        - atm-backup-daily

Policy gate (flex-auth, zone-aware)

warden sign and warden issue evaluate flex-auth whenever a URL is configured. The target resource's compiled security_zone selects the local PEP failure mode. A rendered deny always blocks; evaluator failure blocks or proceeds per zone. Signing records policy_decision_id when present plus policy_zone, policy_failure_mode, and policy_outcome.

policy:
  flex_auth_url: http://127.0.0.1:8080
  fetch_flex_auth_url: http://127.0.0.1:8081
  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
  tenant: tenant:platform
  subject_env: WARDEN_POLICY_SUBJECT
  system: ops-warden

policy.enabled and the global policy.fail_closed are retired; configuration loading rejects them with a migration error. Stance is owned by flex-auth's versioned policy package, not this block. The failure-mode map is PEP behavior for an unavailable or invalid evaluator.

Full request shape and rollout notes: wiki/PolicyGatedSigning.md.

Two evaluators: signing and fetch checks (CUST-WP-0075)

Key Pin Used by Admits
policy.flex_auth_url flex-auth-ops-warden (workstation 127.0.0.1:19090) warden sign, warden issue resource.system: ops-warden only
policy.fetch_flex_auth_url flex-auth-ops-warden-fetch (workstation 127.0.0.1:19091) warden access <need> fetch transports (check_fetch_policy) read for catalog lanes with status active, exec_capable: true and lane secret, for owner systems bound in access-engine values/ops-warden-fetch.yaml

Both use the same caller identity (policy.caller_auth: the ops-warden service account token, audience flex-auth). For fetches a missing caller token refuses the fetch instead of sending an unauthenticated request.

The fetch check never falls back to flex_auth_url: the signing pin binds only the ops-warden system, so every fetch for another owner would get 403. If fetch_flex_auth_url is unset or the evaluator is unreachable, the fetch fails closed (exit 4) with a message naming the missing fetch evaluator, whatever the unknown failure mode says. An explicit 401/403 still maps to "flex-auth refused fetch policy check (HTTP 403); caller authentication or system binding was rejected". warden access <need> prints which evaluator gates fetches on its gate line (or policy line).

In-cluster address: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080 (keep the trailing dot). access-engine tools/gen_ops_warden_fetch.py generates the pin's readable set from this repo's routing catalog. When you add or retire a lane of that kind, tell access-engine so it can regenerate the set and roll a new image. Deployment notes: access-engine examples/ops-warden-fetch/README.md.


Environment variables

Variable Default Description
WARDEN_CONFIG ~/.config/warden/warden.yaml Config file path
VAULT_TOKEN — API token for backend: vault (OpenBao or Vault; name configurable via vault.token_env)
WARDEN_POLICY_SUBJECT — IAM subject id for flex-auth checks

cert_command integration with ops-bridge

Add cert_command to a tunnel in ~/.config/bridge/tunnels.yaml:

tunnels:
  state-hub-railiance01:
    host: railiance01
    remote_port: 8001
    local_port: 8000
    ssh_user: agt-state-hub-bridge
    ssh_key: ~/.ssh/agt-state-hub-bridge_ed25519
    actor: agt-state-hub-bridge
    cert_command: "warden sign agt-state-hub-bridge --pubkey ~/.ssh/agt-state-hub-bridge_ed25519.pub"

Workstation tunnels to the two flex-auth pins (remote_host is each pin Service's ClusterIP, see access-engine examples/ops-warden-fetch/README.md):

tunnels:
  flex-auth-ops-warden-railiance01:          # signing pin -> policy.flex_auth_url
    host: 92.205.62.239
    direction: local
    remote_host: <flex-auth-ops-warden ClusterIP>
    remote_port: 8080
    local_port: 19090
    health_check:
      url: http://127.0.0.1:19090/healthz
  flex-auth-ops-warden-fetch-railiance01:    # fetch pin -> policy.fetch_flex_auth_url
    host: 92.205.62.239
    direction: local
    remote_host: <flex-auth-ops-warden-fetch ClusterIP>
    remote_port: 8080
    local_port: 19091
    health_check:
      url: http://127.0.0.1:19091/healthz

ops-bridge runs cert_command before each SSH launch, captures stdout as the cert, and passes it alongside the private key via ssh -i <key> -i <cert>. See wiki/CertCommandInterface.md for the full contract and wiki/playbooks/ops-bridge-tunnel-cert.md for static-key → cert_command migration.