🇹🇷 Türkçe: Bu yazının Türkçesini oku →

Every WordPress site I have inherited in the last twenty years has one drawer nobody wants to open. It is the wp_options table, and inside it are dozens of rows named something like stripe_secret_key, _sendgrid_api_key, mp_smtp_password, anthropic_api_key, all sitting there in plaintext. Take a database dump for a migration, and every one of those secrets rides along with it. Send that dump to a developer over email so they can spin up a staging clone, and the secrets are now in an inbox somewhere. This is the normal state of a WordPress site in 2026, and it has been the normal state for the entire time I have been in this ecosystem.

I say this without moral drama. WordPress inherited this from a decade when the concept of “an API key” barely existed on the web at all, and the plugin ecosystem grew up around a storage table that was never designed to hold secrets. Everyone knows the situation. Site Kit rolled its own encryption. WooCommerce rolled its own for a subset of gateways. SMTP plugins each shipped a slightly different scheme. Nothing shared, nothing standard, and nothing an auditor could point at and call a boundary.

Yesterday Eric Mann filed the proposal that finally tries to close that drawer.

What is actually new

On August 25, Eric Mann published a proposal for a Secrets API in WordPress 7.2 on Make/Core. The shape is small and deliberately unambitious: four functions, one value object, one drop-in for platform vendors.

wp_set_secret( string $name, string $value ): bool|WP_Error
wp_get_secret( string $name, string $version = WP_Secret_Version::CURRENT ): WP_Secret|null|WP_Error
wp_delete_secret( string $name ): bool|WP_Error
wp_import_option_as_secret( string $option, string $name ): bool|WP_Error

WP_Secret::reveal(): string
WP_Secret::fingerprint(): string

Five design choices matter more than the function list. First, encryption is unconditional. There is no plaintext mode and no constant to turn it off. Every secret is wrapped by a per-secret data key which is wrapped by a master key, using PHP’s bundled libsodium (or sodium_compat where the extension is disabled). Key material comes from a dedicated WP_SECRETS_KEY constant if you define it, or falls back to LOGGED_IN_KEY and LOGGED_IN_SALT if you do not.

Second, there are no filters on the retrieval path. This is unusual for WordPress, and the proposal explains why bluntly: any filter that receives plaintext secrets becomes a documented interception point. Extension happens through two pluggable providers loaded via a secrets.php drop-in — one for where the ciphertext lives (Vault, AWS Parameter Store, a host API) and one for what wraps the master key (KMS, HSM). Neither provider ever sees plaintext.

Third, wp_get_secret() has three return states that mean three different things: a WP_Secret object on success, null when the secret does not exist, and WP_Error when it exists but cannot be decrypted. That distinction is what stops your plugin from cheerfully re-running the “connect your account” wizard because a salt got rotated on the host.

Fourth, the API is write-only from the outside. There is no export. Getting the plaintext value out of a WP_Secret requires an explicit ->reveal() call, which makes every point of use greppable in a code review. The object masks itself in logs, var_dump(), and error output, and it refuses serialization outright so it cannot leak into a persistent object cache on shared hosting.

Fifth, there is no automatic migration. Core cannot reliably distinguish “this option holds a Stripe key” from “this option holds a widget setting”, so it does not try. Plugin authors call wp_import_option_as_secret() deliberately, once per key, and imported values are flagged for rotation because the plaintext version is already in every backup you own.

The timeline is tight but honest. Feedback runs through mid-September, a feature plugin ships first so the API surface can be tested in real code before it freezes, a Trac patch follows in late September, and the whole thing either lands before 7.2 Beta 1 on October 20 or explicitly defers. No admin UI in 7.2 — that is planned for 7.3, after real usage data exists. WP-CLI is in scope from day one, which fixes its own long-standing leak of credentials into shell history and process lists. A working proof-of-concept plugin has been shipping for about six months and is what the API surface is modelled on.

