Keep the database user in sync¶
Database users creates the tenax user once. That is enough for most setups. This page is for
GitOps setups where the user must keep matching its Secret without anyone running anything:
- the Secret's password is rotated;
- the database is rebuilt or restored from an older dump, and the user is gone;
- someone changes the user by hand (drops a grant, changes the password);
- the database is created after the manifests that describe its user (a fresh cluster).
A one-time Job handles none of these: it runs once, and Kubernetes won't rerun a finished Job when the Secret changes. A CronJob that re-applies the same idempotent SQL does: within one period, the user matches the Secret again.
How it works¶
One CronJob per database, in the database's namespace. Each run:
- waits until the database answers (bounded by the Job's deadline);
- logs in as the administrator, from the Secret the database itself uses;
- creates the
tenaxuser if it is missing, and sets its password and grants from the Tenax Secret.
Every statement is idempotent, so a run that finds everything in place changes nothing. The passwords reach the container as environment variables from Secrets; they are never on a command line or in a log.
PostgreSQL¶
Replace the four values marked # ←: the namespace, the database's Service, the database, and the Secret and key
holding the administrator password.
apiVersion: batch/v1
kind: CronJob
metadata:
name: tenax-user-postgres
namespace: team-a # ←
spec:
schedule: "*/30 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: OnFailure
automountServiceAccountToken: false
securityContext: { runAsNonRoot: true, runAsUser: 999, runAsGroup: 999 }
containers:
- name: psql
image: docker.io/library/postgres:16
resources: { requests: { cpu: 10m, memory: 32Mi }, limits: { memory: 128Mi } }
securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: [ALL] } }
env:
- { name: PGHOST, value: postgres } # ← the database's Service
- { name: PGDATABASE, value: app } # ← any database on the server
- { name: PGUSER, value: postgres }
- { name: PGCONNECT_TIMEOUT, value: "10" }
- name: PGPASSWORD
valueFrom: { secretKeyRef: { name: postgres-admin, key: password } } # ←
- name: TENAX_PASSWORD
valueFrom: { secretKeyRef: { name: tenax-postgres, key: password } }
command: [sh, -ec]
args:
- |
until pg_isready -q; do echo "waiting for $PGHOST"; sleep 5; done
psql -v ON_ERROR_STOP=1 -X <<'SQL'
\getenv tenax_password TENAX_PASSWORD
SELECT 'CREATE ROLE tenax LOGIN REPLICATION' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tenax') \gexec
ALTER ROLE tenax WITH LOGIN REPLICATION NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD :'tenax_password';
GRANT pg_read_all_data, pg_monitor TO tenax;
ALTER ROLE tenax SET idle_in_transaction_session_timeout = '10min';
SQL
echo "tenax user in sync on $PGHOST"
\getenv needs psql 15 or later in the image; the server can be any supported version. Set
max_slot_wal_keep_size with the server's configuration (for example -c max_slot_wal_keep_size=2GB), not here:
it is a server setting, not part of the user.
MySQL¶
apiVersion: batch/v1
kind: CronJob
metadata:
name: tenax-user-mysql
namespace: team-a # ←
spec:
schedule: "*/30 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: OnFailure
automountServiceAccountToken: false
securityContext: { runAsNonRoot: true, runAsUser: 999, runAsGroup: 999 }
containers:
- name: mysql
image: docker.io/library/mysql:8.4
resources: { requests: { cpu: 10m, memory: 64Mi }, limits: { memory: 256Mi } }
securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: [ALL] } }
env:
- { name: MYSQL_HOST, value: mysql } # ← the database's Service
- name: ROOT_PASSWORD
valueFrom: { secretKeyRef: { name: mysql-admin, key: root-password } } # ←
- name: TENAX_PASSWORD
valueFrom: { secretKeyRef: { name: tenax-mysql, key: password } }
command: [bash, -ec]
args:
- |
umask 077
printf '[client]\nuser=root\npassword=%s\nhost=%s\nconnect-timeout=10\n' "$ROOT_PASSWORD" "$MYSQL_HOST" > /tmp/root.cnf
until mysqladmin --defaults-extra-file=/tmp/root.cnf ping --silent; do echo "waiting for $MYSQL_HOST"; sleep 5; done
mysql --defaults-extra-file=/tmp/root.cnf <<SQL
CREATE USER IF NOT EXISTS 'tenax'@'%' IDENTIFIED BY '${TENAX_PASSWORD}';
ALTER USER 'tenax'@'%' IDENTIFIED BY '${TENAX_PASSWORD}';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS, RELOAD, SHOW_ROUTINE,
REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'tenax'@'%';
SQL
rm -f /tmp/root.cnf
echo "tenax user in sync on $MYSQL_HOST"
The password goes into the SQL unquoted beyond the string literal, so generate it as hex (openssl rand -hex 24).
Run it now¶
kubectl -n team-a create job --from=cronjob/tenax-user-postgres tenax-user-now
kubectl -n team-a logs -f job/tenax-user-now
Every 30 minutes is plenty: Tenax itself retries a refused login and reports it on the target, so a rotation is
picked up at most one period later. The pg_hba.conf rule and the binlog settings stay server configuration
(Database users, step 3); this CronJob does not touch them.