{"slug": "wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text", "title": "WordPress 7.2 Secrets API: Stop Storing API Keys in Plain Text", "summary": "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.", "body_md": "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.\n\nWordPress 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.\n\n## What the Secrets API Actually Does\n\nThe 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.\n\nUnder 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.\n\nTwo 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.\n\n## The Four Functions You Need\n\nThe API surface is narrow by design. The full [official documentation is on Make WordPress Core](https://make.wordpress.org/core/2026/11/11/the-secrets-api-in-wordpress-7-2/).\n\n- `wp_set_secret( 'my-plugin/api-key', $value )` — stores encrypted; returns`true` or`WP_Error`\n- `wp_get_secret( 'my-plugin/api-key' )` — retrieves a`WP_Secret` object,`null` , or`WP_Error`\n- `wp_delete_secret( 'my-plugin/api-key' )` — removes the stored credential\n- `wp_import_option_as_secret( 'old_option_name', 'my-plugin/api-key' )` — migrates existing plaintext options\n\nSecret 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.\n\n## Write Your Plugin’s Getter Correctly\n\nThe right pattern uses feature detection so your plugin runs on 7.1 and earlier while taking advantage of the new API on 7.2+:\n\n```\nfunction my_plugin_get_api_key(): string {\n    // Allow ops to override via wp-config.php\n    if ( defined( 'MY_PLUGIN_API_KEY' ) && MY_PLUGIN_API_KEY ) {\n        return (string) MY_PLUGIN_API_KEY;\n    }\n\n    // Use Secrets API on WordPress 7.2+\n    if ( function_exists( 'wp_get_secret' ) ) {\n        $secret = wp_get_secret( 'my-plugin/api-key' );\n        if ( $secret instanceof WP_Secret ) {\n            return (string) $secret->reveal();\n        }\n        // WP_Error means the secret exists but cannot be decrypted.\n        // Do NOT fall back to get_option() here.\n        if ( is_wp_error( $secret ) ) {\n            return '';\n        }\n    }\n\n    // Legacy fallback for pre-7.2 installs\n    return (string) get_option( 'my_plugin_api_key', '' );\n}\n```\n\nThe 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.\n\n## Migrating Existing Plugins\n\n`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:\n\n```\nfunction my_plugin_migrate_to_secrets_api(): void {\n    if ( ! function_exists( 'wp_import_option_as_secret' ) ) {\n        return;\n    }\n\n    $legacy = get_option( 'my_plugin_api_key' );\n    if ( ! is_string( $legacy ) || '' === $legacy ) {\n        return;\n    }\n\n    $result = wp_import_option_as_secret( 'my_plugin_api_key', 'my-plugin/api-key' );\n    if ( is_wp_error( $result ) ) {\n        return;\n    }\n\n    // Verify the migration before deleting the original\n    $secret = wp_get_secret( 'my-plugin/api-key' );\n    if ( $secret instanceof WP_Secret && hash_equals( $legacy, (string) $secret->reveal() ) ) {\n        delete_option( 'my_plugin_api_key' );\n    }\n}\nadd_action( 'admin_init', 'my_plugin_migrate_to_secrets_api' );\n```\n\nRun 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.\n\n## What Changed in Beta 5 (Breaking)\n\nIf you tested against an earlier beta, re-test now. [Beta 5 (November 10) introduced two breaking changes](https://developer.wordpress.org/news/2026/11/whats-new-for-developers-november-2026/) that affect anyone who already built against the API.\n\nFirst, `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`.\n\nSecond, `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](https://make.wordpress.org/hosting/2026/11/12/call-to-test-wordpress-7-2-secrets-api/).\n\nNew 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:\n\n```\nphp -r 'echo base64_encode( random_bytes( 32 ) ), \"\\n\";'\n```\n\n## Start Now\n\nRC1 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.", "url": "https://wpnews.pro/news/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text", "canonical_source": "https://byteiota.com/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text/", "published_at": "2026-11-15 20:08:21+00:00", "updated_at": "2026-11-15 20:21:50.862609+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["WordPress", "WordPress 7.2", "WordPress 7.1", "WP_Secret", "Make WordPress Core", "libsodium", "XChaCha20-Poly1305", "WP_SECRETS_KEY"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text", "markdown": "https://wpnews.pro/news/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text.md", "text": "https://wpnews.pro/news/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text.txt", "jsonld": "https://wpnews.pro/news/wordpress-7-2-secrets-api-stop-storing-api-keys-in-plain-text.jsonld"}}