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.
PostgreSQL (recommended)
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:
- Connects as
provisioning.usernameto thepostgresadmin DB - Iterates through the four database names from
postgres.databases - Skips any that already exist (
SELECT 1 FROM pg_database) CREATE DATABASEfor the missing ones with the runtime user asOWNER
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:
| sslMode | Encrypts | Validates server cert | Validates 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 Postgres —
pg_dumpcron + WAL archiving - Self-hosted MySQL —
mysqldumpcron + 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.