Rotating WordPress salts as incident response: the step everyone skips

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.phpAUTH_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 by LOGGED_IN_KEY + LOGGED_IN_SALT.
  • Secure auth cookies (wordpress_sec_*). Used for SSL admin actions. Signed by SECURE_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-root

That 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:

  1. SQL UPDATE wp_users SET user_pass = MD5('<new>') for each admin (WordPress accepts MD5 transitionally and rehashes to phpass on first login).
  2. 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_usermeta and 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.php was 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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.