Why it matters for WordPress / WooCommerce people

The threat model here is not “someone gets shell on your server”. If that happens, nothing running in the WordPress process can protect you — the proposal is candid about that. The threat model is the overwhelmingly more common one: a database dump ends up somewhere it should not be. A backup is stored in an S3 bucket that turns out to be public. A staging clone is handed to a contractor who copies it to a laptop. A SQL injection in an unrelated plugin exposes a read of wp_options. In every one of those scenarios today, all of your API keys are compromised.

What the Secrets API changes is the value of a stolen database. That is not a small thing. For a WooCommerce store, the credentials in wp_options are the live payment gateway keys, the SMTP relay, the shipping label account, the tax calculation service, the ERP sync token, the newsletter provider, and — increasingly — one or more AI provider keys that bill on metered usage. A leaked Stripe test key is a support ticket. A leaked Anthropic or OpenAI production key is a bill that will keep growing until someone notices.

There is a second thing worth naming. This proposal is the first honest attempt to give agency operators an answer to a question we get asked in every enterprise onboarding: “Which credentials does this WordPress site hold, who put them there, and when were they last rotated?” The current answer is a shrug and a database export. With namespaced secrets, an audit hook that fires on change with old and new fingerprints, and eventually a screen in 7.3, that becomes a real answer. That is governance in the sense WordPress has always meant it — a boring, mature layer that scales because it exists at all.

Note the piece that is deliberately not in v1. There is no per-plugin isolation. Any code running as WordPress can call wp_get_secret() for any namespace. Plugin slugs are not authenticated. If a plugin you install is malicious, it has always had your keys and it still will. That is a real limitation, and Eric Mann calls it out in the proposal rather than pretend otherwise. Do not read the Secrets API as a plugin firewall.

What I would do (or not do) about it

My recommendation is short.

If you run agency work: read the proposal this week and post feedback on the Make/Core thread while the API surface is still cheap to change. The five open questions Eric put at the bottom are the ones your operational reality actually answers — how many rotation slots you need, which WP-CLI commands consuming secrets are most painful today, whether the “readable but not writable” case that Pantheon flagged for platform stores affects your hosting stack. This is the window where feedback lands as code. In two months it lands as a support ticket.

If you maintain a plugin: install the Displace Secrets Manager proof-of-concept on a dev site, wire one of your credentials through it end-to-end, and see what breaks. The whole point of publishing as a feature plugin first is to surface awkward API edges before they freeze. Two hours of your time now is worth an ecosystem where the shape lands right.

If you run production WordPress or WooCommerce sites: do not migrate anything yet. The API is a proposal, not a release. The feature plugin is a preview. Even if it lands in 7.2 in October, imported secrets are flagged for rotation because the plaintext version is already in every one of your backups — meaningful migration means rotating those keys with your upstream vendors, one by one, in a maintenance window, with a rollback plan. Add it to the Q1 2027 hygiene backlog. Do not front-run it.

And do not buy anything with “WordPress secrets” in the marketing copy over the next six months. There will be plugins. Some will be well-meaning. None of them will be the standard interface until the standard interface exists in core. If a client asks you to enable “encrypted API key storage” this quarter, the honest answer is that WordPress is working on it, the design is public, and the right move is to wait for the right implementation rather than paper over the gap with a third one.

One personal note. I still remember shipping multi-language CD-ROM projects in the mid-1990s where the “API key” for the online layer lived hardcoded in a Lingo script, compiled into the cast file, and burned onto a hundred thousand discs. When it leaked — because of course it leaked — the fix was to press a new run. Twenty-five years later, the plaintext-in-a-known-location pattern is still with us, just in a different table. It is genuinely nice to see WordPress take the shot at fixing it properly, with an unconditional encryption stance and no escape hatch for laziness. That is the version of this proposal I want to see land.

The drawer is finally going to have a lock on it. Read the proposal, leave feedback, and let the design settle before you touch it in production.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window