# 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/<role>` 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 <lease-accessor>
scripts/credential.py revoke <lease-accessor>
```

`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:<local-user>`;
- `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:<namespace>:<service-account>`. 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 canonical `POST /v1/check` by
default. It maps the grant to a `credential-grant:<grant-id>` resource, action
`issue`, and the bound actor subject. The helper parses `requested_ttl` locally
and sends integer `context.requested_ttl_seconds`; flex-auth policy never has to
infer duration units. Only a DecisionEnvelope with `effect: allow` succeeds;
the helper also requires its decision id, evaluator provenance, and matching
subject/resource binding. Every other or malformed response fails closed. 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
```

Use CREDENTIAL_HELPER_CHILD_ENV for child-only environment assignments needed by the smoke command, for example a Linux-only PATH that exposes ops-warden tooling. These assignments are passed after the credential helper separator and are not used for token handoff.

Emergency revocation by accessor:

```bash
scripts/credential.py revoke <lease-accessor>
```

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.
