railiance-platform/docs/rapp-credential-lane-binding.md
codex dfa6373985
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Close RAILIANCE-WP-0015-T06 rapp credential-lane binding
Document the one recipe a new rapp uses to acquire runtime secrets:
standing KV secrets bind through a CCR target.rapp, leases through
grant rapp_id. Stamp the existing postgres grants and the qonto
workload CCR. Gate, delivery, and revocation are unchanged.
2026-08-14 00:47:28 +02:00

8.1 KiB

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.

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-:

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:

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:

    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.