Security & Privacy

WordPress Salts and Passwords: The Eight Lines Nobody Rotates

The eight define() lines in wp-config.php sign every admin cookie and every nonce your site issues. What each one does, why Math.random cannot be trusted to make them, what actually breaks when you rotate them, and an offline generator for the replacements.

WordPress Salts and Passwords: The Eight Lines Nobody Rotates

Near the top of every wp-config.php there are eight define() lines that somebody pasted in once and never looked at again. They are not passwords. Nothing checks them when you log in, no admin screen will ever show them to you, and no update touches them. They are the HMAC keys that sign every authentication cookie and every nonce your site hands out.

That gets concrete the day a copy of the file escapes. A backup archive left in the web root, a hosting login a former contractor still has, a repository that was public for one afternoon: any of them hands over eight strings that are still, right now, the only thing separating a forged admin cookie from a real one. Changing your password does not touch them. Reinstalling WordPress does not touch them. Somebody has to open the file and replace them.

The quieter failure is having no constants at all. WordPress does not complain when they are missing. It calls wp_generate_password( 64, true, true ), writes the result into the options table and carries on. The most sensitive secret on the site then lives in the database, inside every dump that ever gets emailed to a developer.

The WordPress salt generator below writes the eight lines, plus the two other secrets that tend to come up in the same afternoon: a password with an honest entropy figure attached, and an .htpasswd line for a staging site.

WordPress salts and passwords

Three secrets a WordPress install needs, all made here in this browser tab with crypto.getRandomValues. Nothing is sent anywhere, there is no request to a server and no logging, because a secret that travels to a server is not a secret any more.

1The eight constants for wp-config.php

Replace the whole block between AUTH_KEY and NONCE_SALT. Changing them logs every user out, which is exactly what you want after a break-in.

define( 'AUTH_KEY','' );
define( 'SECURE_AUTH_KEY','' );
define( 'LOGGED_IN_KEY','' );
define( 'NONCE_KEY','' );
define( 'AUTH_SALT','' );
define( 'SECURE_AUTH_SALT','' );
define( 'LOGGED_IN_SALT','' );
define( 'NONCE_SALT','' );

Each value is 64 characters from printable ASCII, 92 of them: the single quote and the backslash are left out so the line stays valid inside single quotes in PHP. That is 417.5 bits per constant.

2A password

For an administrator account, a database user, an FTP login. Every character is an independent draw, with no rule forcing one of each kind, because such a rule would lower the entropy and make the number below a lie.

Entropy

0bits

The symbol set is !@#$%^&*()-_=+[]{};:,.?/~, which leaves out the quotes, the backslash, the backtick and the space, so the password survives being pasted into a shell command or a configuration file. Excluding look-alikes drops the zero and both letter O's, the one together with a lower case L and a capital i, the five together with a capital S, and the eight together with a capital B.

3An .htpasswd line

For locking wp-login.php or wp-admin behind the web server, before WordPress is even reached. The hash is APR1-MD5, worked out here, with the salt taken from crypto.getRandomValues.

Where the server runs Apache 2.4 or newer, bcrypt is the better choice and htpasswd -B -C 12 .htpasswd name writes it. APR1 is 1000 rounds of MD5, which a modern graphics card chews through by the million every second, and it is offered here only because every Apache understands it, including the old ones. This tool does not produce bcrypt: that needs a Blowfish implementation, and a subtly wrong one would be worse than none at all.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

All of it runs in the browser tab. There is no network call anywhere in the tool, so the values you copy have never crossed a wire, sat in a server log or turned up in anybody’s request history. For a value whose entire job is to be unknown to everyone else, that is not a nicety.

What the eight constants actually sign

WordPress has four authentication schemes, not eight: auth, secure_auth, logged_in and nonce. Each gets a key and a salt. wp_salt( $scheme ) in wp-includes/pluggable.php looks up both, concatenates them key first, and returns one string. wp_hash() uses that string as the HMAC key, and nothing else in core ever sees it.

// wp-includes/pluggable.php, the last line of wp_salt()
$cached_salts[ $scheme ] = $values['key'] . $values['salt'];

// and the whole of what wp_hash() does with it
function wp_hash( $data, $scheme = 'auth', $algo = 'md5' ) {
    return hash_hmac( $algo, $data, wp_salt( $scheme ) );
}

