189 lines
8.1 KiB
Markdown
189 lines
8.1 KiB
Markdown
|
|
# 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>/<consumer>-<role>
|
||
|
|
rapp_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 `<rapp_id>/<consumer>-<role>` 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 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 `<rapp_id>/<consumer>-<role>` 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/<role>` 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` | no lane yet; Secret `platform-pg-backup-s3` is S3 custody after RESOURCE-WP-0002 | fail-closed on purpose |
|
||
|
|
| `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.
|