Skip to content

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:

  1. waits until the database answers (bounded by the Job's deadline);
  2. logs in as the administrator, from the Secret the database itself uses;
  3. creates the tenax user 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.