Secrets & credentials management
When to use this skill
Use this skill whenever code handles sensitive credentials:
- Storing an API key, webhook secret, or service token from a settings form.
- Building a custom login or token-verification flow.
- Generating reset tokens, API keys, or one-time nonces that must stay secret.
- Logging or exporting data that might accidentally include secrets.
- Deciding whether to use the user's account password or an application-specific password for API/integration access.
Secrets in source code, reversible password storage, and plaintext API keys in options are common sources of credential leaks.
Related: see the nonces-csrf-protection skill for CSRF tokens and the
settings-options-security skill for sanitizing option values.
Core principles (and why they matter)
- Never hardcode secrets. API keys and passwords committed to source control leak to anyone with repo access and often end up in public repositories.
- Never store user passwords reversibly. WordPress stores passwords as salted hashes
via
wp_hash_password(); usewp_check_password()to verify. Do not roll your own hash. - Prefer wp-config constants or encrypted option storage for service keys. Put keys in
wp-config.phpconstants or encrypt them at rest with libsodium so the database alone is not enough to recover them. - Use Application Passwords for API auth. WordPress 5.6+ provides
WP_Application_Passwordsfor machine-to-machine auth without exposing the user's account password. - Never echo or log secrets. Tokens and keys should never appear in HTML, logs, error messages, shell history, or exported backups.
- Generate tokens with
wp_generate_password()orwp_create_nonce(). They are designed for cryptographic strength (nonces are short-lived; passwords/tokens can be longer).
Step-by-step implementation
- For user passwords: hash with
wp_hash_password()on save, verify withwp_check_password(). - For service/API keys:
- Accept via a secure settings form (nonce + capability + sanitize).
- Store in a
wp-config.phpconstant if possible, or encrypt withsodium_crypto_secretbox()using a key derived fromwp_salt()(or a dedicated constant) beforeupdate_option().
- For machine auth: use
WP_Application_Passwords::create_new_application_password(). - For tokens: generate with
wp_generate_password( $length, false ); store only a hash if you need to verify it later. - Sanitize any log/export output to redact known secret keys.
- If a secret leaks, respond in this order:
- Rotate first. Assume compromise the moment the secret left your control — once
pushed to a remote, treat it as captured (clones, forks, and scrapers may already
hold it). Issue a new key at the provider, then update the
wp-configconstant or re-save the encrypted option, and revoke the old key; issuing a replacement alone may leave the leaked key usable. For active abuse, revoke immediately. - Revoke what rotation cannot cover. Delete leaked Application Passwords
(Users → Profile → Application Passwords) and destroy sessions for affected users
with
WP_Session_Tokens::get_instance( $affected_user_id )->destroy_all()for each trusted, verified affected user ID.wp_destroy_all_sessions()targets only the current user, not an arbitrary affected user. Session revocation does not revoke Application Passwords. Ifwp-config.phpleaked, rotate all eight keys/salts and exposed database/service credentials. If an encryption key changes, re-encrypt retained secrets before discarding it; salt-derived keys also change when salts rotate. Seewp-hardening-best-practices. - Consider history cleanup after revocation, never instead. If needed, plan a
git filter-reporewrite with repository owners and collaborators. Do not automatically rewrite history or force-push: publishing rewritten history is destructive and requires explicit approval and coordinated protection of others' work. Old clones still contain the secret and can reintroduce it; arrange re-cloning or careful cleanup. Forks and cached views need separate coordination; consult GitHub Support's removal criteria where applicable. - Add push protection to block supported secrets when pushing to GitHub, not when making local commits. Use local secret scanning/pre-commit checks for earlier feedback; neither catches every secret or replaces safe handling.
- Rotate first. Assume compromise the moment the secret left your control — once
pushed to a remote, treat it as captured (clones, forks, and scrapers may already
hold it). Issue a new key at the provider, then update the
Supporting references
| Reference | Load when |
|---|---|
| Secrets & credentials management checklist | Before final verification of the secrets & credentials management controls. |
| Secure secret storage | Implementing service API-key storage through wp-config.php constants or encrypted options. |
Common AI mistakes / anti-patterns
Mistake 1 — API key as a string literal
// ❌ Insecure: key is in source control and readable by anyone with file access.
$api_key = 'sk-live-abc123';
// ✅ Secure: read from a wp-config constant or an encrypted option.
if ( ! defined( 'MY_PLUGIN_API_KEY' ) ) {
wp_die( esc_html__( 'API key is not configured.', 'my-plugin' ) );
}
$api_key = MY_PLUGIN_API_KEY;
Mistake 2 — Storing user passwords in plaintext or with a weak hash
// ❌ Insecure: reversible or weak storage.
update_option( 'my_plugin_user_password', $_POST['password'] );
update_option( 'my_plugin_user_password', md5( $_POST['password'] ) );
// ✅ Secure: use WordPress password hashing.
$hash = wp_hash_password( $_POST['password'] );
update_option( 'my_plugin_user_password_hash', $hash );
// Verify later:
if ( wp_check_password( $submitted, $stored_hash ) ) { /* ... */ }
Mistake 3 — Logging tokens or API keys
// ❌ Insecure: secret lands in error_log / shell history.
error_log( 'Request failed with token: ' . $api_key );
// ✅ Secure: log an identifier or redacted value.
error_log( 'Request failed for token ending in: ' . substr( $api_key, -4 ) );
Mistake 4 — Reusing nonces as long-lived secrets
// ❌ Insecure: nonces expire and are not designed for persistent auth.
$token = wp_create_nonce( 'my_api' );
update_user_meta( $user_id, 'my_api_token', $token );
// ✅ Secure: generate a dedicated token and hash it for storage.
$token = wp_generate_password( 32, false );
$token_hash = wp_hash_password( $token );
update_user_meta( $user_id, 'my_api_token_hash', $token_hash );
// Hand the plaintext token to the user once, then verify with wp_check_password().
Mistake 5 — Storing service keys in plaintext options
// ❌ Insecure: database dump exposes the key.
update_option( 'my_plugin_stripe_key', $_POST['stripe_key'] );
// ✅ Secure: encrypt at rest with libsodium (PHP 7.2+).
function my_plugin_encrypt_secret( $plaintext ) {
$key = sodium_base642bin( MY_PLUGIN_ENCRYPTION_KEY, SODIUM_BASE64_VARIANT_ORIGINAL );
$nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
$cipher = sodium_crypto_secretbox( $plaintext, $nonce, $key );
return base64_encode( $nonce . $cipher );
}
function my_plugin_decrypt_secret( $encoded ) {
$raw = base64_decode( $encoded );
$nonce = substr( $raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
$cipher = substr( $raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
$key = sodium_base642bin( MY_PLUGIN_ENCRYPTION_KEY, SODIUM_BASE64_VARIANT_ORIGINAL );
return sodium_crypto_secretbox_open( $cipher, $nonce, $key );
}
Correct code examples
A complete settings-page snippet that stores an API key either as a wp-config constant or
encrypted in options is in references/secure-secret-storage.php.
Checklist
- No secrets, API keys, or passwords are hard-coded in source files.
- User passwords are hashed with
wp_hash_password()and verified withwp_check_password(). - Service/API keys are stored in
wp-config.phpconstants or encrypted at rest. - Machine-to-machine auth uses Application Passwords where possible.
- Tokens are generated with
wp_generate_password()(orwp_create_nonce()for short-lived CSRF). - Stored tokens are verified against a hash, not compared in plaintext.
- Secrets are never echoed, logged, exported, or sent to the browser.
- Leaked credentials are revoked/rotated; affected users' sessions are revoked when warranted.
- Any history rewrite/publication has explicit approval and coordinated clone/fork cleanup.
- Settings forms that collect secrets use nonce + capability + HTTPS.