# Rapp credential-lane binding Work record: `RAILIANCE-WP-0015-T06` Owner: `railiance-platform` (S3) This is the single recipe a new rapp uses to acquire runtime secrets. It binds the existing S3 lanes. It does not invent a package-local broker, change grant TTLs, delivery modes, approval, or revocation. ## The rule A rapp **declares** what it needs. S3 **vends** it through the lanes that already exist. The package never commits a credential, never holds a grant catalog, and never files a CCR in the rapp repo. ```text rapp.yaml S3 lane object live delivery ───────── ────────────── ───────────── runtime_dependencies[] ──► capability (who satisfies it is not the rapp's problem) secret_references[] ──► CCR (standing KV secret) openbao.kv_path == the reference workload lane: delivery.surface is external-secrets / kubernetes consumer_contract ──► grant catalog (short-lived lease) + consumers[] id: /- rapp_id: ``` Two existing lane objects. Not a third. | Need | Lane | Home | How the rapp names it | | --- | --- | --- | --- | | Standing secret (API key, provider token, webhook secret) | Credential change request | `credential-change-requests/CCR-*.yaml` | `secret_references` lists the OpenBao path | | Short-lived lease (dynamic DB password, bounded OpenBao token) | Grant catalog | `credential-grants/catalog.yaml` | grant `id` is `/-` and grant `rapp_id` equals the package | Operator/human fetch of the same KV path is a **separate** CCR (`delivery.surface: operator-workstation`). It may share the path. It is not the rapp's runtime bind. ## What each rapp field means `runtime_dependencies` names **capabilities**. The schema is explicit: the list does not say who provisions the thing that satisfies a name. `openbao-database-secrets-engine` and `s3-backup-target` are capabilities. The database engine is configured by S3 from statements the package owns; the backup bucket is a `resource-control` resource and the provider credential is S3 custody (`secret:railiance-platform/backup`, first projection Secret `platform-pg-backup-s3`). Fail-closed until that handoff lands is correct, not a hole in the package. `secret_references` names **OpenBao paths**, never values. Each path on a placed rapp must have exactly one **workload** CCR whose `openbao.kv_path` matches. "Workload" means the CCR's delivery surface is the in-cluster consumer (External Secrets or Kubernetes auth), not an operator workstation. A rapp with no standing KV secret omits the field. `consumer_contract.credential_lane` names the **broker**, not a grant. Today that value is `railiance-platform-credential-broker`. Individual consumer leases are catalog entries bound by `rapp_id`. ## Choose the lane 1. Is the secret a long-lived value that a pod must hold across restarts? File a CCR. Delivery is External Secrets into a namespace-scoped Secret. The CCR is the approval, policy, auth role, and ops-warden front door. Follow `docs/credential-change-approval.md` and `docs/credential-lane-lifecycle-runbook.md`. 2. Is the secret something that should exist only for the length of a connection or an attended command? Add a grant. Delivery is `scripts/credential.py exec` (or in-cluster database/creds). Follow `docs/credential-broker.md`. 3. Never both for the same need. A CCR is not a grant with a longer TTL. A grant is not a KV write. Do not open a package-local OpenBao policy, a rapp-owned Secret manifest with a value, or a second catalog. ## Binding fields These fields are pointers. They do not change who may approve, how a lease is minted, or how it is revoked. **Grant catalog** — required when the grant id starts with `rapp-`: ```yaml id: rapp-postgres/audit-core-runtime rapp_id: rapp-postgres ``` `rapp_id` must equal the id prefix before `/`. The validator enforces that. Grants that are not package-owned (`ops-warden/warden-sign`) omit the field. **CCR** — set when the lane is the runtime bind for a rapp: ```yaml target: workload: qonto-assistant # existing; the running unit rapp: rapp-qonto # optional; the package that owns rollout ``` `target.rapp` is optional because most live CCRs belong to workloads that are not yet packages. Adding it is part of extracting a rapp, not a reason to invent a CCR. The validator accepts the field and rejects a value that is not a `rapp-*` slug. `target.workload` stays the workload identity. It must not become the repo slug. ## Recipe for a new rapp Do these in order. Stop if a step would change a gate, a TTL, a delivery mode, or a revocation rule — that work is a CCR or a grant change, not a rapp-local patch. 1. Write `runtime_dependencies` for every capability the package assumes, including secret-bearing ones. 2. Decide standing-secret vs lease for each credential need. 3. Standing secret: put the OpenBao path in `secret_references`. File the CCR in this repo (`scripts/credential-change.py`), set `target.rapp`, and keep policy HCL here. The rapp repo may hold only the ExternalSecret that *consumes* the store S3 applies. 4. Lease: add a grant whose id is `/-` and whose `rapp_id` matches. For a provisioning rapp, declare the consumer in `consumers:` and keep `consumer_contract.credential_lane: railiance-platform-credential-broker`. 5. Validate without minting anything: ```bash make credential-grants-validate make credential-change-validate ``` 6. Apply and verify through the existing attended helpers. Do not add a rapp-local apply path. A workload that is not yet a rapp keeps using a CCR or grant with no `rapp` / `rapp_id`. Extraction adds the pointer; it does not restamp the secret. ## Worked examples ### `rapp-openbao` — the store, not a consumer No `secret_references`, no grant, no CCR. OpenBao is the custody engine. Package-owned smoke stays non-secret. Authenticated policy and audit checks stay in S3. A later platform rapp must not copy this shape unless it *is* a secrets engine. ### `rapp-postgres` — provisioning rapp, lease lane - `runtime_dependencies` includes `openbao-database-secrets-engine` and `s3-backup-target`. - `consumer_contract.credential_lane` is the broker. - Catalog grants `rapp-postgres/audit-core-runtime` and `rapp-postgres/audit-core-migration` carry `rapp_id: rapp-postgres`. - No `secret_references`: the package does not read a KV path. Consumers receive a short-lived `database/creds/` lease. - `s3-backup-target` is unsatisfied until `resource-control` hands an endpoint. `make postgres-backup-deploy` stays fail-closed. That is not a missing CCR on this package. ### `rapp-qonto` — consuming rapp, standing-secret lane - `secret_references: [tenants/binky/qonto-api]`. - Workload CCR: `CCR-2026-0009` (`delivery.surface: external-secrets`, `target.rapp: rapp-qonto`). That is the runtime bind. - Operator CCR: `CCR-2026-0008` (same path, workstation delivery). Not the rapp bind. Rotating the value rotates both lanes because they share the secret; deactivating the operator front door must not be mistaken for taking the workload offline. ## Current bind table | Rapp | Declaration | S3 lane | Status | | --- | --- | --- | --- | | `rapp-openbao` | none | none — package is the store | correct | | `rapp-postgres` | `consumer_contract` + `openbao-database-secrets-engine` | `rapp-postgres/audit-core-runtime`, `rapp-postgres/audit-core-migration` | bound | | `rapp-postgres` | `s3-backup-target` / `secret_references: platform/workloads/railiance/backup/object-storage` | `CCR-2026-0012` | pointer set; CCR `proposed`; first consumer of the general backup key; Secret not vended | | `rapp-qonto` | `secret_references: tenants/binky/qonto-api` | `CCR-2026-0009` | pointer set; CCR itself is still `proposed` | Live CCRs without `target.rapp` are un-rapped workloads. They stay on the CCR lane. They do not get a grant or a package invented to make this table look complete.