You’ve cleaned the malware files, deleted the backdoor admin accounts, rotated everyone’s password. The site is fine, you’re fine. Three weeks later someone logs in with a session cookie they grabbed during the compromise window and creates a fresh admin account. The cookie was still valid because — even though the password it was issued against has been changed — WordPress doesn’t actually invalidate sessions on password change. The cookie was signed by your AUTH_KEY and AUTH_SALT, and those haven’t changed.
Rotating WordPress salts is the step almost every incident-response checklist either skips or misunderstands. It’s a one-liner; the value is enormous. Here’s exactly what it invalidates and how to do it properly across multiple sites.
What the salts actually do
The eight constants in your wp-config.php — AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY, plus their four *_SALT counterparts — are inputs to WordPress’s HMAC functions. They sign three things:
- Auth cookies (
wordpress_logged_in_*). Issued at login, valid for two weeks by default. Signed byLOGGED_IN_KEY+LOGGED_IN_SALT. - Secure auth cookies (
wordpress_sec_*). Used for SSL admin actions. Signed bySECURE_AUTH_KEY+SECURE_AUTH_SALT. - Nonces. Form/action tokens, valid for 24 hours. Signed by
NONCE_KEY+NONCE_SALT.
Change any of these constants and every cookie or nonce signed against the old value stops verifying immediately. Every logged-in user is forced to log in again. Every nonce-protected form submission has to be re-fetched. Every “remember me” cookie an attacker exfiltrated during the compromise window becomes a useless string.
Rotating passwords alone doesn’t do this. WordPress’s password check is independent of cookie verification — a stolen cookie keeps working until either it expires (two weeks) or the salts change. Salts are the only mechanism that retroactively invalidates issued credentials.
How to rotate
The supported way is wp-cli:
wp config shuffle-salts --skip-plugins --skip-themes --allow-rootThat regenerates all eight values in-place. Done. Every existing session is dead.
If wp-cli is hanging on bootstrap (some old plugins do that — particularly anything calling create_function() on PHP 8), do it directly with a small Python script. It edits wp-config.php with regex, doesn’t load WordPress at all:
import re, secrets, string
KEYS = ['AUTH_KEY','SECURE_AUTH_KEY','LOGGED_IN_KEY','NONCE_KEY',
'AUTH_SALT','SECURE_AUTH_SALT','LOGGED_IN_SALT','NONCE_SALT']
CHARS = string.ascii_letters + string.digits + r" ~`!@#$%^&*()-_=+[]{}|;:'\",.<>/?"
def gen_salt(n=64):
return "".join(secrets.choice(CHARS) for _ in range(n))
with open("/path/to/wp-config.php") as f: src = f.read()
for k in KEYS:
new = gen_salt().replace("\\", "\\\\").replace("'", "\\'")
src = re.sub(
rf"define\s*\(\s*['\"]" + re.escape(k) + r"['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)\s*;",
f"define( '{k}', '{new}' );", src, count=1, flags=re.M)
with open("/path/to/wp-config.php", "w") as f: f.write(src)The escape dance — backslash + single quote — matters because some random characters in the salt will land on those literals. Skip it and you’ll write a syntactically broken wp-config.php, which means a 500 across your whole site until you notice.
Pair it with the password rotation, not after
The window between “I rotated passwords” and “I rotated salts” is exactly when a stolen cookie can be used to set a new password and lock you out. Do them together:
- SQL
UPDATE wp_users SET user_pass = MD5('<new>')for each admin (WordPress accepts MD5 transitionally and rehashes to phpass on first login). - Immediately after, in the same script, regenerate the salts in
wp-config.php.
Now any cookie an attacker has — issued under either the old password or the new one — fails verification, because the keys it was signed with are gone. They’d need fresh credentials and a fresh login, and the password they have doesn’t work anymore.
What it doesn’t invalidate
- Application Passwords (the per-app tokens introduced in WP 5.6). Those are stored hashed in
wp_usermetaand verified against the user’s password hash, not the salts. Rotate them in WP Admin → Users → Profile → Application Passwords if you suspect any have been exfiltrated. - OAuth tokens from plugins like Jetpack or WooCommerce REST. Those are usually long-lived and stored independently. Check each plugin’s settings.
- API keys stored in
wp_options. Rotate from each integration’s dashboard. - The MySQL credentials. Salts don’t touch the database — if your
wp-config.phpwas read during the compromise, the DB credentials in it are also potentially exfiltrated. Rotate those separately.
Side effect: every active user gets logged out
This is intentional and fine, but warn your team before doing it on a busy site. Anyone editing a draft post mid-shuffle will get a “session expired” prompt next time they save and may lose unsaved changes. Schedule the rotation for a low-traffic window or notify editors first.
The whole rotation takes one second per site to execute. As incident-response steps go, it’s the highest-leverage one — and the most commonly skipped.
