Skip to content

Secrets API: Generate WP_SECRETS_KEY during installation - #15124

Open
ericmann wants to merge 2 commits into
WordPress:trunkfrom
ericmann:secrets-key-at-install
Open

ericmann wants to merge 2 commits into
WordPress:trunkfrom
ericmann:secrets-key-at-install

Conversation

@ericmann

@ericmann ericmann commented Oct 28, 2026 •

Copy link
Copy Markdown

setup-config.php now writes a dedicated WP_SECRETS_KEY, so new installs don't depend on the salts. Rotating the salts made every stored secret unreadable on a site that derived its key from them (#66187 comment:23, confirmed in comment:24). #15258 removes that fallback. Without a key, the Secrets API is then unavailable.

This depends on #15258. It adds a placeholder to wp-config-sample.php, and only #15258 makes the Secrets API reject it. Without that patch, the placeholder would become a public key.

What changes

  • The sample has a placeholder. wp-config-sample.php defines WP_SECRETS_KEY with the placeholder after the keys and salts. It sits outside their /**#@+ group, because the group's docblock says they can be changed at any time, and this key must not be.
  • setup-config.php fills it in. It replaces the placeholder line in the same loop that fills in the salts. The value is a base64-encoded random 32-byte key, the form the Secrets API uses as raw key material.
  • No CSPRNG: the key comes from wp_generate_password(), as the salts do.
  • No placeholder line in the sample, as with a custom or older localized sample: the define is added after the salts.

Existing sites are not affected. The Site Health side is #66380.

Testing

setup-config.php has no unit tests. I tested it end to end on PHP 8.5 and MySQL 8:

  1. Run setup-config.php?step=2 and install.php?step=2 against a fresh database.
  2. Store a secret.
  3. Replace all eight keys and salts, as wp config shuffle-salts does.
  4. Read the secret again.
trunk this PR this PR + #15258 this PR, old sample
WP_SECRETS_KEY defines in wp-config.php none one one one
Value is canonical base64 of 32 bytes – yes yes yes
Site Health key source fallback key dedicated key dedicated key dedicated key
Read after shuffling the salts secret_key_unavailable the stored value the stored value the stored value

Also checked:

  • The generated file passes php -l.
  • phpcs reports nothing new on the changed files.
  • phpstan analyse with core's configuration passes.

To test by hand:

  1. Delete wp-config.php and run the installer.
  2. Check that WP_SECRETS_KEY follows the salts.
  3. Store a secret and run wp config shuffle-salts.
  4. Read the secret back.

WP-CLI's wp config create writes its own file, so it needs the same change separately.

Trac ticket: https://core.trac.wordpress.org/ticket/66399

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Drafting the patch, running the install tests above, and this description. I reviewed the change and the results.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Without `WP_SECRETS_KEY`, the Secrets API derives its key from `LOGGED_IN_KEY` and `LOGGED_IN_SALT`, so rotating the salts makes every stored secret unreadable. `setup-config.php` now writes a base64-encoded random 32-byte `WP_SECRETS_KEY` after the keys and salts, outside their group, whose advice to change them at any time does not apply to it.

The key is left out when no CSPRNG is available, or when `wp-config-sample.php` already defines it. `wp-config-sample.php` itself is unchanged: the Secrets API accepts any `WP_SECRETS_KEY` string, so a placeholder there would become a public key on hand-edited installs.

Follow-up to [64337].

See #66399, #66187, #66380.
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props ericmann, peterwilsoncc.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@peterwilsoncc peterwilsoncc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working for a manual install but a fresh install via WP-CLI. It looks like an associated PR will need to be made to https://github.com/wp-cli/config-command, https://github.com/wp-cli/core-command or both.

Added some notes inline.

* left out, and the Secrets API derives its key from those salts instead.
*/
try {
$secrets_key = base64_encode( random_bytes( 32 ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other values fall back to wp_generate_password() if CSPRING isn't available. It might be worth doing so here too.

On the ticket I've suggested coordinating with meta to have WP_SECRETS_KEY added to the secrets API to allow this to fall back to the API if needs be.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c93419. Without a CSPRNG it now uses wp_generate_password( 64, true, true ), like the salts. With the fallback removed in #15258, a weaker key is better than none. I've replied on Trac about the API: it should be a separate endpoint, because people paste the /1.1/salt/ output to rotate their salts.

break;
}

if ( 'NONCE_SALT' === $constant ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little flakey if additional secrets are added to the sample config file.

I also think it a good idea to add this to the sample config file but let me know if there are reasons you don't wish to do this.

I suggest adding this above the /**#@-*/ but separated from the group by some line breaks, so the output becomes:

/**#@+
 * Authentication unique keys and salts.
 *
 * Change these to different unique phrases! You can generate these using
 * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
 *
 * You can change these at any point in time to invalidate all existing cookies.
 * This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
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',       '...' );

/**
 * Encryption key for the Secrets API.
 *
 * Secrets stored by WordPress and plugins are encrypted with this key.
 * Unlike the keys and salts above, changing it makes every stored secret unreadable.
 *
 * @since 7.2.0
 */
define( 'WP_SECRETS_KEY', '...' );

/**#@-*/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The sample now defines WP_SECRETS_KEY with the placeholder, and the loop that fills in the salts replaces it, so constants added to the sample later don't matter. A sample without the line still gets it added after the salts.

I put it after /**#@-*/ rather than inside the group. phpDocumentor applies the /**#@+ docblock to everything up to /**#@-*/, and that docblock says these values can be changed at any time, which is exactly what this key must not be. I'm happy to move it inside if you'd rather keep the keys together.

…sample.php`.

Addresses review. `wp-config-sample.php` now defines `WP_SECRETS_KEY` with the placeholder, after the keys and salts and outside their group, and `setup-config.php` replaces it in the same way it fills in the salts. A custom or older localized sample without the line still gets it added after the salts. Without a CSPRNG the key comes from `wp_generate_password()`, as the salts do, rather than being left out.

This depends on the Secrets API rejecting the placeholder, which the patch on #66187 that requires `WP_SECRETS_KEY` adds.

See #66399, #66187.
@ericmann

Copy link
Copy Markdown
Author

This is working for a manual install but a fresh install via WP-CLI. It looks like an associated PR will need to be made to wp-cli/config-command, wp-cli/core-command or both.

Added some notes inline.

Agreed. wp config create writes its own file, so it needs the same change. I'll open an issue on wp-cli/config-command. wp config shuffle-salts already touches only the eight keys and salts, so it's fine as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants