cd /news/developer-tools/wordpress-7-2-secrets-api-stop-stori… · home › topics › developer-tools › article
[ARTICLE · art-170116] src=byteiota.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

WordPress 7.2 Secrets API: Stop Storing API Keys in Plain Text

WordPress 7.2 will ship a Secrets API that stores plugin credentials as encrypted, versioned objects instead of plain-text strings in the options table, with release candidate 1 due November 17 and final release December 8, according to the official Make WordPress Core documentation. An audit of one WordPress 7.1 installation with 48 active plugins found 56 API keys and tokens stored as plain text, 36 of them autoloaded on every page request, 48 present in a six-month-old database dump, and 15 AI service keys still active when tested. The API uses XChaCha20-Poly1305 (libsodium) with a four-tier key hierarchy rooted in a WP_SECRETS_KEY constant in wp-config.php, exposes four functions (wp_set_secret, wp_get_secret, wp_delete_secret, wp_import_option_as_secret), and keeps CURRENT and PREVIOUS version slots per secret for rotation without downtime.

read5 min views2 publishedNov 15, 2026
WordPress 7.2 Secrets API: Stop Storing API Keys in Plain Text
Image: Byteiota (auto-discovered)

An audit of a single WordPress 7.1 installation with 48 active plugins turned up 56 API keys and tokens sitting in the options table as plain text. Thirty-six of them were autoloaded on every page request. Forty-eight appeared in a database dump from six months prior — still sitting unencrypted on the server. Fifteen of the AI service keys were still active when tested. That is not a misconfiguration story. That is how WordPress has worked for two decades.

WordPress 7.2 ships a Secrets API that replaces this pattern with encrypted, versioned credential storage. RC1 lands November 17. Final release is December 8. If you write plugins, here is what you need to know before your users upgrade.

What the Secrets API Actually Does #

The core idea is simple: instead of a string sitting in wp_options, you get a WP_Secret object. The object prints as [secret:my-plugin/api-key] in logs, in var_dump() output, in JSON responses, and in REST API calls. The actual value only surfaces when you call ->reveal() on it explicitly. Serializing or cloning the object throws an exception.

Under the hood, WordPress uses XChaCha20-Poly1305 (libsodium) with a four-tier key hierarchy. The root key is stored encrypted, derived from a WP_SECRETS_KEY constant you add to wp-config.php. The encrypted credential sits in the options table — same location as before — but now it is ciphertext, not a string your database admin can read.

Two version slots exist per secret: CURRENT and PREVIOUS. Writing a new value moves the old one to PREVIOUS, giving you credential rotation without a service interruption window.

The Four Functions You Need #

The API surface is narrow by design. The full official documentation is on Make WordPress Core.

  • wp_set_secret( 'my-plugin/api-key', $value ) — stores encrypted; returnstrue orWP_Error
  • wp_get_secret( 'my-plugin/api-key' ) — retrieves aWP_Secret object,null , orWP_Error
  • wp_delete_secret( 'my-plugin/api-key' ) — removes the stored credential
  • wp_import_option_as_secret( 'old_option_name', 'my-plugin/api-key' ) — migrates existing plaintext options

Secret names follow the convention plugin-slug/key-name — lowercase letters, digits, hyphens, and underscores, single slash, no leading or trailing hyphens. Names without a slash trigger _doing_it_wrong(). The slash is namespacing, not access control: any plugin on the site can read any secret.

Write Your Plugin’s Getter Correctly #

The right pattern uses feature detection so your plugin runs on 7.1 and earlier while taking advantage of the new API on 7.2+:

function my_plugin_get_api_key(): string {
    // Allow ops to override via wp-config.php
    if ( defined( 'MY_PLUGIN_API_KEY' ) && MY_PLUGIN_API_KEY ) {
        return (string) MY_PLUGIN_API_KEY;
    }

    // Use Secrets API on WordPress 7.2+
    if ( function_exists( 'wp_get_secret' ) ) {
        $secret = wp_get_secret( 'my-plugin/api-key' );
        if ( $secret instanceof WP_Secret ) {
            return (string) $secret->reveal();
        }
        // WP_Error means the secret exists but cannot be decrypted.
        // Do NOT fall back to get_option() here.
        if ( is_wp_error( $secret ) ) {
            return '';
        }
    }

    // Legacy fallback for pre-7.2 installs
    return (string) get_option( 'my_plugin_api_key', '' );
}

The return value of wp_get_secret() has three states, not two, and you must handle all three. WP_Secret means success. null means the secret was never stored — show your setup screen. WP_Error means the secret exists but cannot be decrypted, usually because the encryption key changed. Falling back to get_option() on a WP_Error is the worst possible mistake: you silently bypass encryption for a credential that was deliberately migrated away from plain-text storage.

Migrating Existing Plugins #

wp_import_option_as_secret() handles the common case: you have an option with a plaintext key, and you want to move it to encrypted storage without requiring users to re-enter it:

function my_plugin_migrate_to_secrets_api(): void {
    if ( ! function_exists( 'wp_import_option_as_secret' ) ) {
        return;
    }

    $legacy = get_option( 'my_plugin_api_key' );
    if ( ! is_string( $legacy ) || '' === $legacy ) {
        return;
    }

    $result = wp_import_option_as_secret( 'my_plugin_api_key', 'my-plugin/api-key' );
    if ( is_wp_error( $result ) ) {
        return;
    }

    // Verify the migration before deleting the original
    $secret = wp_get_secret( 'my-plugin/api-key' );
    if ( $secret instanceof WP_Secret && hash_equals( $legacy, (string) $secret->reveal() ) ) {
        delete_option( 'my_plugin_api_key' );
    }
}
add_action( 'admin_init', 'my_plugin_migrate_to_secrets_api' );

Run it on admin_init. Existing installs migrate silently on the first admin page load after upgrading to 7.2. The hash_equals() check is not paranoia — it is the guard against deleting the legacy option before confirming the encrypted copy matches.

What Changed in Beta 5 (Breaking) #

If you tested against an earlier beta, re-test now. Beta 5 (November 10) introduced two breaking changes that affect anyone who already built against the API.

First, WP_Secrets_Keyring, WP_Secrets_Store, and WP_Secrets_Provider are now abstract classes, not interfaces. Any secrets.php drop-in that uses implements WP_Secrets_Keyring will throw a fatal error on 7.2. Update to extends.

Second, WP_SECRETS_KEY in wp-config.php is now mandatory. The earlier fallback to LOGGED_IN_KEY and LOGGED_IN_SALT is gone. If you have secrets stored under the old fallback key on a beta site, you need to perform a three-step re-encryption sequence before RC1. The WordPress hosting team has the procedure.

New installs get WP_SECRETS_KEY generated automatically. Existing sites will see a “Recommended” notice in Site Health until it is added. Generate the key with:

php -r 'echo base64_encode( random_bytes( 32 ) ), "\n";'

Start Now #

RC1 lands in two days. The final release ships December 8. The adoption path is clear: add function_exists() guards, handle all three return states, run the import on admin_init. The options table was never a secrets manager. Everyone knew it. Nobody fixed it. WordPress 7.2 fixes it — use it.

── more in #developer-tools 4 stories · sorted by recency
── more on @wordpress 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/wordpress-7-2-secret…] indexed:0 read:5min 2026-11-15 · —