The three cookie schemes map onto three cookie names, all built in wp-includes/default-constants.php out of COOKIEHASH, which is nothing more than an MD5 of the site URL. auth signs wordpress_[COOKIEHASH], the admin cookie for plain HTTP. secure_auth signs wordpress_sec_[COOKIEHASH], the same cookie once the admin area is on HTTPS, which on any site built this decade makes it the pair that matters. logged_in signs wordpress_logged_in_[COOKIEHASH], which travels with every front end request and tells your theme who is reading.

The cookie value is four pipe separated fields: login, expiry, session token, and a SHA-256 HMAC over the first three. The key for that HMAC is not the salt directly. It is wp_hash( login|pass_frag|expiration|token, $scheme ), where pass_frag is four characters lifted out of the user’s stored password hash. A live cookie is therefore bound to three things: the salt in the config file, the user’s current password hash, and one session token row in user meta. Break any one and the cookie stops verifying.

The fourth scheme signs nonces, and a nonce is smaller than people expect. wp_create_nonce() returns ten characters cut out of an HMAC: substr( wp_hash( $tick . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ). The tick is ceil( time() / ( DAY_IN_SECONDS / 2 ) ), and that halving is why core documents the lifetime as “between 12 and 24 hours” rather than a fixed number. Every _wpnonce field in the dashboard and every X-WP-Nonce header the block editor sends is signed with NONCE_KEY and NONCE_SALT.

Table of the four WordPress authentication schemes: auth signs the wordpress_ cookie, secure_auth signs wordpress_sec_, logged_in signs wordpress_logged_in_, and nonce signs _wpnonce and X-WP-Nonce, each from its own KEY and SALT pair, with the two lines of core code that concatenate the pair into one HMAC key.

Keys and salts are not two different things

The naming implies a cryptographic distinction. There is not one, at least not any more. WordPress 2.5 shipped a single SECRET_KEY; 2.6 added the four pairs still in the sample file today, the idea being that the key lived in the file and the salt lived in the database. Modern core glues them together into one long HMAC key. Both halves come out of the same generator, both are 64 characters, and neither deserves more care than the other.

What core does care about is whether a value is real. Before reading anything, wp_salt() builds a list of duplicated values, marks put your unique phrase here as duplicated (in English and in the translated form used by localised sample files), then treats any constant whose value appears more than once as though it were never defined. Pasting the same 64 characters into all eight lines does not give you eight secrets. It gives you none: core falls back to the database for every one of them, generating and storing one there instead.

The moment you change them

Everybody is logged out, including you. That is not a side effect to work around, it is the entire feature.

The mechanism explains what survives. An existing wordpress_logged_in_ cookie carries a session token, but the token is only reached once the HMAC verifies. Change the salt and it does not verify, so the cookie is thrown away before the token is ever looked up and the visitor is anonymous on the next request. The session rows in user meta are simply orphaned.

Passwords are untouched. wp_hash_password() runs the password through PHP’s password_hash() with PASSWORD_BCRYPT (bcrypt since WordPress 6.8, phpass before that), and no salt constant appears in it. Application passwords are untouched too: since 6.8 they go through wp_fast_hash(), a BLAKE2b hash from libsodium keyed with a fixed literal string. Posts, media, options, plugin settings and orders do not know the salts exist.

What genuinely stings is nonces in flight. Anyone with an editor open at the moment you save the file gets a failed autosave (rest_cookie_invalid_nonce from the REST API) or the classic “Are you sure you want to do this?” screen from an older admin form. Nothing is destroyed: their text is still in the browser. But if five people are working, do it at a quiet hour and tell them first.

Where the values come from

The official source is the secret key service at api.wordpress.org/secret-key/1.1/salt/, which is exactly what the comment block in wp-config-sample.php sends you to. It is fine. Served over TLS, real random source, twenty years of history. WP-CLI’s wp config shuffle-salts fetches from the same endpoint, which is why that command carries an --insecure flag for retrying the download when a TLS handshake fails.

Fine is not the same as best available. A remote generator means the bytes that will sign your admin cookies were produced on a machine you do not control, crossed a network, and then passed through whatever sits between the response and the file: terminal scrollback, a clipboard manager, browser history. None of that is likely to be what gets a site compromised. All of it is avoidable for nothing.

Then there is the edit itself. If you are unsure what else belongs in that file, wp-config.php explained covers the constants that earn their place. Two notes specific to this edit: use an editor that will not helpfully convert straight quotes into curly ones, and check that no stray apostrophe has landed inside a value, because an unescaped quote inside a single quoted PHP string is a parse error, and a parse error in wp-config.php is a white screen on every page of the site including the login form.

Why Math.random is disqualified

Every WordPress salt generator has to answer one question: where do the bytes come from. Math.random() is not a cryptographic generator and has never claimed to be. V8 implements it as xorshift128+ with 128 bits of state, seeded once per context and never reseeded, and the language specification explicitly declines to promise anything about predictability. Its internal state can be recovered from a modest run of observed outputs. Pointing it at a value that signs an admin cookie is an unforced error, and plenty of generators still do.

The right primitive is crypto.getRandomValues(), the platform’s own CSPRNG. Then comes the subtler half. Take a random byte modulo 92 and you have quietly broken uniformity: 256 divided by 92 leaves a remainder of 72, so the first 72 characters of the alphabet arrive on three byte values each while the remaining 20 arrive on two. Rejection sampling is the fix. Work out 256 minus (256 modulo 92), which is 184, discard any byte from 184 upwards and use the rest. The tool does that on every draw, so alphabets that do not divide 256 stay uniform, and Math.random appears nowhere in it.

The salt alphabet is printable ASCII from 33 to 126 with the single quote and the backslash removed, because either would need escaping inside a single quoted PHP string. That leaves 92 characters, 6.52 bits each, 417.5 bits for a 64 character constant. Nobody needs to argue about whether that is enough. The eight lines come out padded into the same column wp-config-sample.php uses, so the block drops straight over the old one.

The password section runs the same machinery over a smaller alphabet and shows its working. The entropy figure is length multiplied by the base 2 logarithm of the alphabet the tool is drawing from at that moment, which is why it moves when you flip a class off. There is deliberately no rule forcing one character from each selected class: such a rule shrinks the space being drawn from, which would make the number on screen an overstatement, and the page says so.

Table showing the cost of the No look-alikes option: a to z falls from 26 characters to 24, A to Z from 26 to 22, digits from 10 to 6, and all three together from 62 to 52, which is 5.95 bits per character down to 5.70, about 5.1 bits over a twenty character password.

The No look-alikes switch drops 0 O o 1 l I 5 S 8 B from whichever classes are on. It is a readability decision with a measurable price: with letters and digits enabled the alphabet falls from 62 characters to 52, a quarter of a bit per character. Over twenty characters that is about five bits, a factor of thirty four in guessing effort, and one extra character buys all of it back. Turning all four classes off is handled rather than crashed: an error line appears, the password and entropy readout clear, and the two buttons disable.

The time figure underneath is stated against a specific attack, a trillion offline guesses a second against a stolen hash, not against a login form with rate limiting in front of it. The arithmetic stays in log10 from end to end, because a guess count with a couple of hundred digits stops being an exact number in a JavaScript double long before it stops being a number worth printing.

The .htpasswd line for a staging site

The third section writes one line of an .htpasswd file: a user name, a colon, and an APR1-MD5 hash. The format is $apr1$, an eight character salt, then a 22 character digest, and the hash is Apache’s own key stretching loop: a thousand MD5 rounds remixing the password, the salt and the previous digest. The tool implements MD5 and that loop itself, and the output was checked against openssl passwd -apr1 on twelve vectors: the empty password, one and two characters, 15, 16 and 17 characters (either side of MD5’s 16 byte block boundary), a 34 and a 64 character password, symbol heavy and UTF-8 passwords, and salts containing a dot and a slash. All twelve matched, and htpasswd -vb accepts a line the shipped code produced.

Two behaviours there are deliberate. The salt survives edits to the user name and the password, so the line updates live as you type and only New salt re-rolls it, which incidentally makes the hash testable: same inputs, same line, twice. And a user name containing a colon, whitespace or a control character is refused with the reason given. The colon is the field separator in that file, so a name containing one writes a record Apache cannot parse.

What the tool will not do is bcrypt. Correct bcrypt needs a Blowfish implementation, and a subtly wrong one would be worse than none, so where Apache 2.4 or newer is available the page points you at htpasswd -B -C 12 instead. Be clear about what APR1 is: a thousand rounds of MD5 is not a strong password hash by any modern standard. For a gate on a staging copy it is adequate. For anything holding real accounts it is not.

HTTP authentication is a fence, not a lock

Basic auth stops search engines, casual visitors and anybody following a stale link into your staging copy. It does not stop somebody who has the credentials, and those credentials go out as base64 of user:password in a header on every request, which is not encryption and reverses in one command. Over plain HTTP that is a password in clear text on every page load. Put the staging site behind HTTPS or do not bother with the fence.

It also breaks things you will have forgotten about. Anything reaching the site without a browser now gets a 401: payment gateway webhooks, deploy hooks, uptime monitors, and WordPress’s own loopback request to wp-cron.php, an ordinary HTTP request the site makes to itself, which the fence rejects like any other. A staging site that stops publishing scheduled posts is usually this, and why WP-Cron is not a cron job covers the rest of that failure. The repair is a real system cron with DISABLE_WP_CRON set, or an allow rule for that one file.

The first move after a break in

If a site has genuinely been broken into, rotating the salts is one of the earliest useful actions available, because it evicts every session at once. An intruder holding a stolen cookie but not a password loses access the second you save the file. It costs nothing, takes ten seconds, and does not depend on knowing yet how they got in.

It is also not a fix by itself. If they still have write access to the filesystem they can read the new constants as easily as the old ones, and an early rotation mostly tells them you noticed. The order that works: take the site off the public network or into maintenance, find and remove the persistence (a modified theme functions.php is one of the classic hiding places, and functions.php explained is a tour of what should and should not be in that file), then rotate the salts, then reset passwords and revoke application passwords, then let people back in. Rotate first and clean second and you will only be doing it twice.

Those eight lines are the only secret on a WordPress site with no interface, no expiry date and no reminder attached. Nothing in the dashboard will ever mention that they are eleven years old, or shared with two other installs because the site was cloned twice, or living quietly in wp_options where the next database export picks them up.

So give them some attention on a schedule you will actually keep. Rotate when a site is cloned for staging, because the clone inherits the original’s secrets and that is the same as sharing a password. Rotate when someone with server access leaves. Rotate the moment anything looks like an intrusion. It is a two minute edit with exactly one predictable consequence, everybody signs in again, and that consequence is the thing you were trying to cause.

And generate the replacements somewhere they cannot leak on the way to the file: local, no request, clipboard, paste, save. The whole point of a secret is the list of people who have seen it, and that list should be one name long.

WordPress Salts and Passwords: The Eight Lines Nobody Rotates

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

WordPress Development

Dynamic Templates: Build the Design Once, Let Your Posts Fill It In

You build the layout once, tell a few layers where to get their content, and from then on every post, product or page renders its own version.

SEO & Structured Data

Redirect Rule Generator: The Rules That Never Fire

A redirect list rarely breaks. It accumulates: a broad prefix rule that kills every rule below it, a target missing a trailing slash that doubles your hop count, a 301 where a 308 was needed. Generate the Apache, nginx and CSV versions of your rules, then walk a URL through them and watch where it actually goes.

Photo Editing

Remove an unwanted object from a photo without uploading it anywhere

A clone tool copies pixels from elsewhere. An inpainting model predicts what should have been behind the thing you removed, which is a different question with a much better answer on grass, hedges and brickwork. It runs in your browser, and the rest of the photograph comes back byte for byte unchanged.

Photo Editing

Repair a photo that messengers and re-saves have turned to blocks

JPEG damage is not random. It is made in blocks of eight by eight pixels, in a known order, which is why a model trained on compressed images beats any amount of sharpening. It runs in your browser, repairs at four times the size and hands the result back clean.

Developer Tools

Encoding Forensics: How to Fix Mojibake Like ü

ü is not corruption. It is one umlaut, two UTF-8 bytes, read by a program that believed they were Windows-1252, then saved again so the wrong reading became the content. Here is how to tell which wrong turn your text took, why copied replacement lists only half work, and what to check in the database before you clean anything.

Photo Editing

Take the scratches and dust out of a scanned family photograph

A scratch is thin and disagrees with its surroundings in almost every direction at once. A real edge disagrees in one or two. That single difference finds the damage with plain arithmetic, and an inpainting model fills what you agree to. All of it in your browser.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.