Storage Backends on Kubernetes

The Helm chart auto-wires four databases on a single PostgreSQL or MySQL server when either is enabled:

  • management — accounts, peers, groups, policies, setup keys
  • flow — traffic events
  • activity — audit log
  • dex — IdP storage (when Dex is enabled)

All four databases share the same connection user (openzro by default). A pre-install Helm hook creates them if they don't exist; the management daemon and Dex run their own schema migrations on first boot.

The chart points at an external Postgres — Cloud SQL, AWS RDS, Cloud Spanner with PG dialect, self-hosted with replication, etc. Bundled subchart was removed; for HA the production target is a managed service.

postgres:
  enabled: true
  host: 10.x.x.x          # private IP / hostname of your Postgres
  port: 5432
  sslMode: require        # see SSL section below
  username: openzro
  password: "REPLACE_WITH_PG_PASSWORD"
  databases:
    management: openzro
    flow: openzro_flow
    activity: openzro_activity
    dex: dex
  provisioning:
    enabled: true         # CREATE DATABASE if absent (idempotent)
    username: openzro     # DBA user with CREATEDB privilege
    password: "REPLACE_WITH_PG_PASSWORD"

Pre-install Job behavior

When provisioning.enabled: true the chart runs a pre-install / pre-upgrade Helm hook that:

  1. Connects as provisioning.username to the postgres admin DB
  2. Iterates through the four database names from postgres.databases
  3. Skips any that already exist (SELECT 1 FROM pg_database)
  4. CREATE DATABASE for the missing ones with the runtime user as OWNER

The user used by the Job needs CREATEDB. The runtime user used by the management daemon and Dex only needs CREATE/ALTER TABLE inside its own databases — covered automatically by the OWNER grant.

When to disable provisioning

Cloud-managed Postgres (Cloud SQL, RDS) often restricts the CREATEDB global privilege to a separate admin user. Two ways to live without it:

postgres:
  provisioning:
    enabled: false

Then ask your DBA to pre-create the four databases:

CREATE DATABASE openzro            OWNER openzro;
CREATE DATABASE openzro_flow       OWNER openzro;
CREATE DATABASE openzro_activity   OWNER openzro;
CREATE DATABASE dex                OWNER openzro;

SSL — Cloud SQL ENCRYPTED_ONLY

Cloud SQL with "Allow only SSL connections" (the ENCRYPTED_ONLY mode) rejects plaintext with pg_hba.conf rejects ... no encryption. Match it with:

postgres:
  sslMode: require

require cifrates the connection without validating the server certificate — sufficient when the DB is in a peered VPC or otherwise trusted at the network layer.

For full validation:

sslModeEncryptsValidates server certValidates server hostname
disable
require
verify-ca✅ (needs CA file)
verify-full

verify-ca/verify-full need the server CA mounted in the pod via a Secret + PGSSLROOTCERT env var. See the management deploy guide for the full mount pattern.

MySQL

Symmetric structure — separate mysql.enabled: true block. Same four databases, same single-user model:

mysql:
  enabled: true
  host: 10.x.x.x
  port: 3306
  tls: preferred           # true | false | preferred | skip-verify
  username: openzro
  password: "REPLACE_WITH_PASSWORD"
  databases:
    management: openzro
    flow: openzro_flow
    activity: openzro_activity
    dex: dex

postgres.enabled: true and mysql.enabled: true are mutually exclusive — the chart fails fast at template time.

SQLite (lab only)

With both postgres.enabled: false and mysql.enabled: false, each component falls back to its own SQLite file. Multi-replica diverges silently — different pods write to different files and authentication state desyncs. Use SQLite only for single-replica labs.

Dex storage

Dex storage routes through the same postgres: block when postgres is enabled. The chart renders dex.config.storage to point at the dex database with the same credentials. No duplication needed.

For lab installs (sqlite), the upstream Dex chart provisions a 1Gi PVC at /var/lib/dex — flip dex.persistence.enabled: true and uncomment the dex-data volume in dex.volumes / dex.volumeMounts.

Backup

Database backup is the operator's responsibility — outside the chart's scope. Recommended:

  • Cloud SQL / RDS — managed automated backups + PITR
  • Self-hosted Postgrespg_dump cron + WAL archiving
  • Self-hosted MySQLmysqldump cron + binlog archiving

The dataStoreEncryptionKey (AES-256 encrypting sensitive fields in the openzro DB) is not rotatable in place — losing it invalidates the encrypted columns. Back it up alongside the DB.

GeoLite2 database (geo-location posture checks)

Independent of the SQL backend, the management binary fetches a GeoLite2 database for posture checks. The chart pulls from the openZro mirror by default (pkg.openzro.io/geolocation-dbs), no operator action needed. To pin a MaxMind license key for direct upstream pulls:

management:
  config:
    geoLite:
      licenseKey:
        existingSecret: openzro-maxmind
        existingSecretKey: licenseKey

Or use value: directly. With license key empty, the openZro mirror serves the same content with some hours of lag.