Skip to content

Secrets API: Require WP_SECRETS_KEY - #15258

Closed
ericmann wants to merge 9 commits into
WordPress:trunkfrom
ericmann:secrets-require-key
Closed

ericmann wants to merge 9 commits into
WordPress:trunkfrom
ericmann:secrets-require-key

Conversation

@ericmann

@ericmann ericmann commented Oct 30, 2026 •

Copy link
Copy Markdown

Without WP_SECRETS_KEY, the default keyring derived its key from LOGGED_IN_KEY . LOGGED_IN_SALT. This removes that fallback, as #66187 comment:27 proposes.

Two problems with the fallback:

  • Rotating the salts locked every stored secret. Rotation is routine advice for the salts (comment:23, confirmed in comment:24).
  • The salts were read directly, not through wp_salt(). So a translated placeholder from a localized wp-config-sample.php, or a value shared by several salts, became the key (comment:29).

What changes

  • No fallback. Without a usable WP_SECRETS_KEY, every call that needs the key returns secret_key_unavailable. That includes WP_SECRETS_KEY_PREVIOUS during a rotation.
  • Placeholders are rejected in every language. A key equal to the wp-config-sample.php placeholder is not usable. Neither is one equal to any of the eight keys and salts, nor one that is empty or only whitespace. An unfilled sample gives all of them the same phrase, translated or not, so the second check catches a localized sample without calling __(). A __() check would depend on timing: it returns English before load_default_textdomain(), and after that it follows the current user's locale.
  • Site Health gives the fix. The key source test reports a missing or unusable key as "recommended". It shows a newly generated define() line, to add, or to replace the unusable one with. The "using a fallback key" result is gone. Site Health reads the key's state from WP_Secrets_Config_Key_Provider::get_key_status() and its KEY_STATUS_* constants, never from the translated get_key_source() text.
  • wp_is_secrets_api_available(). A plugin can check it before moving a credential into the Secrets API. It returns false when the drop-in failed to load, when the default keyring has no usable key, or when the key cannot unwrap the stored root key. The last is a site that stored secrets and then added or changed the key without re-wrapping the root key (comment:56).
  • Test bootstrap. It defines the fixed test key whenever WP_SECRETS_KEY is not defined, not only when the salts are placeholders. As before, it doesn't do this in process-isolated tests.

#66399 / #15124 adds the placeholder to wp-config-sample.php and has setup-config.php fill it in. That PR needs this one first, because only this one rejects the placeholder.

#15263 (#66380) is built on this one and should land after it.

Testing

  • --group secrets: 423 tests on single site and 431 on multisite, no failures (PHP 8.5, trunk at r65378). With trunk's three src/ files and this PR's tests, there are 21 errors and 7 failures, all in the new tests.
  • New tests:
    • an undefined key
    • the placeholder
    • the value of a salt
    • an empty or whitespace-only key
    • a translated, unfilled sample
    • WP_SECRETS_KEY_PREVIOUS not leaking into the current key
    • both new Site Health results
    • wp_is_secrets_api_available(), including a key that cannot unwrap the stored root key
  • test_key_unavailable_is_wp_error_not_null now writes under WP_SECRETS_KEY_PREVIOUS instead of the ambient salts, so it no longer skips on placeholder salts. Neither does any other Secrets API test.
  • PHPCS is clean on the changed files. PHPStan passes with core's configuration.

A site that stored secrets under the fallback, for example during the beta, can move to a dedicated key. Set WP_SECRETS_KEY to a new key and WP_SECRETS_KEY_PREVIOUS to the old LOGGED_IN_KEY . LOGGED_IN_SALT, then re-wrap the root key with WP_Secrets_Key_Manager::rotate_site_key(). The feature plugin's wp secret rotate does this. A previous key in that form is hashed exactly as the fallback hashed the salts.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Drafting the patch, tests and this description, and running the checks above. 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 default keyring derived its key from `LOGGED_IN_KEY` and `LOGGED_IN_SALT`. Rotating the salts, which is routine advice, then made every stored secret unreadable. The constants were also read directly rather than through `wp_salt()`, so a translated placeholder from a localized `wp-config-sample.php`, or a value shared by several of the salts, became the source of the key.

There is no fallback any more. Without a usable `WP_SECRETS_KEY`, every call that needs the key returns `secret_key_unavailable`. A key equal to the `wp-config-sample.php` placeholder is not usable, and neither is one equal to any of the authentication keys and salts. An unfilled sample gives all of them the same phrase, so the second check rejects the placeholder in any language.

The Site Health key source test now reports a missing or unusable key as recommended, with a newly generated line to add to `wp-config.php`.

Follow-up to [64337].

See #66187, #66399.
@github-actions

github-actions Bot commented Oct 30, 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 testing well.

I've left a couple of notes inline re: i18n and a possible helper. Otherwise it's notes about test suite improvementws.

Resolves the conflicts with r65272: the tests this branch changes now follow the
same conventions, with a summary, `@covers` and a failure message for each
assertion.
`WP_Secrets_Config_Key_Provider::get_key_source()` returned English text that
Site Health showed as is and also searched for "not defined", "not usable" and
"legacy interpretation" to choose its result. The text could not be translated
without breaking those checks.

The provider now reports the state of `WP_SECRETS_KEY` through
`get_key_status()`, which returns one of four `KEY_STATUS_*` constants. Site
Health uses that, and `get_key_source()` returns translated text.
Now that `WP_SECRETS_KEY` is required, a plugin needs to know whether the
Secrets API can be used before moving a credential into it. This returns false
when the secrets.php drop-in failed to load, or when the default keyring has no
usable key. A provider or keyring from a working drop-in manages its own keys,
so it is reported as available.

Suggested by peterwilsoncc in review.
ericmann added a commit to ericmann/wordpress-develop that referenced this pull request Nov 6, 2026
Brings in trunk through r65272 and the updated WordPress#15258. The key source test now
uses `WP_Secrets_Config_Key_Provider::get_key_status()` rather than the text of
the key source, and the Site Health tests this branch adds or changes follow
r65272's conventions: a summary, `@covers`, and a failure message for each
assertion.
@ericmann

ericmann commented Nov 6, 2026

Copy link
Copy Markdown
Author

Updated:

  • Merged trunk, including r65272, so this no longer conflicts.
  • get_key_source() is translated, and Site Health uses the new get_key_status() instead of its text.
  • Added wp_is_secrets_api_available().
  • The tests this PR touches follow r65272's conventions.

--group secrets: 395 tests single site, 403 multisite. #15263 is updated to match.

@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.

A few minor notes inline.

…te Health.

A WP_SECRETS_KEY of only whitespace was accepted as key material. It is now
treated like an empty key.

Site Health wraps WP_SECRETS_KEY, wp-config.php and wp-config-sample.php in
<code>, as the other Site Health results do, and quotes the translated
"stop editing" line the way network.php does.

From review on PR 15258 by peterwilsoncc.
… the root key.

A site that stored secrets and then added or changed WP_SECRETS_KEY without
re-wrapping the root key has a usable-looking key, but every read and write
fails. wp_is_secrets_api_available() now checks the stored root key, so a
caller that checks first keeps its existing storage instead of failing on the
write. With no root key stored yet, the API is still available: the first write
creates one under the key.

See #66187 comment:56.
@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 65395
GitHub commit: 0b25c19

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Nov 10, 2026
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