From 05d12fe39f789ce891a757c858c457052af31d85 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 17 Aug 2026 10:44:59 +0200 Subject: [PATCH] TEN-WP-0009-T01: target platform-pg via rapp-postgres consumer declaration Corrects the credential assumption: shared-cluster access is a broker lease, not a CNPG-minted secretKeyRef. Co-Authored-By: Claude Opus 5 --- .../TEN-WP-0009-postgres-production-store.md | 112 ++++++++++++++---- 1 file changed, 90 insertions(+), 22 deletions(-) diff --git a/workplans/TEN-WP-0009-postgres-production-store.md b/workplans/TEN-WP-0009-postgres-production-store.md index 4ceb828..e7e8594 100644 --- a/workplans/TEN-WP-0009-postgres-production-store.md +++ b/workplans/TEN-WP-0009-postgres-production-store.md @@ -48,15 +48,33 @@ than a formality wrapped around one implementation. ## Credential handling -CloudNativePG mints the connection secret itself: a cluster named `X` produces -secret `X-app` with key `uri`. The Deployment references it via -`secretKeyRef`, exactly as `user-engine` does with -`USER_ENGINE_DATABASE_URL` ← `user-engine-pg-app/uri`. +**Corrected 2026-08-17.** This section originally said CloudNativePG mints an +`X-app` secret we reference with `secretKeyRef`, as `user-engine` does. That is +true for a cluster's *own* app database — and wrong for the path we are +actually taking. -So **no secret enters this repo, this workplan, or any evidence dump**, and no -`warden route` lookup is needed — this is not a credential we fetch, custody, -or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an -operator-minted in-cluster secret is outside that lane. +`railiance-platform/docs/rapp-postgres-boundary.md` is explicit: `rapp-postgres` +owns the role and database provisioning surface for the shared cluster, and +*"the workload receives a short-lived lease through the platform broker"* — the +OpenBao database secrets engine — rather than a static secret. `railiance-platform` +retains cluster-wide governance and the credential-broker grant catalog. + +Consequences, and the second one is real work: + +- **We do not provision our own database.** We *declare a consumer request* and + `rapp-postgres` provisions it. Writing a CNPG `Database` manifest into this + repo's `deploy/`, or adding a managed role to a shared cluster's spec, would + both be reaching across that boundary — the latter also mutating shared + infrastructure other consumers depend on. +- **Credentials are leased, not mounted.** A short-lived lease means the + connection string can change under a running pod, so the store cannot read a + DSN once at startup and hold it forever. T03 and T05 must account for + credential refresh; a long-lived pool built on an expired lease fails at the + worst possible moment. + +Either way **no secret enters this repo, this workplan, or any evidence dump**. +Runtime secret custody stays OpenBao's per `SCOPE.md`, which is exactly what +the broker lease implements. ## Placement: shared by default, movable by design @@ -110,16 +128,62 @@ priority: high state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873" ``` -Provision a dedicated **database** on an existing **shared** cluster, per the -principles above. Candidates are `net-kingdom-pg` (already exists, and -`tenant-engine` is a NetKingdom service) and `apps-pg`. Prefer the one whose -grouping matches the product family, since that is the axis a future split -would most likely follow — moving is cheap, but moving less often is cheaper. +**Target: `platform-pg`, via a `PostgresConsumer` declaration to +`rapp-postgres`.** Not `net-kingdom-pg` or `apps-pg`, and not provisioned by us. -Note the deviation from precedent and why: `user-engine` runs a dedicated +Reasoning, revised 2026-08-17 after finding the actual contract. The candidate +list originally weighed product-family fit and would have picked +`net-kingdom-pg`. But `platform-pg` is the shared cluster that has a +*governed provisioning path* — `rapp-postgres` owns its consumer declarations, +role provisioning, isolation tests, and recovery procedure. The other clusters +carry their roles as hand-added entries in the shared cluster spec +(`net-kingdom-pg` has `privacyidea`, `apps-pg` has `vergabe` and +`coulomb_social`). Choosing product-family fit would mean improvising outside +the one contract that exists, to gain an affinity that portability makes cheap +to change later anyway. `audit-core` is the precedent: a platform service, on +`platform-pg`, declared as a consumer. + +Note the deviation from `user-engine` and why: they run a dedicated `user-engine-pg`. We are deliberately *not* copying that. Their choice is fine -and ours is reversible in an afternoon; picking dedicated now would spend -capacity on an isolation guarantee nothing has asked for yet. +and ours is reversible; picking dedicated now would spend capacity on an +isolation guarantee nothing has asked for yet. + +The declaration to request, modelled on `consumers/audit-core.yaml`: + +```yaml +apiVersion: rapp-postgres.railiance.io/v1alpha1 +kind: PostgresConsumer +metadata: + name: tenant-engine +spec: + database: tenant_engine + schema: tenant_engine + costAttributionKey: platform:tenant-engine + clientNamespaces: [tenant-engine] + roles: + owner: tenant_engine_owner + migration: tenant_engine_migrate + runtime: tenant_engine_app + limits: + migrationConnections: 2 + runtimeConnections: 12 + statementTimeout: 30s + idleInTransactionSessionTimeout: 15s + tenantKeyingRequired: true + tenantIsolation: consumer-service-boundary +``` + +Two things to raise with them rather than assume: + +- **`tenantKeyingRequired: true`** is right for `audit-core`, whose rows are + per-tenant. Ours are *about* tenants — `tenant_id` is the primary key of the + `tenants` table, not a partition key on someone else's data. Confirm that + their isolation tests read our shape correctly rather than flagging a false + positive. +- **Statement timeout.** 30s is generous for this workload; every query here is + a single-row lookup or a small transaction. A tighter timeout is a better + failure mode for a service `flex-auth` calls synchronously on the + authorization path — a slow query should fail closed fast, not hold the PDP. Record what would trigger a move to dedicated, so the reversal is a judgement already made rather than one improvised under pressure. Candidate triggers: @@ -133,13 +197,17 @@ backup story at all, so this is a gain to claim explicitly rather than inherit by accident — and a shared cluster means inheriting *someone else's* retention choice, which is worth checking rather than assuming. -Write whatever manifest the choice needs into `deploy/` — a CNPG `Database` -resource against the shared cluster, not a new `Cluster`, if that is the -correct shape. +**Do not write a CNPG `Database` or `Cluster` manifest into this repo's +`deploy/`.** That surface belongs to `rapp-postgres`. Our `deploy/` gains only +the consumption side — how the workload reaches the leased credential. -Done when the database exists on shared capacity, the connection reaches the -app only through an injected URL, and the move-to-dedicated trigger list and -runbook are recorded. +Done when `rapp-postgres` has provisioned the consumer, the connection reaches +the app only through an injected, refreshable credential, and the +move-to-dedicated trigger list and runbook are recorded. + +Status 2026-08-17: declaration requested from `rapp-postgres`; awaiting their +provisioning and an answer on how a workload consumes the broker lease in +practice. Nothing to apply on our side until that returns. ## T02 - Implement `PostgresTenantStore`