A WordPress login loop that only happened sometimes
wp-admin bounced back to the login page with reauth=1, but not always. The cause was random auth salts generated per pod behind a non-sticky load balancer.
At Optimacros the marketing sites are a fleet of nine WordPress installations on Kubernetes, each running two to five replicas under an HPA. The complaint sounded like a password problem: “The password is right, I logged in once, and then it starts looping.”
After basic auth, opening /wp-admin/ sent the user back to wp-login.php?...&reauth=1. Sometimes a login stuck for a moment; the next request bounced again. Every site was affected.
The word that gave it away: sometimes
The first suspect in any WordPress-behind-a-proxy loop is TLS: WordPress thinks the request is plain HTTP and keeps redirecting. But the sites already mapped X-Forwarded-Proto to $_SERVER['HTTPS'] and set FORCE_SSL_ADMIN. More importantly, a TLS misconfiguration loops every time. An intermittent loop points at something that differs between replicas.
The cause: every pod had its own keys
WordPress signs its authentication cookie with eight salt constants from wp-config.php: AUTH_KEY, SECURE_AUTH_KEY and friends. In this deployment they were not set anywhere, neither in the environment nor in extra config. The official image’s entrypoint handles that by generating random salts in each pod.
The Service had sessionAffinity: None, so the load balancer spread requests across replicas. A cookie signed by pod A was rejected by pod B, and WordPress asked for a fresh login. “I logged in once” simply meant two requests in a row had landed on the same pod.
A check that lies
My first attempt to prove it gave a false negative. I hashed the define() lines in wp-config.php across pods. But those lines hold the random per-pod fallback, not the value WordPress actually uses. The reliable check is the effective value: printenv WORDPRESS_AUTH_KEY in each pod and compare the hashes. They differed, which is exactly the point.
The fix, through Git
Argo CD ran with self-heal, so a kubectl fix would have been reverted within minutes. Everything went through the repository, using the pattern the cluster already had for secrets:
- Generate eight salts once per site and store them in Vault.
- Add a
VaultStaticSecrettemplate to the shared landing-site chart, so the Vault Secrets Operator creates a Kubernetes Secret per site. - Feed the eight
WORDPRESS_*_KEYandWORDPRESS_*_SALTvariables from that Secret. The image reads them withgetenv_docker(), so every pod uses the same values.
Argo CD rolled it to all nine sites with maxUnavailable: 0, without downtime. Changing the salts logged every admin out once, which was expected and announced.
Takeaways
- “It works sometimes” is the signature of per-replica state. A configuration bug fails every time.
- Any stateless app scaled past one replica behind a non-sticky balancer needs shared signing and session keys. Sticky sessions only hide the problem.
- Verify the effective value inside the process, not the line in a config file that might be a fallback.
- With Argo CD self-heal on, the fix belongs in Git, not in
kubectl.