Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt

Use this file to discover all available pages before exploring further.

WordPress stores a hash of the admin password in wp_users, so updating a Secret alone doesn’t change the live login. The fp-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

secret manager (ASM / GCP SM / Vault)
   │  rotation policy fires (or operator runs a manual update)

External Secrets Operator
   │  pulls new value into K8s Secret

<release>-fp-site-install Secret  (admin_password key updated)
   │  Stakater Reloader watches the Deployment's referenced Secrets

Deployment rolling restart  (default strategy: maxSurge:1, maxUnavailable:0)
   │  Pod template recreates → initContainer runs first

sync-admin-credentials initContainer
   │  reads new admin_password from env, compares against wp_users
   │  → idempotency check (wp_check_password) detects drift
   │  → wp user update writes new hash

Site container starts. wp_users now matches Secret.
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:
# values.yaml
commonAnnotations:
  # Use commonAnnotations, NOT podAnnotations. Reloader watches the
  # Deployment object's metadata.annotations, not the pod-template's
  # spec.template.metadata.annotations.
  secret.reloader.stakater.com/reload: "<release>-fp-site-install"
Or, for sites that want any referenced Secret/ConfigMap change to roll the Deployment:
commonAnnotations:
  reloader.stakater.com/auto: "true"
For the auto-generated install Secret the name is <release>-fp-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:
# Resolve current admin user from the Secret
ADMIN_EMAIL=$(kubectl -n <ns> get secret <release>-fp-site-install \
  -o jsonpath='{.data.admin_email}' | base64 -d)

# Confirm wp_users has the new email (and was touched recently)
kubectl -n <ns> exec deploy/<release>-fp-site -c site -- \
  wp --allow-root --path=/app/web/wp user get "$ADMIN_EMAIL" \
  --fields=ID,user_login,user_email,user_registered
For password verification, evaluate wp_check_password against the expected value in-pod:
EXPECTED=$(kubectl -n <ns> get secret <release>-fp-site-install \
  -o jsonpath='{.data.admin_password}' | base64 -d)

kubectl -n <ns> exec deploy/<release>-fp-site -c site -- \
  env EXPECTED="$EXPECTED" wp --allow-root --path=/app/web/wp eval '
    $u = get_user_by("email", getenv("WP_ADMIN_EMAIL")) ?: get_user_by("login", "admin");
    echo wp_check_password(getenv("EXPECTED"), $u->user_pass, $u->ID) ? "ok\n" : "mismatch\n";
  '
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>-fp-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 fp-charts.

Failure modes

StateBehaviour
Reloader not installedSecret update doesn’t roll the Pod. The initContainer never re-runs, so wp_users drifts from the Secret until something else triggers a roll (kubectl rollout restart, helm upgrade, autoscaler scale-up, etc.). Install Reloader.
Reloader watching the wrong keyAnnotation set on podAnnotations instead of commonAnnotations. Reloader watches the Deployment object’s metadata.annotations, not the pod-template’s. Move the annotation.
Pod stuck Init:Error after rotationThe initContainer ran but failed (typically a wp-cli error visible in kubectl logs <pod> -c sync-admin-credentials). The Deployment never becomes Ready, the rollout times out, old replicas keep serving with the old password. Investigate the log; the most likely cause is the admin user not being resolvable by either email or username (e.g. someone manually renamed it via wp-admin).
smtp.enabled=false (default)Each rotation suppresses WP’s password-change notification via a WP_CLI::add_hook( 'after_wp_load', ... ) filter file. The site container’s view of wp_mail() is unchanged.
smtp.enabled=trueWP fires its built-in “Password Changed” / “Email Changed” notification when wp user update writes. Expected behaviour — site authors and ops both like seeing rotation in their inbox. To suppress globally, install a wp_mail filter mu-plugin (out-of-scope for this chart; see What’s not in the platform).
Drifted manual edit via wp-adminIf an operator changed the admin password in wp-admin without updating the Secret, the next Pod roll detects drift and overwrites their manual change with the Secret’s value. The Secret is the source of truth — set syncAdminCredentials: false if you want the in-DB value to win.

Opting out

Set syncAdminCredentials: false to skip the initContainer entirely:
syncAdminCredentials: false
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 fp-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).