Skip to main content
WordPress stores a hash of the admin password in wp_users, so updating a Secret alone doesn’t change the live login. The site chart ships with a credential-sync initContainer on every site Pod that reads the install Secret and reconciles wp_users on Pod start. Pair it with Stakater Reloader and rotation is self-driving end-to-end.

End-to-end flow

No helm upgrade, no argocd app sync, no kubectl exec. Available in chart v0.4.0+.

How the initContainer works

Every site Pod runs a sync-admin-credentials initContainer before the main site container. The initContainer:
1

Wait for the database

Direct mysqli probe (doesn’t load WordPress) up to 60×2s. The same probe the install Job uses — works against fresh DBs that haven’t seen wp core install yet.
2

Skip on fresh installs

If wp core is-installed returns false, exit 0. The post-install Helm hook Job is responsible for first-time bootstrap; the initContainer would have nothing to reconcile.
3

Idempotency check

Resolve the target user (prefer admin_email, fall back to admin_user). Compare the desired credentials against the current DB state via wp eval:
  • wp_check_password(getenv("ADMIN_PASSWORD"), $u->user_pass, $u->ID) — hash compare
  • $u->user_email === getenv("ADMIN_EMAIL") — string compare
  • $u->user_login === getenv("ADMIN_USER") — string compare
All three match → exit 0 with [sync-creds] DB already in sync with Secret; skipping update.
4

Reconcile on drift

Any mismatch → run wp user update --user_pass --user_email. Pod proceeds; site container starts.

Multi-replica idempotency

With replicaCount > 1, every Pod runs the initContainer on start. The default rolling-update strategy (maxSurge: 1, maxUnavailable: 0) brings new Pods up sequentially, so:
  • The first Pod to roll detects drift, runs wp user update. WP fires its “Password Changed” notification (one email).
  • Every subsequent Pod sees the DB already-in-sync and short-circuits before calling wp user update. No DB write. No notification.
Net: one DB write per rotation, at most one WP-emitted “Password Changed” email regardless of replica count. This is why the idempotency check matters — without it, every replica would race to wp user update on every roll, generating noise and duplicate emails.

Reloader setup

The initContainer only runs when the Pod restarts. To turn an updated Secret into a Pod restart automatically, install Stakater Reloader cluster-wide and annotate the Deployment. Full setup is in Operations → Email → Auto-rolling on Secret change — Reloader is platform-wide infrastructure, not feature-specific. Once installed, the same controller solves rotation for the SMTP token, DB password, WP keys+salts, and the admin Secret. For admin-credential rotation specifically, watch the install Secret via the chart’s typed reloader block (chart v0.13.0+):
The chart emits a secret.reloader.stakater.com/reload: "<csv>" annotation on the Deployment (the only place Reloader actually looks — see Failure modes below). For sites that want any referenced Secret/ConfigMap change to roll the Deployment, fall back to the pre-v0.13 commonAnnotations form:
For the auto-generated install Secret the name is <release>-site-install. With siteInstall.existingSecret set, use whatever name you supplied.

Verifying a rotation took effect

After the Pod has rolled, check the live DB user from any running Pod:
For password verification, evaluate wp_check_password against the expected value in-pod:
ok confirms the live DB hash matches the Secret value. mismatch means rotation hasn’t propagated yet — check the Pod has actually rolled (kubectl rollout status deployment/<release>-site) and that Reloader sees the Secret (kubectl logs -n reloader deploy/reloader-reloader). For a full e2e (rotate + assert hash matches across all replicas), see tests/credential-sync-test.sh in charts.

Failure modes

Opting out

Set syncAdminCredentials: false to skip the initContainer entirely:
helm template confirms no initContainer block renders. The admin user becomes whatever wp core install set on first install — never touched again by the chart. When to opt out:
  • External IdP owns the WP admin user. SSO plugin (e.g. SAML, LDAP, OIDC) provisions and updates wp_users from the identity provider. The chart’s reconciler would fight with the SSO plugin’s writes.
  • You don’t trust the Secret as source of truth. Some teams prefer wp-admin as the canonical credential store. Opting out means manual edits via wp-admin are not overwritten by Pod rolls.
  • Static demo / single-shot install. No rotation expected, saves the per-Pod-start initContainer cost (~1-3s).

What’s not in the platform

  • Auto-suppression of WP “Password Changed” emails when SMTP is on. When smtp.enabled=true and wp user update writes, WP fires its built-in notification email. That’s correct WP behaviour and most operators want to see rotation in their inbox. Sites that want this suppressed globally need a one-line wp_mail filter mu-plugin (add_filter('send_password_change_email', '__return_false')); not shipped in mu-plugin because of conflicting opinions.
  • Sync of arbitrary user fields. The initContainer reconciles user_pass, user_email, and user_login. Display name, roles, and metadata are not synced — those live in WP and stay there.
  • Multi-user reconciliation. One admin user per site, resolved by email-then-username. Multi-admin orgs run their own provisioning (SSO plugin or wp user create in build-time scripts).