Feature plugin for the Secrets API proposed for WordPress 7.2. Encrypted, versioned credential storage with pluggable storage and keyring back ends.
This exists so contributors can run the API instead of reading about it, and so sites on 6.6
and later get something usable before the core patch lands. Everything under src/ is written to
be copied verbatim into wordpress-develop — same paths, same coding style, same default text
domain.
Once core ships the API, the plugin stands down on its own and says so in an admin notice rather than shadowing core's implementation.
Status: feature plugin, pre-core-merge. The public API surface is settled — it is the one described in the proposal. See
docs/open-questions.mdfor what is deliberately unresolved.
Two commands. The second brings up wp-env, installs the WordPress test suite inside it, and runs exactly what CI runs.
composer install
bin/ci-local.shAdd --keep to leave the environment running between iterations.
If you already have a WordPress test suite and a database, skip wp-env entirely:
make install # composer install + bin/install-wp-tests.sh
make cimake ci is the single source of truth. The CI workflow is a thin wrapper around the same
targets, so a green local run means a green pipeline. Run make with no arguments for the full
target list.
| Target | What it does |
|---|---|
make lint / make lint-fix |
phpcs / phpcbf |
make compat |
PHPCompatibilityWP at testVersion 7.4- |
make analyse |
phpstan |
make test / make test-ms |
phpunit, single site / multisite |
make coverage |
phpunit with an HTML coverage report (see docs/open-questions.md re: wp-env) |
make ci |
all of the above |
Runners without egress to wordpress.org can point the installer at a mirror with WP_MIRROR_BASE
or WP_TESTS_ZIP_URL. See docs/ci.md.
A key hierarchy in which exactly one value is ever stored wrapped:
flowchart LR
SK["<b>site key</b><br/>from the keyring"]
RK["<b>root key</b><br/>one per install<br/><i>the only wrapped value stored</i>"]
MK["<b>master key</b><br/>per scope<br/><i>derived on demand, never stored</i>"]
DK["<b>data key</b><br/>per secret, per slot"]
V["<b>secret value</b>"]
SK -- wraps --> RK
RK -- derives --> MK
MK -- wraps --> DK
DK -- encrypts --> V
Rotating the site key re-wraps one value, on a single site or on a 500-site network.
A few rules the design won't bend on:
- Encryption is unconditional. There is no plaintext mode and no constant to disable it.
- No filter on the retrieval path. Nothing intercepts a credential between storage and the caller. A filter there would amount to a supported way of reading every credential on the site.
- Fail closed. An unreachable store or keyring is a
WP_Error, never a fallback to local storage or local key wrapping. - No export.
WP_Secret::reveal()is the only path to a stored plaintext. Migrations and staging pushes mean re-entry at the destination. - Three states, never collapsed.
wp_get_secret()returns aWP_Secret,nullwhen the secret does not exist, or aWP_Errorwhen it exists but could not be retrieved. Absent and broken are different things.
There is no per-plugin isolation. Namespacing (plugin-slug/secret-name) is organisational:
it groups secrets by owner so listings, and a future admin screen, can be sensible. It was never
meant to be an access-control or visibility boundary. Masking guards against shoulder-surfing and
accidental logging; it isn't a privilege boundary either. Any plugin that can run PHP can read
any secret.
There is no admin settings screen. The proposal defers it to 7.3. The hooks and accessors a future screen needs are in scope; the screen is not.
- PHP 7.4+ (core's floor —
src/contains no PHP 8 syntax) - WordPress 6.6+
- libsodium, via the extension or core's bundled
sodium_compat
Some plugins were built against an earlier prototype of this idea. This plugin does not implement
that prototype's API. Instead, a read for a secret that exists only in the prototype's format is
upgraded into the current format the first time it is read, and the prototype's own data is never
touched or deleted. See docs/migrating-from-displace.md.
Platforms that manage credentials themselves — a KMS-backed store, an HSM, a control panel that is the system of record — all work, and each of them protects a credential better at rest than the default does. The rule is stronger than the default, never weaker. Plaintext at rest is still banned; what changed is that "WordPress has to do the encrypting" turned out to be a stand-in for that rule rather than the rule itself.
WP_Secrets_Provider is where a platform plugs in, and the provider that ships with WordPress is
one implementation of it rather than a privileged case.
docs/host-provider-model.md has the reasoning, the routing rules,
and what does not flex.
Three interfaces, outermost first:
| Interface | Replaces | Reach for it when |
|---|---|---|
WP_Secrets_Provider |
Everything — the platform is the system of record | A control panel, Secrets Manager, or an HSM holds the credential |
WP_Secrets_Keyring |
How the root key is wrapped | A KMS holds your keys, but secrets stay in WordPress |
WP_Secrets_Store |
Where a record lives | Ciphertext belongs somewhere other than wp_options |
The keyring is what most hosts actually want, and it's three methods. A wp-content/secrets.php
drop-in installs any of them. See docs/extending.md for the contracts and
docs/drop-in-example.php for a runnable skeleton.
examples/ holds reference implementations for wiring this API to a cloud provider.
Nothing there is loaded by the plugin, and it's excluded from make ci so those SDK dependencies
never become this project's. Read its README before writing one: a key-management service (AWS
KMS, Google Cloud KMS) is a WP_Secrets_Keyring and takes three methods, while a secret store
(Secrets Manager, Parameter Store) is a WP_Secrets_Provider and takes eight. People routinely
pick the wrong one and pay for it in per-operation API calls.
See CONTRIBUTING.md — especially for what feedback this most needs, which
right now is someone implementing WP_Secrets_Provider against a real platform and reporting
what broke.
Commits are small and logically scoped, and tests land in the same commit as the code they cover.
Before changing anything under src/, read the constraints above — several of them are enforced
by architectural tests that read the source, and those tests are never weakened to make a build
green.
Security issues go through SECURITY.md rather than a public issue: this project
stores credentials, and a flaw in it is a flaw in the thing protecting everything else.
CI (.github/workflows/ci.yml) is a thin wrapper around the make targets above, running on
github.com's hosted runners: static analysis gates a PHP 7.4/8.0/8.3 × WordPress latest/trunk
matrix plus a multisite job. See docs/ci.md.