Finish RPF-WP-0018; RPF-WP-0019 repository-complete
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

RPF-WP-0018 closed: all seven tasks done. The provider-declaration finding
was adopted upstream and its canonical form is the provider: block in
tenancy.yaml; adaptive-pricing declined the standing co-signature and
supplied typed tier minima instead, recorded in ADR-0002. Three corrections
against our own output are recorded in the documents rather than edited
away.

RPF-WP-0019 T03 done (ceiling of three, memory binding, apps-pg-2 named as
overflow, enforced by make apps-pg-verify-capacity). T01/T02 are
repository-complete: backup target, retention, per-consumer connection
limits, role timeouts and Burstable resources are declared in source and
published in s3-consumer-interfaces 1.1.0 before rollout. They stay in
progress because no live application, backup success or restore proof
exists, and declared configuration is not a section 13 artifact. T04 waits
on that window.

apps-pg R reason corrected to say the target is declared-not-applied rather
than absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-08-18 13:35:04 +02:00
parent e52ae26001
commit dc4245361d
20 changed files with 650 additions and 73 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ helm/*.yaml
!helm/*-cluster.yaml
!helm/*-networkpolicies.yaml
!helm/*-databases.yaml
!helm/*-backup.yaml
# ArgoCD repository credentials — encrypt locally, never commit
argocd/repositories/*.repository.sops.yaml

View file

@ -82,7 +82,10 @@ consumption-preflight: ## Refuse a new order that exceeds a restricted entity's
##@ Shared apps-pg (S5 application databases)
apps-pg-deploy: ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
apps-pg-verify-capacity: ## Verify cell ceiling, role limits and distinct backup prefixes
python3 tools/verify_apps_pg_capacity.py helm/apps-pg-cluster.yaml helm/apps-pg-2-cluster.yaml
apps-pg-deploy: apps-pg-verify-capacity ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
@if [ -n "$(CONSUMING_ENTITY)" ]; then \
$(MAKE) consumption-preflight CONSUMING_ENTITY='$(CONSUMING_ENTITY)' \
ESTIMATE_EUR='$(ESTIMATE_EUR)' CONSUMPTION_CLASS=new-order; \
@ -90,6 +93,21 @@ apps-pg-deploy: ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
$(KUBECTL) apply -f helm/apps-pg-cluster.yaml
$(KUBECTL) apply -f helm/apps-pg-networkpolicies.yaml
apps-pg-backup-deploy: ## Apply the daily backup after the governed S3 Secret exists
@$(KUBECTL) get secret platform-pg-backup-s3 -n databases >/dev/null || { echo 'missing governed platform-pg-backup-s3 secret' >&2; exit 2; }
$(KUBECTL) apply -f helm/apps-pg-backup.yaml
apps-pg-overflow-dry-run: apps-pg-verify-capacity ## Server-validate the unapplied overflow cell
@$(KUBECTL) get secret apps-pg-2-credentials -n databases >/dev/null || { echo 'missing distinct apps-pg-2-credentials secret' >&2; exit 2; }
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-cluster.yaml
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-networkpolicies.yaml
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-backup.yaml
apps-pg-overflow-deploy: apps-pg-overflow-dry-run ## Provision only for an approved fourth consumer
$(KUBECTL) apply -f helm/apps-pg-2-cluster.yaml
$(KUBECTL) apply -f helm/apps-pg-2-networkpolicies.yaml
$(KUBECTL) apply -f helm/apps-pg-2-backup.yaml
apps-pg-status: ## Show apps-pg cnpg cluster health
$(KUBECTL) cnpg status apps-pg -n databases 2>/dev/null || \
$(KUBECTL) get cluster apps-pg -n databases -o wide

View file

@ -21,6 +21,7 @@
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
| workplan | RPF-WP-0018 | active | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| workplan | RPF-WP-0019 | active | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
| task | RAILIANCE-WP-0005-T03 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
@ -88,3 +89,7 @@
| task | RPF-WP-0018-T05 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T06 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T07 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0019-T01 | progress | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
| task | RPF-WP-0019-T02 | progress | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
| task | RPF-WP-0019-T04 | wait | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |

View file

@ -12,7 +12,8 @@ review_interval: 6m
## Context
`net-kingdom/canon/standards/tenancy-posture_v0.1.md` §8.2 proposes that
An earlier draft of
`net-kingdom/canon/standards/tenancy-posture_v0.1.md` §8.2 proposed that
database placement policy — dedicated versus shared, and when that changes —
be owned by `railiance-platform`, co-signed by `adaptive-pricing`.
`tenant-engine` raised the same gap independently on 2026-08-16: both patterns
@ -49,15 +50,18 @@ compliance position; S3 knows the substrate. A placement change needs both.
Putting the decision solely here would recreate the problem §8.2 exists to
avoid — an operations-shaped repo holding a commercial decision alone.
`adaptive-pricing` co-signature is requested, not assumed.
`adaptive-pricing` declined the standing co-signature on 2026-08-17. The
replacement is binding and machine-reconcilable: it publishes typed tier
minimums; this policy validates placement against them. Ownership stays here.
## Consequences
- `docs/placement-policy.md` is the operative document; this ADR is the
decision behind it.
- §19.8 is answered by obligation rather than by number: Rule P-4.1 requires
`platform-pg` and `apps-pg` to declare a ceiling before their third consumer.
Both are at two consumers.
- §19.8 is answered for `platform-pg` by `rapp-postgres` ADR-0004: four
consumers, with `platform-pg-2` named as the still-absent overflow target.
Rule P-4.1 still requires `apps-pg` to declare its own ceiling before its
third consumer.
- This repo owes `apps-pg`'s ceiling under its own rule. A policy whose author
is exempt is not a policy.
- §8.1 requires triggers to be *monitored*. This repo has no monitoring plane,
@ -65,8 +69,8 @@ avoid — an operations-shaped repo holding a commercial decision alone.
pending `railiance-telemetry`. Declaring a ceiling is a partial substitute —
a written threshold is checkable by a human at provisioning, which beats a
limit discovered by an OOM kill.
- If `adaptive-pricing` declines, this ADR is revised to record single
ownership explicitly rather than leaving an unsigned co-signature line.
- `adaptive-pricing`'s decline is recorded as single policy ownership plus a
mandatory typed constraint join, not as an absent signature.
## Alternatives considered

View file

@ -97,18 +97,30 @@ applied; coulomb-social connectivity smoke from labeled consumer ns OK.
## Backup And Roadmap
`apps-pg` starts as a conservative single-instance, 10Gi cluster to match
the current node capacity and existing CNPG footprint. Adding a replica,
PgBouncer/CNPG `Pooler`, resize policy, and CNPG-native backup coverage
are follow-up platform work items.
`apps-pg` remains a conservative single-instance, 10Gi cluster. The desired
state now includes continuous WAL archival, a daily 02:15 UTC base backup and
30-day retention under the distinct `apps-pg/` object-store prefix. The
credential lane is shared with `platform-pg`; the backup data path is not.
Until backup coverage is explicitly added, consumer onboarding should
record whether app data is disposable, externally reproducible, or
requires an immediate backup follow-up before production use.
The declared ceiling is three consumers. Each gets at most 20 connections;
40 of the explicit 100-connection aggregate remains for CNPG and operator
headroom. Memory (1Gi limit), not the clean connection refusal, is treated as
the binding safety constraint. A fourth consumer goes to the named
`apps-pg-2` overflow substrate, which must be provisioned before onboarding.
Reviewed, unapplied source for that cell is
`helm/apps-pg-2-{cluster,backup,networkpolicies}.yaml`; it uses a distinct
bootstrap Secret and backup prefix. `make apps-pg-verify-capacity` rejects a
fourth role on either cell, missing connection limits, duplicate roles, missing
resource envelopes, or a reused backup path.
`statement_timeout` and `idle_in_transaction_session_timeout` are 15 seconds
per consumer role. CNPG 1.28 cannot express those role settings, so
`helm/apps-pg-consumer-controls.sql` is the idempotent controlled-operator
step; `connectionLimit` remains declaratively reconciled by CNPG.
Resource evidence for `resource:railiance:apps-pg` (capacity, recovery,
labor, allocation drivers) is published under
`docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md`. The
2026-08-14 observation: no ScheduledBackup, railiance01 `apps-pg` is
not an Option A target, RPO is unbounded, RTO is undefined. That is
unresolved local-storage risk, not verified recovery.
2026-08-14 observation remains historically correct. The 2026-08-18 desired
state is not called verified recovery until the first backup succeeds and a
scratch restore artifact is recorded.

View file

@ -5,7 +5,7 @@ Effective: 2026-08-17
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` §4.4, §8
Contract version of this document: `1.0.0`
Status: **provisional** — binding on this repo now; formally conditional on
the framework reaching `accepted` and on `adaptive-pricing` co-signature.
the framework reaching `accepted`.
Answers `tenant-engine`'s 2026-08-16 request ("a default and the conditions
that override it, so migrations stop being decided by whichever neighbour was
@ -13,8 +13,9 @@ consulted") and Tenancy Posture §19.2.
## 1. Ownership, and its limit
§8.2 proposes placement ownership to `railiance-platform` co-signed by
`adaptive-pricing`. **Accepted, with the scope stated:**
Draft-8 §8.2 assigns the placement rule to `railiance-platform`, substrate
numbers to the package repo, workload requirements to the consumer and tier
minimums to `adaptive-pricing`. **Accepted, with the scope stated:**
> **This repo owns the rule. The package repo owns the number.**
@ -32,10 +33,11 @@ an observation only the operator of the package can make. Our obligation under
this policy is §4 below — that a ceiling and an overflow target **exist and
are published**. Theirs is what they are.
`adaptive-pricing` co-signature is **requested, not assumed**. The reasoning in
§8.2 is sound: tenancy model selection is commercial as much as technical. If
they decline, this document records single ownership and says so plainly rather
than leaving a co-signature line nobody signed.
`adaptive-pricing` declined a standing co-signature on 2026-08-17 and supplied
the stronger replacement adopted by draft-8: typed tier minima are joined to
consumer and provider declarations at tier definition and on change. This repo
therefore owns this policy singly; it must validate, not copy, the commercial
constraint artifact.
## 2. The default, and what overrides it
@ -73,7 +75,7 @@ triggers. It is not the same role as the operator of the cluster.
| Workload | Substrate | Provided level | Placement owner | Co-signer | §13 evidence |
| --- | --- | --- | --- | --- | --- |
| `audit-core` | `platform-pg` | P1 | `audit-core` | this repo | probes exist |
| `tenant-engine` | `platform-pg` | P1 | `tenant-engine` | this repo | probes exist |
| `tenant-engine` | SQLite PVC current; `platform-pg` desired | off-ladder current; P1 desired | `tenant-engine` | this repo | source conformance; live cutover absent |
| `user-engine` | `user-engine-pg` | P2 | `net-kingdom` | this repo | not assessed here |
| `target-revenue` | `target-revenue-pg` | P2 | `target-revenue` | this repo | not assessed here |
| `forgejo` | `forgejo-db` | P2 | this repo | — | single consumer |
@ -85,7 +87,7 @@ triggers. It is not the same role as the operator of the cluster.
§13.1 claims a level only with its artifact present. The P1P4 artifact is
*"provisioning declaration plus the platform's isolation probes"*.
`platform-pg` has them — `rapp-postgres` runs 15 adversarial probes against the
`platform-pg` has them — `rapp-postgres` runs 19 adversarial probes against the
consumer boundary. **`apps-pg` has none.** `scripts/capture-apps-pg-evidence.py`
captures *capacity* evidence for `resource-control`; it makes no isolation
assertion. So the P1 levels recorded above for `vergabe` and `coulomb_social`
@ -145,8 +147,8 @@ one". "Provision a second cluster" is an answer; "revisit at the time" is not.
| Cluster | Ceiling declared | Overflow target | Standing |
| --- | --- | --- | --- |
| `platform-pg` | not yet — `rapp-postgres` owes it | not yet | **2 consumers of ~6 practical; owed before the 3rd** |
| `apps-pg` | not yet — this repo owes it | not yet | 2 consumers; owed before the 3rd |
| `platform-pg` | 4 declarations (estimated; ADR-0004) | `platform-pg-2`, named but absent | 3/4 including the isolation probe; measure before the next workload |
| `apps-pg` | 3 consumers; 20 connections each, 1Gi memory binds first | `apps-pg-2` | 2/3; overflow must exist before a 4th |
| `forgejo-db` | n/a — single consumer | n/a | — |
Both live shared clusters are one consumer away from the trigger. This repo

View file

@ -55,29 +55,28 @@ provisions the role and CNPG `Database`, labels the namespace
RW endpoint: `apps-pg-rw.databases.svc.cluster.local:5432`. Bootstrap
role `apps_admin` is not a consumer credential.
**Quotas and limits** — read from `helm/apps-pg-cluster.yaml`, 2026-08-17:
**Quotas and limits** — declared in `helm/apps-pg-cluster.yaml` and
`helm/apps-pg-consumer-controls.sql`, 2026-08-18. These values are published
before the corresponding live rollout:
| Property | Value | Note |
| --- | --- | --- |
| `max_connections` | **PostgreSQL default (100)** | Not tuned in the CR. Not partitioned per consumer — there is no per-consumer connection allowance, so one consumer can exhaust the cluster. |
| `statement_timeout` | **none** | Not set. A runaway query runs until it finishes. |
| `idle_in_transaction_session_timeout` | **none** | Not set. |
| CPU / memory limits | **none declared** | The pod is BestEffort QoS and is the first thing evicted under node pressure. |
| `max_connections` | 100 | Explicit aggregate cap. Forty connections remain outside the declared three-consumer ceiling for operator/CNPG overhead. |
| Per-consumer connections | 20 | CNPG `managed.roles[].connectionLimit`, for both current roles. |
| `statement_timeout` | 15s | Per role; idempotent operator SQL because CNPG 1.28 has no managed-role settings field. |
| `idle_in_transaction_session_timeout` | 15s | Per role, same enforcement path. |
| CPU / memory | 100m / 256Mi requests; 1 CPU / 1Gi limits | Burstable QoS; memory is expected to bind before connections if workload size grows. |
| Instances | 1 | No HA. Restart recovery only, per the single-node rail. |
| Storage | 10Gi | |
| Backup | **none configured** | No `barmanObjectStore`, no `retentionPolicy`. |
| Effective erasure horizon | **undefined** | Follows from the line above. |
| Backup | 30d; daily 02:15 UTC + continuous WAL | S3 prefix `s3://railiance-platform-pg-backup/apps-pg/`; rollout is not evidence of recoverability until first backup and restore proof exist. |
| Effective erasure horizon | 30d, or the max across co-residents | Shared-cluster rule from placement policy §7. |
| Declared ceiling / overflow | 3 consumers / `apps-pg-2` | Memory is the binding safety constraint; provision the named overflow substrate before admitting a fourth. |
**Read the backup row before provisioning against this cluster.** `apps-pg`
has no backup at all — not a short retention, none. Recovery from data loss is
not available on this interface today. This is disclosed rather than fixed
because §10.2 requires disclosure now and the fix needs a backup target; it is
recorded as the R-axis gap in `docs/tenancy-posture.md` and is the highest
value item that declaration surfaced.
The unpartitioned connection pool is the same shape of risk `rapp-postgres`
reported for `platform-pg`: the bound is aggregate, so every consumer can be
politely inside its own expectations while the cluster still fails.
The manifests close the configuration gap, not the evidence gap. Until the
cluster change is applied and `LastBackupSucceeded=True` plus a scratch restore
are captured, recovery remains operationally unproven. Likewise the P1 noisy-
neighbour claim waits for the controlled probe window; source configuration is
not substituted for either artifact.
## `rapp-postgres.v1`
@ -99,7 +98,7 @@ Consumer is a `consumers[].declaration`. Boundary unit is
| Backup retention | 30d | Instance-wide, per `rapp-postgres` ADR-0002. |
| Effective erasure horizon | **30d, or the max across co-residents** | A consumer needing shorter cannot have it at P1. See `docs/placement-policy.md` §7. |
| Statement timeout | consumer-set | `tenant-engine` sets 5s for its own path; not a platform default. |
| Declared ceiling / overflow target | **not yet declared** | Owed by `rapp-postgres` before the third consumer, per `docs/placement-policy.md` Rule P-4.1. |
| Declared ceiling / overflow target | **4 declarations / `platform-pg-2`** | The isolation probe counts. `rapp-postgres` rejects a fifth declaration on one cell; ADR-0004 owns the number. |
Service classes currently co-resident on `platform-pg`: `latency-critical`
(`tenant-engine`) and `batch` (`audit-core`). Nothing prioritises between them

View file

@ -2,9 +2,13 @@
Work record: `RPF-WP-0018` T01
Declared: 2026-08-17
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` (draft-5, proposed)
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` (draft-8, proposed)
Contract version of this document: `1.0.0`
Machine-readable source: `tenancy.yaml`. This document preserves the review
reasoning; where a snippet and the root declaration differ, `tenancy.yaml` is
authoritative.
This is `railiance-platform`'s response to the Tenancy Posture ratification
test (§20.2): *"Each publishes its own posture vector as part of review. The
framework is validated by whether it can describe them accurately."*
@ -55,8 +59,8 @@ surface to the `rapp-*` repos while custody and policy stayed here.
tenancy:
service: apps-pg
role: storage-provider
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
service_class: interactive
reviewed: "2026-08-17"
gap:
@ -70,7 +74,7 @@ tenancy:
concept, carries no tenant claim, and applies no tenant predicate. The
tenant boundary for data on this cluster sits entirely in the consuming
S5 application. See the provider note below."
- "P is null rather than a number. See §P-axis note."
- "P is n/a rather than a number. See §P-axis note."
```
### `forgejo-db` — dedicated cluster for the forge
@ -79,8 +83,8 @@ tenancy:
tenancy:
service: forgejo-db
role: storage-provider
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
service_class: interactive
reviewed: "2026-08-17"
gap:
@ -97,8 +101,8 @@ tenancy:
tenancy:
service: openbao
role: credential-provider
current: { I: 0, A: 0, E: 0, P: 0, R: 0 }
target: { I: 0, A: 0, E: 0, P: 0, R: 1 }
current: { I: 0, A: 0, E: 0, P: 0, R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: 0, R: 1, V: 1 }
permanent: [I, A, E]
service_class: latency-critical
reviewed: "2026-08-17"
@ -145,7 +149,7 @@ great deal, it is central, and it is mechanically evidenced —
it *can* sign with `ssh/sign/agt-role`, and asserts it *cannot* read policy
metadata, then revokes by accessor. That is exactly the shape §13 asks for at
`A2`: choke point identified, unbound request refused. It is pointed at the
**consumer** boundary, and none of the five axes has anywhere to put it.
**consumer** boundary, and none of the tenancy axes has anywhere to put it.
That evidence is therefore cited under the provider statement rather than
against a consumer axis. Consumer separation in OpenBao is structural — a
@ -164,7 +168,7 @@ owner per consumer, and the triggers. The numbers — `instances: 1`,
`max_connections: 100`, `1Gi`, `retentionPolicy: 30d` — are `rapp-postgres`'s
cluster CR and its declaration to make.
## The P-axis note — why two entries are `null`
## The P-axis note — why two entries are `n/a`
The P ladder describes **which substrate holds a tenant's data**, from the
point of view of the workload whose data it is. `apps-pg` and `forgejo-db` are
@ -173,14 +177,18 @@ well-formed answer: `apps-pg` *provides* P1 to each of its consumers, and
`forgejo-db` *provides* P2 to forgejo, but neither *is* at a level.
Declaring `P: 1` for `apps-pg` would read as though the cluster had a
placement, and a later reader would use it to argue about isolation. `null`
plus the provider statement is the accurate form. This is routed as a finding.
placement, and a later reader would use it to argue about isolation. `n/a`
plus the provider statement is the accurate canonical form.
## Finding routed to `net-kingdom`
## Finding adopted by `net-kingdom`
**The five ladders describe a consumer of storage. They do not describe a
**The six ladders describe a consumer of storage. They do not describe a
provider of it.**
**Resolved in draft-7 and formalised in draft-8.** Decision 5.5 adopted the
provider declaration; the draft-8 schema standardises `available`, `maximum`,
conditions and evidence per axis. The canonical form is now in `tenancy.yaml`.
**Narrowed on re-reading, 2026-08-17.** An earlier version of this finding
claimed the framework had no way to say "this zero is structural, not weak".
That was wrong: §6 says exactly that, and §13.1a supplies the mechanism —
@ -208,15 +216,16 @@ rather than a tenant one. A reader scanning a table of E values would rank
OpenBao below a service doing per-query filtering in application code, which
inverts the actual security position.
**Proposed amendment, offered as the first correction from review rather than
**Proposed amendment at draft-5, offered as the first correction from review rather than
from research (§Status observes that every correction so far came from research
or relocation):** add a **provider declaration** beside the posture vector — a
platform service states, per axis, the maximum level it makes reachable and
what the consumer must do to reach it. For `apps-pg` that is: E4 unreachable
(shared credential per consumer, no per-tenant credential), E3 reachable once
the GUC contract exists, R2 blocked on a backup target. That is the sentence a
consumer actually needs, and no arrangement of the five consumer ladders
produces it.
consumer actually needs, and no arrangement of the consumer ladders produces
it. Draft-8 adopted and standardised that amendment; the root declaration is
now authoritative.
This is offered as an addition, not a replacement. The consumer vector is
right for consumers, and this repo is not asking for the ladders to be

View file

@ -0,0 +1,14 @@
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: apps-pg-2-daily
namespace: databases
labels:
cnpg.io/cluster: apps-pg-2
spec:
schedule: "0 15 2 * * *"
backupOwnerReference: self
cluster:
name: apps-pg-2
immediate: true
method: barmanObjectStore

View file

@ -0,0 +1,45 @@
# Provisionable overflow cell for the apps-pg three-consumer ceiling.
# Do not apply while empty; see docs/apps-pg.md.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: apps-pg-2
namespace: databases
labels:
app.kubernetes.io/name: apps-pg-2
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: manual
railiance.io/layer: s3-platform
railiance.io/role: shared-apps-database
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: "1", memory: 1Gi}
storage:
size: 10Gi
bootstrap:
initdb:
database: apps_meta
owner: apps_admin
secret:
name: apps-pg-2-credentials
postgresql:
parameters:
max_connections: "100"
log_connections: "on"
log_disconnections: "on"
log_lock_waits: "on"
log_min_duration_statement: "1000"
track_io_timing: "on"
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: s3://railiance-platform-pg-backup/apps-pg-2/
endpointURL: https://s3.nl-ams.scw.cloud
s3Credentials:
accessKeyId: {name: platform-pg-backup-s3, key: ACCESS_KEY_ID}
secretAccessKey: {name: platform-pg-backup-s3, key: ACCESS_SECRET_KEY}
wal: {compression: gzip, maxParallel: 2}
data: {compression: gzip, jobs: 2}

View file

@ -0,0 +1,54 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-kube-api-apps-pg-2
namespace: databases
spec:
podSelector:
matchLabels: {cnpg.io/cluster: apps-pg-2}
policyTypes: [Egress]
egress:
- ports:
- {port: 443, protocol: TCP}
- {port: 6443, protocol: TCP}
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
ports:
- {port: 53, protocol: UDP}
- {port: 53, protocol: TCP}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-from-cnpg-operator-apps-pg-2
namespace: databases
spec:
podSelector:
matchLabels: {cnpg.io/cluster: apps-pg-2}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: cnpg-system}
ports:
- {port: 5432, protocol: TCP}
- {port: 8000, protocol: TCP}
- {port: 9187, protocol: TCP}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-from-app-namespaces-apps-pg-2
namespace: databases
spec:
podSelector:
matchLabels: {cnpg.io/cluster: apps-pg-2}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {railiance.io/postgres-client: apps-pg-2}
podSelector: {}
ports:
- {port: 5432, protocol: TCP}

14
helm/apps-pg-backup.yaml Normal file
View file

@ -0,0 +1,14 @@
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: apps-pg-daily
namespace: databases
labels:
cnpg.io/cluster: apps-pg
spec:
schedule: "0 15 2 * * *"
backupOwnerReference: self
cluster:
name: apps-pg
immediate: true
method: barmanObjectStore

View file

@ -26,6 +26,13 @@ metadata:
spec:
instances: 1 # bump to 3 when node RAM > 8GB
imageName: ghcr.io/cloudnative-pg/postgresql:16
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
storage:
size: 10Gi
bootstrap:
@ -43,14 +50,41 @@ spec:
- name: vergabe # RAILIANCE-WP-0002 T04 (vergabe-teilnahme)
ensure: present
login: true
connectionLimit: 20
passwordSecret:
name: vergabe-app-credentials
- name: coulomb_social # CSOC-WP-0002 (coulomb-social)
ensure: present
login: true
connectionLimit: 20
passwordSecret:
name: coulomb-social-app-credentials
postgresql:
parameters:
max_connections: "100"
log_connections: "on"
log_disconnections: "on"
log_lock_waits: "on"
log_min_duration_statement: "1000"
track_io_timing: "on"
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: s3://railiance-platform-pg-backup/apps-pg/
endpointURL: https://s3.nl-ams.scw.cloud
s3Credentials:
accessKeyId:
name: platform-pg-backup-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: platform-pg-backup-s3
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 2
data:
compression: gzip
jobs: 2
# HA replica + connection pooler are deferred (RAILIANCE-WP-0003 Notes):
# services:
# additional:

View file

@ -0,0 +1,7 @@
-- Idempotent controls not expressible in the installed CNPG managed.roles CRD.
-- Apply as the controlled apps-pg cluster administrator only after notifying
-- both consumers of the published limits in docs/s3-consumer-interfaces.md.
ALTER ROLE vergabe SET statement_timeout = '15s';
ALTER ROLE vergabe SET idle_in_transaction_session_timeout = '15s';
ALTER ROLE coulomb_social SET statement_timeout = '15s';
ALTER ROLE coulomb_social SET idle_in_transaction_session_timeout = '15s';

View file

@ -22,8 +22,19 @@ spec:
- Egress
egress:
- ports:
- port: 443
protocol: TCP
- port: 6443
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy

126
tenancy.yaml Normal file
View file

@ -0,0 +1,126 @@
# Canonical vector set for services owned or held in custody here.
# Human reasoning remains in docs/tenancy-posture.md.
schema_version: "0.1"
framework: netkingdom-tenancy-posture
services:
- service: apps-pg
role: relational-storage-provider
tenancy:
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
reviewed: "2026-08-17"
review_due: "2027-02-17"
service_class: interactive
permanent: [I, A, E, P]
reason:
I: "The storage provider receives credentials, not tenant claims."
A: "No tenant context is bound at the database provider."
E: "Tenant enforcement belongs to each consuming application."
P: "apps-pg provides P1; a substrate is not itself at a P level."
R: "A backup target and 30-day retention are declared in source and
not yet applied; no successful backup or restore exists. R stays 0
until evidenced, per §13.1 — declared configuration is not an
artifact."
V: "No recovery exercise or failover evidence exists."
gap:
R: "RPF-WP-0019 establishes a backup target and declared horizon."
V: "Exercise recovery after the backup target is established."
provider:
capability: data.relational
profile: s5-application-databases
axes:
E:
available: 2
maximum: 3
conditions:
- "E2 is entirely consumer-side."
- "E3 requires adoption of the rapp-postgres GUC contract."
- "E4 is unreachable on a shared cluster without per-tenant credentials."
P:
available: 1
maximum: 2
conditions:
- "P2 requires a new dedicated cluster for the consumer."
R:
available: 0
maximum: 2
conditions:
- "A governed backup target must exist before R2 is available."
V:
available: 0
maximum: 1
conditions:
- "Single-node recovery must be documented and exercised."
- service: forgejo-db
role: relational-storage-provider
tenancy:
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
reviewed: "2026-08-17"
review_due: "2027-02-17"
service_class: interactive
permanent: [I, A, E, P]
reason:
I: "The database receives a Forgejo credential, not tenant claims."
A: "No NetKingdom tenant context is bound at this provider."
E: "Tenant enforcement, if any, belongs to Forgejo."
P: "forgejo-db provides P2 to Forgejo; it is not itself at P2."
R: "A backup procedure exists, but no declared retention or erasure horizon does."
V: "No exercised recovery objective or failover evidence exists."
gap:
R: "Declare the backup window and publish the effective erasure horizon."
V: "Exercise recovery and record the interruption."
provider:
capability: data.relational
profile: dedicated-forgejo-database
axes:
P:
available: 2
maximum: 2
R:
available: 0
maximum: 2
conditions:
- "Declare and evidence retention on the existing backup path."
V:
available: 0
maximum: 1
conditions:
- "Single-node recovery must be exercised."
- service: openbao
role: credential-provider
tenancy:
current: { I: 0, A: 0, E: 0, P: 0, R: 0, V: 0 }
target: { I: 0, A: 0, E: 0, P: 0, R: 1, V: 1 }
reviewed: "2026-08-17"
review_due: "2027-02-17"
service_class: latency-critical
permanent: [I, A, E, P]
reason:
I: "OpenBao authenticates workloads and does not resolve NetKingdom tenants."
A: "No tenant context is carried or bound."
E: "OpenBao holds workload secrets, not tenant-keyed records."
P: "One instance is shared by every credential consumer."
R: "No retention position covers audit output or KV version history."
V: "Node recovery requires operator unseal; no recovery objective is evidenced."
gap:
R: "Declare the platform default for audit output and KV history."
V: "Exercise the complete restart and unseal path against a recovery objective."
provider:
capability: credential.vending
axes:
E:
available: 0
maximum: 4
conditions:
- "Per-tenant paths, roles and credentials are required for tenant-level E4."
- "Current evidence proves consumer-boundary scoping, not tenant-boundary scoping."
evidence:
- "scripts/openbao-verify-token-grants.py"
V:
available: 0
maximum: 1
conditions:
- "Single-node restart and unseal recovery must be exercised."

View file

@ -0,0 +1,47 @@
from __future__ import annotations
from copy import deepcopy
import importlib.util
from pathlib import Path
import sys
import pytest
import yaml
ROOT = Path(__file__).parents[1]
SPEC = importlib.util.spec_from_file_location(
"verify_apps_pg_capacity", ROOT / "tools/verify_apps_pg_capacity.py"
)
capacity = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = capacity
SPEC.loader.exec_module(capacity)
MAX_CONSUMERS = capacity.MAX_CONSUMERS
verify = capacity.verify
MANIFESTS = [ROOT / "helm/apps-pg-cluster.yaml", ROOT / "helm/apps-pg-2-cluster.yaml"]
def test_reviewed_apps_pg_cells_fit_capacity_and_use_distinct_backups() -> None:
assert verify(MANIFESTS) == {"apps-pg": 2, "apps-pg-2": 0}
def test_fourth_role_is_rejected(tmp_path: Path) -> None:
primary = yaml.safe_load(MANIFESTS[0].read_text(encoding="utf-8"))
template = deepcopy(primary["spec"]["managed"]["roles"][0])
while len(primary["spec"]["managed"]["roles"]) <= MAX_CONSUMERS:
role = deepcopy(template)
role["name"] = f"consumer_{len(primary['spec']['managed']['roles'])}"
primary["spec"]["managed"]["roles"].append(role)
path = tmp_path / "apps-pg.yaml"
path.write_text(yaml.safe_dump(primary), encoding="utf-8")
with pytest.raises(ValueError, match="3-consumer ceiling"):
verify([path, MANIFESTS[1]])
def test_unbounded_role_is_rejected(tmp_path: Path) -> None:
primary = yaml.safe_load(MANIFESTS[0].read_text(encoding="utf-8"))
del primary["spec"]["managed"]["roles"][0]["connectionLimit"]
path = tmp_path / "apps-pg.yaml"
path.write_text(yaml.safe_dump(primary), encoding="utf-8")
with pytest.raises(ValueError, match="connectionLimit"):
verify([path, MANIFESTS[1]])

View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
import yaml
MAX_CONSUMERS = 3
MIN_RESERVED_CONNECTIONS = 40
REVIEWED_CELLS = ("apps-pg", "apps-pg-2")
def verify(paths: list[Path]) -> dict[str, int]:
cells: dict[str, dict] = {}
role_owner: dict[str, str] = {}
prefixes: dict[str, str] = {}
for path in paths:
document = yaml.safe_load(path.read_text(encoding="utf-8"))
if document.get("kind") != "Cluster":
raise ValueError(f"{path}: expected a CNPG Cluster")
name = document["metadata"]["name"]
if name not in REVIEWED_CELLS:
raise ValueError(f"{path}: {name!r} is not a reviewed apps-pg cell")
cells[name] = document
missing = set(REVIEWED_CELLS) - cells.keys()
if missing:
raise ValueError(f"missing reviewed overflow desired state: {sorted(missing)}")
counts: dict[str, int] = {}
for name, document in cells.items():
spec = document["spec"]
roles = spec.get("managed", {}).get("roles", [])
if len(roles) > MAX_CONSUMERS:
raise ValueError(
f"{name}: {len(roles)} roles exceeds the {MAX_CONSUMERS}-consumer ceiling"
)
max_connections = int(spec["postgresql"]["parameters"]["max_connections"])
allocated = 0
for role in roles:
role_name = role["name"]
if role_name in role_owner:
raise ValueError(
f"role {role_name!r} appears in both {role_owner[role_name]} and {name}"
)
role_owner[role_name] = name
limit = role.get("connectionLimit")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1:
raise ValueError(f"{name}/{role_name}: positive connectionLimit is required")
allocated += limit
if allocated + MIN_RESERVED_CONNECTIONS > max_connections:
raise ValueError(
f"{name}: {allocated} consumer connections leave fewer than "
f"{MIN_RESERVED_CONNECTIONS} reserved connections"
)
resources = spec.get("resources", {})
if not resources.get("requests") or not resources.get("limits"):
raise ValueError(f"{name}: pod requests and limits are required")
prefix = spec["backup"]["barmanObjectStore"]["destinationPath"]
if prefix in prefixes:
raise ValueError(f"{name}: backup prefix is shared with {prefixes[prefix]}")
prefixes[prefix] = name
counts[name] = len(roles)
return counts
def main() -> int:
parser = argparse.ArgumentParser(description="Verify apps-pg cell admission controls")
parser.add_argument("manifests", nargs="+", type=Path)
args = parser.parse_args()
try:
counts = verify(args.manifests)
except (KeyError, TypeError, ValueError, yaml.YAMLError) as error:
parser.error(str(error))
print(
"apps-pg capacity verified: "
+ ", ".join(f"{name}={counts[name]}/{MAX_CONSUMERS}" for name in REVIEWED_CELLS)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -4,11 +4,11 @@ type: workplan
title: "Align S3 with the estate policy surface (Tenancy Posture + policy-nexus)"
domain: financials
repo: railiance-platform
status: active
status: finished
owner: codex
topic_slug: railiance
created: "2026-08-17"
updated: "2026-08-17"
updated: "2026-08-18"
related:
- POLICY-NEXUS-WP-0001
- TEN-WP-0009
@ -297,3 +297,37 @@ this workplan routes the finding and does not build the ingress.
**A declaration that goes stale the day it is written.** Mitigation is the
`reviewed:` date on the vector set and the review interval in the ADR
template — the same currency mechanism `policy-nexus` T05 will read.
## Closed 2026-08-18
All seven tasks done. What the workplan produced, and what it changed
elsewhere:
- `docs/tenancy-posture.md` + `tenancy.yaml` — the vector set, machine-readable
with the human reasoning kept beside it.
- `docs/placement-policy.md` — placement rule, owner per workload, triggers,
retention floor and ceiling.
- `docs/s3-consumer-interfaces.md` `1.1.0` — quota disclosure.
- `docs/adr/` — created from nothing; `ADR-0001`, `ADR-0002`, `ADR-0003`.
**Two findings were adopted upstream.** The provider-declaration proposal (F4,
narrowed) is in the framework and its canonical form is the `provider:` block
in `tenancy.yaml`. `adaptive-pricing` declined the standing co-signature and
supplied a stronger replacement — typed tier minima joined at tier definition —
which draft-8 adopted; `ADR-0002` records the outcome as single ownership plus
a mandatory typed constraint join, not as an absent signature.
**Three corrections were issued against our own output**, all in the same
direction — claiming levels we could not evidence. `openbao A: 2` retracted to
`A: 0`; the provider finding narrowed once §6's `flex-auth` precedent was read;
the §11 summary to `adaptive-pricing` corrected, since §11.2 keeps marketing
vocabulary free and only the *cannot-reach* claim is restricted, to E4. All
three are recorded in the documents rather than edited away.
**What this workplan deliberately did not do:** fix anything. It found that
`apps-pg` had no backup, no per-consumer controls and no isolation probes, and
published those as visible defects. `RPF-WP-0019` closes them.
**Left open, not owned here:** `bao.coulomb.social` still needs confirmation
against live reef state (`railiance-master` `ba477968`), and F1's substrate
split is with `policy-nexus` to act on.

View file

@ -4,15 +4,16 @@ type: workplan
title: "apps-pg: backup, per-consumer controls, and the isolation probes they make possible"
domain: financials
repo: railiance-platform
status: proposed
status: active
owner: codex
topic_slug: railiance
created: "2026-08-17"
updated: "2026-08-17"
updated: "2026-08-18"
related:
- RPF-WP-0018
origin: residual
origin_ref: RPF-WP-0018
state_hub_workstream_id: "160e226d-27b0-4c94-9e99-331f4354dd12"
---
# RPF-WP-0019 — apps-pg recoverability and per-consumer controls
@ -65,12 +66,43 @@ the `platform-pg-backup-s3` credential lane — the same handoff
`make postgres-backup-deploy` waits on. Check whether that is now live before
assuming this is blocked.
## Status 2026-08-18 — repository-complete, live-blocked
Everything this repo can do without touching the cluster is done and
committed. What remains on T01, T02 and T04 is a single operator window
against a live shared rail, in this order:
1. `make apps-pg-deploy` — Cluster reconcile: role connection limits,
Burstable requests/limits, explicit aggregate parameters.
2. Apply `helm/apps-pg-consumer-controls.sql` — the two 15s role timeouts.
Idempotent; CNPG 1.28 has no managed-role settings field, so this is
operator SQL by necessity, not by preference.
3. `make apps-pg-backup-deploy` — the ScheduledBackup, once the governed
Secret is confirmed live.
4. Capture `LastBackupSucceeded=True` and a scratch restore. **Until both
exist, `apps-pg` R stays 0** — declared configuration is not a §13
artifact, and this workplan exists because that distinction was missed once
already.
5. T04's probes, in an announced window, after 13 have settled.
**Blocker as of this session:** the cluster is unreachable from the
workstation — `kubectl` returns `Unauthorized` against
`config-hosteurope`. Credentials or the tunnel need attention before any of
the above runs. This is a session-local access problem, not a defect in the
manifests: `make apps-pg-verify-capacity` passes and the capacity tests are
green.
**Do not treat the rollout as evidence.** T04's P1 claim and the R-axis both
need artifacts produced *after* application, and `docs/placement-policy.md`
§3.1 and `tenancy.yaml` should be updated only then.
## Tasks
```task
id: RPF-WP-0019-T01
status: todo
status: progress
priority: high
state_hub_task_id: "0f5175c7-bde3-46a0-87a8-b50b5eeac32c"
```
**Establish a backup target for `apps-pg`.** Confirm the state of the
`resource-control` bucket and the `platform-pg-backup-s3` OpenBao Secret; if
@ -78,10 +110,17 @@ live, configure `barmanObjectStore` and a `retentionPolicy` on the cluster. If
not live, record the dependency and say so — do not leave the absence
undocumented a second time.
2026-08-18 repository readiness: the governed Secret exists live and reviewed
desired state now carries a distinct `apps-pg/` prefix, 30-day retention,
continuous WAL and a daily 02:15 backup. NetworkPolicy admits the DNS/HTTPS
path archival needs. The ScheduledBackup is not applied and no successful
backup/restore evidence exists, so T01 remains progress.
```task
id: RPF-WP-0019-T02
status: todo
status: progress
priority: high
state_hub_task_id: "c5b0b2ac-b9f5-42fb-8e75-2fac2ab1e852"
```
**Declare and enforce per-consumer controls.** Per-consumer connection
allowance, `statement_timeout`, `idle_in_transaction_session_timeout`, and
@ -90,20 +129,34 @@ every value in `docs/s3-consumer-interfaces.md` before it takes effect —
§10.2 is a disclosure rule, and applying a timeout consumers learn about by
hitting it would breach the rule while implementing it.
2026-08-18 repository readiness: both roles declare a 20-connection limit,
the pod has Burstable requests/limits, aggregate/logging parameters are
explicit, and controlled operator SQL sets both 15s role timeouts. Every value
was published in `docs/s3-consumer-interfaces.md` before application. Live SQL
and Cluster reconciliation remain an operator window, so T02 remains progress.
```task
id: RPF-WP-0019-T03
status: todo
status: done
priority: medium
state_hub_task_id: "736cbc11-1992-4f1b-9ff0-cb4622ff39a5"
```
**Declare the ceiling and overflow target.** Owed under this repo's own
Rule P-4.1 before `apps-pg`'s third consumer; it is at two. Name the binding
resource per Rule P-4.2 — memory or connections — and a named overflow
substrate per P-4.3.
Completed 2026-08-18. The declared ceiling is three, memory is the binding
constraint, and `apps-pg-2` is a named, source-provisionable overflow cell with
a distinct credential and backup prefix. `make apps-pg-verify-capacity`
rejects a fourth consumer per cell and unbounded/duplicate roles. The cell
intentionally remains absent until a fourth consumer is approved.
```task
id: RPF-WP-0019-T04
status: todo
status: wait
priority: medium
state_hub_task_id: "2fccdd26-c9c6-43cb-b069-27ed5668afac"
```
**Isolation probes, after T02.** Consumer-boundary probes on the
`rapp-postgres` model, then the §13 noisy-neighbour artifact: per-consumer
@ -112,6 +165,10 @@ judged against each consumer's declared service class. Update
`docs/placement-policy.md` §3.1 and `docs/tenancy-posture.md` when the P1
claims become evidenced.
Waiting on T02 live application and an announced probe window. No saturation
or destructive recovery experiment is run against the shared production rail
as part of repository preparation.
## Boundaries
- `apps-pg` only. `platform-pg`'s equivalents are `rapp-postgres`'s.