TazLab K8s: tazlab-db Detail
Level 3 (Detail) — PostgreSQL cluster configuration, databases, users, backup.
Concept
The cluster runs a single shared PostgreSQL instance managed by Crunchy PostgreSQL Operator (PGO, 5.7.2). It serves every stateful workload in the lab: Mnemosyne and Hindsight (vector memory), Grafana (dashboards), pgAdmin, the TazLab test/validation databases, and Multica (Paperclip was removed on 2026-10-19).
It is on PostgreSQL 17.2 since 2026-09-27 (major upgrade from 16, §The 17 major upgrade).
PostgresCluster
File: infrastructure/instances/tazlab-db/cluster.yaml
| Field | Value |
|---|---|
| Name | tazlab-db |
| Namespace | tazlab-db |
| PostgreSQL version | spec.postgresVersion: 17 — status.postgresVersion: 17; the server reports PostgreSQL 17.2 (server_version=17.2, data directory /pgdata/pg17) |
| Server image | crunchy-postgres:ubi8-17.2-1 (PGO 5.7.2 operator image ubi8-5.7.2-0) |
| Instances | 1 (instance1) |
| Storage | 4Gi RWO, StorageClass tazlab-storage |
| pgBouncer | Enabled, 1 replica |
| Patroni | Dynamic configuration with custom pg_hba |
Databases and Users
Six roles are declared in spec.users (verified live 2026-10-20):
| User | Databases | Purpose |
|---|---|---|
pgadmin | postgres | pgAdmin web UI connection |
mnemosyne | mnemosyne | Semantic memory (vector search) |
tazlab-admin | tazlab, tazlab-test | Main app + rebirth validation |
grafana | grafana | Grafana dashboards backend |
hindsight | hindsight | Hindsight multi-agent memory (vector search) |
postgres | postgres | Superuser identity used by the Git-declared one-shot Jobs |
paperclip | Removed 2026-10-19 with the Paperclip decommission; the (DATABASE, USER, ADDRESS) reject-row pattern is now carried by the pgadmin row (host postgres pgadmin) | |
buzz | Removed 2026-10-19 with the Buzz decommission | |
multica | Removed 2026-10-20 with the Multica decommission |
Initialization SQL
File: infrastructure/instances/tazlab-db/init-sql.yaml
Applied as ConfigMap tazlab-db-init-sql. On first cluster creation, it:
mnemosyne database
- Creates
vectoranduuid-osspextensions - Creates
memoriestable with UUID PK,VECTOR(3072)embedding column - Creates
archived_filestable for session archival tracking
tazlab database
- Creates a
memoriestable (different schema — serial PK, JSONB metadata/tags)
tazlab-test database
- Creates
rebirth_testtable - Inserts a row with timestamp for rebirth validation
grafana database
- Grants all privileges to
grafanauser
S3 Backup
File: infrastructure/configs/tazlab-db/s3-external-secret.yaml
PGBackrest configuration:
| Field | Value |
|---|---|
| Stanza | db |
| Repo | repo1 — S3 (tazlab-storage, eu-central-1) |
| Path | /pgbackrest/repo1 |
| Full schedule | Weekly (Sunday 01:00) |
| Differential | Daily (Monday–Saturday 01:00) |
| Restore | --delta mode |
| Repo host image | crunchy-pgbackrest:ubi8-2.54.0-0 |
Credentials are injected via ExternalSecret s3-backrest-creds which renders the PGBackrest config file format.
One-off backups are declared, not run by hand: the annotation postgres-operator.crunchydata.com/pgbackrest-backup in metadata makes PGO create the Job, and its value is a counter that must change for each new backup (PGO consumes it once). spec.backups.pgbackrest.manual selects the type (--type=full). Both were used for the 17 upgrade (pre-upgrade-pg17-20260927-0955, then post-upgrade-pg17-20260927-1014) — the second one because a major upgrade starts a new WAL timeline, which invalidates the differentials taken on the old one (live timeline after the upgrade: 3).
Within the maintenance window the schedules were commented out, not left running: with the instance scaled to zero a scheduled backup fails immediately instead of queueing, and a failed stanza is worse than a paused one. They were restored in the same commit that set postgresVersion: 17.
The 17 major upgrade (2026-09-27)
Executed with PGO’s declarative object — PGUpgrade plus shutdown: true plus the postgres-operator.crunchydata.com/allow-upgrade annotation in one commit, then removal of the CR, then the postgresVersion: 17 bump. The whole sequence is in Git, in order:
| Commit | Step |
|---|---|
5bbd96a | spec.dataSource removed (it would have re-cloned 16 over the new data directory) |
2b49844 | one-off full backup as the restore base |
2876c6e | maintenance window: shutdown: true, schedules suspended |
343ecd4 | spec.transferMethod dropped from the PGUpgrade object |
dbb7456 | upgrade succeeded — the PGUpgrade object removed |
08c65f2 | postgresVersion: 17, shutdown: false, schedules restored |
3ad7659 | a second full backup, on the new timeline |
Measured cost: ~90 seconds of data work, ~6 minutes of total downtime; 80 pods healthy afterwards and every consumer re-checked with a real query (hindsight, paperclip, mnemosyne, grafana).
Two facts about the path, worth keeping because they cost time:
spec.transferMethoddoes not exist in PGO 5.7.2’s CRD. It came from the 6.0.x documentation a research pass had cited, and the API server refused the object (field not declared in schema). The image tags were wrong the same way (17.2-1, not17.2-0).- There is no in-place rollback. A major upgrade does not go back; the revert path of the Longhorn volume does not exist on a bound PVC and the CSI snapshotter is not installable in this cluster. The accepted way back is restore from the pre-upgrade full backup — which is why that backup is a step in Git rather than a precaution. The operator accepted the irreversibility explicitly before the window opened.
Extensions after the upgrade
pgaudit is re-created at 17.0 in the databases that carry it, and vector is upgraded with ALTER EXTENSION vector UPDATE only — never DROP/CREATE, which destroys the columns and indexes that depend on it. Extension work is declared as one-shot Jobs (hindsight-db-extensions-job.yaml), because databaseInitSQL runs only at first initialisation.
Verified live 2026-09-28 — pgaudit 17.0 in postgres, mnemosyne, grafana, hindsight, tazlab, paperclip; vector 0.8.0 in mnemosyne and hindsight; pgaudit.log = none (preloaded, auditing not switched on). Exception: the multica database reports pgaudit 16.0, and so does template1 — a database created from template1 after the upgrade inherits the stale catalog row. See Known Issues.
Service exposure
File: infrastructure/instances/tazlab-db/tailnet-service.yaml
| Field | Value |
|---|---|
| Name | tazlab-db-tailnet |
| Type | LoadBalancer, loadBalancerClass: tailscale |
| Address | 100.105.253.2 — tazlab-db.magellanic-gondola.ts.net |
| Port | 5432 → NodePort 32227, pinned on purpose |
| Selector | the primary instance (postgres-operator.crunchydata.com/role: master) |
This is the cluster’s only external PostgreSQL path. The older MetalLB service (tazlab-db-external, 192.168.1.241) was disabled on 2026-05-23 and is no longer a resource in the repository — a page or a runbook that still points at 192.168.1.241 is reading an old revision. The NodePort is pinned because the Tailscale operator would otherwise allocate it dynamically: a Service recreation or a cluster rebuild could move the port silently, and out-of-cluster consumers (the TazPod containers, project paperclip-lxc-deployment) address it literally.
Users Secret Format
PGO generates a Secret for each user in the format <cluster-name>-pguser-<username> (e.g., tazlab-db-pguser-mnemosyne). Each Secret contains:
user,password,host,port,dbname,uri,jdbc-uri
Applications reference these secrets via secretKeyRef in their env vars.
Server TLS (Vault PKI)
File: infrastructure/instances/tazlab-db/cluster.yaml (PostgresCluster spec)
| Field | Value |
|---|---|
customTLSSecret | tazlab-db-server-tls |
customReplicationTLSSecret | tazlab-db-server-tls |
Both the Postgres server and replication traffic use the same Vault PKI secret, provisioned via a VaultPKISecret resource that requests a server certificate from the Vault PKI engine. This replaces the default PGO-generated self-signed cert with a trusted CA-signed certificate.
PGO CA Trust Bundle
PGO’s operator-managed root CA and the Vault PKI intermediate CA are combined into a single trusted CA bundle. This ensures PostgreSQL accepts both:
- Certificates signed by the PGO internal CA (operator-internal communication)
- Certificates signed by the Vault PKI intermediate (client mTLS connections)
The bundle is injected into the PostgresCluster’s customTLSSecret configuration so the server presents the Vault-issued cert while trusting clients signed by either CA.
Client VaultPKISecrets
Six VaultPKISecret resources issue client certificates for authentication to the database (verified live 2026-09-28; notice that each lives in the namespace of its consumer, not in tazlab-db):
| Secret Name | Namespace | Used By |
|---|---|---|
db-client-grafana-tls | monitoring | Grafana sidecar cert copy |
db-client-mnemosyne-tls | tazlab-db | Mnemosyne MCP service |
db-client-pgadmin-tls | tazlab-db | pgAdmin web UI |
db-client-tazlab-admin-tls | tazlab-db | TazLab application |
db-client-hindsight-tls | hindsight | Hindsight (vector memory) |
db-client-multica-tls | multica | Multica backend (/etc/certs, mounted by a postRenderer patch) |
Each secret requests a client certificate from the Vault PKI intermediate, enabling certificate-based authentication (pg_hba cert method) for those users.
Application mTLS Authentication
Every application in the cluster authenticates with a client certificate. Paperclip was the one exception (password + md5); it was removed on 2026-10-19, and the reject-row pattern it demonstrated lives on in the multica row. The Vault Database Engine (database/) has been removed — no dynamic credentials are needed.
File: infrastructure/instances/tazlab-db/cluster.yaml (PostgresCluster patroni.dynamicConfiguration)
The live pg_hba block (28 rules, verified 2026-10-20) is order-significant: pg_hba is first-match-wins, so each cert rule that should be the only way in must be followed by a reject rule scoped to that user before the generic 10.0.0.0/8 / 10.244.0.0/16 md5 rows at the bottom. The rows are also (DATABASE, USER, ADDRESS) — a reject written as host pgadmin all would let the user fall through to the generic rules on every other database; it must read host postgres pgadmin.
hostssl grafana grafana 0.0.0.0/0 cert # then md5 fallbacks for grafana's own user (retained)
hostssl mnemosyne mnemosyne 0.0.0.0/0 cert
hostssl hindsight hindsight 0.0.0.0/0 cert
hostssl postgres pgadmin 0.0.0.0/0 cert
host postgres pgadmin 0.0.0.0/0 reject
host tazlab-admin all 0.0.0.0/0 md5
host all all 10.0.0.0/8 md5
host all all 10.244.0.0/16 md5
(IPv6 ::/0 rows mirror each 4-address rule; the listing above shows one family for readability.)
Grafana
Certificate: db-client-grafana-tls (CN=grafana). Mounted via extraInitContainer that copies from VaultPKISecret. grafana.ini.database sets ssl_mode: require, ca_cert_path, client_cert_path, client_key_path.
mnemosyne
Certificate: db-client-mnemosyne-tls (CN=mnemosyne). Go code refactored in 12-mnemosyne-mtls-refactor to support sslmode=verify-full. DB_PASS and md5 rules removed. Verified via pg_stat_ssl: ssl=t, client_dn=/CN=mnemosyne.
pgAdmin
Certificate: db-client-pgadmin-tls (CN=pgadmin). Pre-configured servers.json ConfigMap with cert paths pre-filled. rolloutRestartTargets added for cert rotation.
paperclip (password, deliberately)
hostssl paperclip paperclip … md5 plus user-scoped rejects. The application hands its connection URL to postgres.js and never builds an ssl option, so sslcert/sslkey/sslrootcert are ignored and a certificate cannot be presented. The rejects are what keep the role out of every other database. Its connection string uses sslmode=require against the pinned LAN NodePort 192.168.1.211:32227, and the server certificate carries only in-cluster names, so hostname verification is not available from there (TD-085).
Vault Database Engine — Removed
The database/ engine has been disabled (vault secrets disable database, 2026-07-09). All apps use mTLS — no dynamic credentials needed. TD-044 closed. The Terraform configuration remains in the repository for re-creation if needed.
Schema grants for late-added databases
databaseInitSQL (tazlab-db-init-sql) runs only at first cluster initialisation, so a database added afterwards does not get its schema ownership from it. The Flux-managed equivalent is a one-shot Job: multica-db-grants-job.yaml (GRANT ALL ON SCHEMA public + ALTER SCHEMA public OWNER TO multica, run as the postgres identity). The Job carries manualSelector: true — without it the API server injects controller-uid/job-name labels into spec.template on creation, the next server-side apply sees an immutable diff, and the whole instances Kustomization freezes until the Job is deleted by hand.
DAG Position
operators-data (Level 0, installs PGO)
→ configs (Level 2, creates s3-backrest-creds ExternalSecret)
→ instances (Level 3, creates PostgresCluster + services + grants Jobs)
→ apps-data (Level 3, mnemosyne-mcp depends on DB being ready)
Known Issues
- The
memoriestable is created withVECTOR(3072)butgemini-embedding-001produces 768-dim vectors (TD-002). This works because the init SQL runs only on first creation and the dimension mismatch is handled by the auto-detection fallback. - Only 1 replica — no HA for the database. Acceptable for lab, inadequate for production.
pgauditcatalog version is stale inmulticaandtemplate1(16.0 while the binaries are 17.0). Observed 2026-09-28:pg_available_extensionsreports default17.0, the other six databases report17.0, themulticadatabase andtemplate1report16.0. The server is unaffected today (pgaudit.log = none, the module is loaded from the 17 binary), but a database created fromtemplate1inherits the stale row and a dump/restore ofmulticawould carry a version the 17 binaries do not offer. Remediation isALTER EXTENSION pgaudit UPDATEin both — reported to the Cluster agent, not done by the wiki.- A major upgrade has no in-place rollback (§The 17 major upgrade): the only way back is restoring the pre-upgrade full backup. Treat any future major bump as a one-way door.
- The Flux dependency stall observed during the upgrade (TD-096) is an operator-layer defect, not a database one:
infrastructure-instancesstaysDependencyNotReadyafter every commit ontazlab-k8seven when the dependency isReady, so instance changes need ordered forced reconciliations (vso→configs→instances→auth).
See Also
- Parent topics: Repository Mapping, Operators Inventory
- Sibling details: External Secrets Detail, Mnemosyne Deployment Detail
- Consumers: Multica Detail · Hindsight entity · Paperclip container detail
- Reference: ExternalSecret Example