# Credential Request And Lease Broker **Workplan:** `RAILIANCE-WP-0005` **Owner:** `railiance-platform` **Status:** source implementation complete; live verification pending approved token path This document records the Railiance credential broker ownership decision and the first implementation contract for short-lived OpenBao credential leases. ## Decision `railiance-platform` owns OpenBao credential request, generation, delivery, audit, and revocation because this repo owns the platform secrets service and the OpenBao policy surface. The broker may later split into a dedicated service repo if the implementation grows, but the grant catalog and OpenBao policy contracts remain platform-owned. The broker is not a new secret store. It is a controlled request path for bounded credentials that already belong to OpenBao or adjacent platform authorities. ## Boundaries | Concern | Owner | Boundary | | --- | --- | --- | | OpenBao mounts, policies, token roles, response wrapping, audit | `railiance-platform` | Generates and revokes bounded credentials. | | Human login, OIDC, MFA, IAM profile claims | `key-cape` | Authenticates human and service identities. | | Authorization decision | `flex-auth` | Decides whether an actor may request a grant for a purpose, TTL, audience, and delivery mode. | | SSH certificate signing | `ops-warden` | Issues SSH certificates only. It does not vend OpenBao tokens, API keys, or provider secrets. | | Request tracking | State Hub | Stores non-secret metadata only: request ids, actor, grant, purpose, TTL, decision id, lease accessor, status, timestamps, and audit pointers. | | Agent/runtime consumption | `llm-connect` and callers | Never place secrets in prompts. Consume credentials through local exec injection, response wrapping, service-account auth, or approved local files. | ## Non-Secret Metadata Only State Hub, workplans, docs, Git, chat, and prompts may contain: - grant ids such as `ops-warden/warden-sign`; - requested TTL and bounded max TTL; - actor and subject ids; - purpose strings; - lease handles or accessors when they are not sufficient to use the secret; - OpenBao audit request ids or timestamps; - status values such as requested, issued, denied, revoked, or expired. They must not contain: - OpenBao root tokens, platform-admin tokens, or wrapped token values; - unseal shares, recovery codes, private keys, OTP seeds, passwords, or API keys; - raw bearer tokens in command lines, prompt text, State Hub bodies, or logs; - screenshots or pasted command output containing secret values. ## Grant Catalog The catalog lives at: ```text credential-grants/catalog.yaml ``` Validate it with: ```bash make credential-grants-validate ``` Every grant entry defines: - a stable grant id; - credential type and OpenBao policy set; - grant class: `self-service`, `approval-required`, or `break-glass`; - default and max TTL; - allowed actor types and purpose examples; - allowed and denied delivery modes; - audit and revocation expectations. The first pilot grant is `ops-warden/warden-sign`, which creates a short-lived OpenBao token with only the `warden-sign` policy. ## OpenBao Token Roles OpenBao-token grants are configured from source with: - an issuer policy under `openbao/policies/`; - an `auth/token/roles/` token role with allowed policies, disallowed admin policies, non-renewable TTL bounds, no default policy, and orphan token issuance; - verification that reads the issuer policy, token role, and target workload policy before any smoke token is minted. Dry-run the current grant configuration with: ```bash make openbao-token-grants-dry-run make openbao-verify-token-grants-dry-run ``` Live application uses an operator-approved OpenBao token from `OPENBAO_TOKEN_FILE` or an interactive hidden prompt. The token is passed to the OpenBao pod through stdin, never through argv: ```bash OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-configure-token-grants OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants ``` The smoke verifier can mint a short-lived child token, confirm that it can list `ssh/roles`, confirm that it cannot list unrelated secret engines, and revoke the token by accessor: ```bash OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants-smoke ``` ## Delivery Modes `exec-env` is the preferred local path. The helper obtains a lease, injects the credential only into a child process environment, redacts output, and then revokes or lets the credential expire. `response-wrap` is for attended handoff. The broker returns a single-use OpenBao wrapping token instead of the raw credential. The recipient unwraps it once; a second unwrap must fail. `local-token-file` is for tools that cannot consume environment variables cleanly. Files must be mode `0600`, stored under `.local/credential-leases/`, and removed when the lease is revoked or expires. That directory is ignored by Git. `kubernetes-auth` is for in-cluster workloads. Workloads should authenticate with service-account-bound auth instead of receiving manually handed tokens. For the pilot grant, `request --delivery kubernetes-auth` returns only non-secret OpenBao auth metadata such as the auth mount, role, service account names, and namespaces; it does not mint or print a bearer token. The denied modes are absolute unless a later ADR updates the catalog: - `chat` - `state-hub-body` - `git` - `command-line-token-argument` - `llm-prompt` ## Pilot Flow The target ops-warden smoke path is: ```bash credential exec --grant ops-warden/warden-sign --ttl 15m -- \ SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh ``` The source helper MVP lives at `scripts/credential.py` until this flow graduates into a packaged command. It supports the same core shape: ```bash scripts/credential.py request --grant ops-warden/warden-sign --purpose flex-auth-openbao-smoke scripts/credential.py exec --grant ops-warden/warden-sign --purpose flex-auth-openbao-smoke -- \ SMOKE_VAULT=1 /home/worsch/ops-warden/scripts/policy_gate_production_smoke.sh scripts/credential.py status scripts/credential.py revoke ``` `request` defaults to `local-token-file`: the raw child token is written only to `.local/credential-leases/` with mode `0600`, and stdout contains the lease handle/accessor plus metadata. `--delivery response-wrap` returns an OpenBao wrapping token for attended handoff, not the raw child token. `exec` mints a bounded child token, injects it as `VAULT_TOKEN` only into the child process environment, redacts token-looking output, and revokes the token by accessor when the child exits. The helper rejects caller-supplied `VAULT_TOKEN`/`BAO_TOKEN` env assignments and unsafe OpenBao debug/trace log settings. Dry-run all helper paths with: ```bash make credential-helper-dry-run ``` Pass helper global options before the subcommand. For example, if the OpenBao pod has an approved token helper session: ```bash make credential-exec-ops-warden-smoke CREDENTIAL_HELPER_GLOBAL_ARGS=--use-token-helper ``` The child process receives `VAULT_TOKEN` in its environment. The token is not printed, written to shell history, sent to State Hub, or placed in an LLM prompt. ## Identity And Authorization The helper records the following non-secret request identity fields: - `actor`: the requester identity, defaulting to `codex:`; - `actor_type`: one of the grant-approved actor classes such as `human-operator`, `approved-agent`, or `ci-runner`; - `subject`: the bound human, agent, CI, or Kubernetes service-account subject. Human operators should use the KeyCape/OIDC path with MFA when the grant class or purpose requires it. Agents and CI runners should use stable subject strings that can be mapped to IAM profile claims, for example `agent:codex/railiance-platform` or `system:serviceaccount::`. Headless automation must use Kubernetes auth or an explicitly approved non-interactive identity; it must not reuse a human OpenBao token. The helper performs local catalog checks before any issuance: - purpose is required; - requested TTL must not exceed the grant max TTL; - delivery mode must be allowed by the grant; - actor type must be allowed by the grant. Optional flex-auth preflight is enabled with `--flex-auth-url` or `FLEX_AUTH_URL`. The helper posts non-secret request metadata to `/credential-grants/authorize` by default and accepts allow/deny responses using `allowed`, `decision`, or `status` fields plus optional `decision_id` and `reason`. Use `--require-flex-auth` when local preauthorization is not acceptable. Use `--decision-id` to carry an already-approved external decision without calling flex-auth again. ## State Hub Metadata State Hub recording is opt-in through `--record-state-hub` or `CREDENTIAL_RECORD_STATE_HUB=1`. The helper writes request lifecycle notes to `/progress/` with non-secret metadata only: - grant id, actor, actor type, subject, purpose, requested TTL, delivery mode; - authorization mode, decision id, and decision reason; - lease accessor, wrapping accessor, or wrapped accessor when available; - status values such as `requested`, `issued`, `wrapped`, `revoked`, or `delegated`. It never records raw child tokens, wrapping tokens, token files, passwords, OpenBao root/platform-admin tokens, or command output. ## Verification And Revocation Offline checks: ```bash make credential-grants-validate make credential-tests make openbao-token-grants-dry-run make openbao-verify-token-grants-dry-run make credential-helper-dry-run ``` Live source-owned checks, once an approved OpenBao issuer token path exists: ```bash OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-configure-token-grants OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-token-grants-smoke OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make credential-exec-ops-warden-smoke ``` Emergency revocation by accessor: ```bash scripts/credential.py revoke ``` When using `local-token-file`, remove stale local lease material after revoke or expiry: ```bash find .local/credential-leases -type f -maxdepth 1 -print ``` Response wrapping live verification is manual until a richer integration test exists: unwrap the returned wrapping token once with OpenBao, confirm the second unwrap attempt fails, then revoke the wrapped child token by accessor. ## Routing And Rollout Credential routing remains split by responsibility: - `ops-warden` signs SSH certificates only; - OpenBao token or dynamic-lease needs route to `railiance-platform`; - login/MFA routes to KeyCape; - authorization decisions route to flex-auth. The rollout sequence is: 1. `ops-warden/warden-sign` pilot for the flex-auth/ops-warden smoke. 2. Platform-readonly token helper for diagnostics. 3. Workload-specific grants for application repositories. 4. Optional split to a dedicated credential-broker repo if the helper grows beyond platform ownership. The workplan can close only after the live warden-sign pilot runs through the helper and the credential routing catalog returns this railiance-platform flow for VAULT_TOKEN/OpenBao-token requests. ## Implementation Sequence 1. Validate and maintain the non-secret grant catalog. 2. Add bounded OpenBao token role configuration for each OpenBao-token grant. 3. Build a small helper that supports `request`, `exec`, `status`, and `revoke`. 4. Add optional flex-auth preflight and State Hub request lifecycle metadata. 5. Update ops-warden routing so OpenBao token needs point here, while SSH certificate issuance remains in ops-warden. Live token issuance requires an approved operator path to create or use the non-root issuer capability. Source-only validation and dry-run helper behavior must remain useful without a live token.