Blog · Security

API Keys and Secrets: Stop Committing Them to the Theme

Every WordPress site eventually needs an API key. Cloudflare, a mail provider, a payment gateway, a mapping service, an analytics platform. The key gets pasted into functions.php to get something working, the fix ships, and nobody thinks about it again until the key shows up in a public GitHub repo or a support ticket asks why a bot is draining an email quota. This is one of the most common and most avoidable security failures on WordPress sites, and fixing it is mostly a matter of habit, not tooling.

Why secrets end up in the wrong place

Secrets leak into themes and plugins for predictable reasons. A developer is testing quickly and hardcodes a key to save a step. A theme gets copied from a staging site to production, key and all. A snippet from a tutorial gets pasted in wholesale, credentials included. Then the theme folder gets zipped up, shared with a contractor, pushed to a public repository, or backed up to a shared drive, and the key travels with it, permanently, in every copy and every commit.

The core problem is that theme and plugin files are treated as code, but secrets are not code. Code is meant to be shared, versioned, and inspected. Secrets are meant to be private and disposable. Mixing the two means every place your code goes, your secrets go too.

Where secrets actually belong

The right home for a secret is somewhere that is never committed to version control and never bundled into a theme or plugin package. On a typical WordPress install that means wp-config.php, using constants defined above the line that says require_once ABSPATH . 'wp-settings.php';:

define( 'MAILER_API_KEY', 'your-key-here' );
define( 'CLOUDFLARE_API_TOKEN', 'your-token-here' );

Your theme or plugin code then references the constant instead of a literal string:

$api_key = defined( 'MAILER_API_KEY' ) ? MAILER_API_KEY : '';

wp-config.php sits outside the web root on many managed hosts, is excluded from theme exports, and should never be tracked in git. If your workflow uses environment variables instead (common with containerized or CI-driven deployments), the same principle applies: read the value at runtime with getenv() and keep the actual value out of any file that gets committed or packaged.

You can confirm what constants are actually defined in your config with wp-cli, which is useful when auditing a site you did not build:

wp config list

Run this only on a system you trust, since it will print the values, not just the names. That is also a reminder of why wp-config.php needs strict file permissions and should never be world-readable.

Keeping git clean

If your theme or plugin lives in a git repository, treat any file that might ever contain a secret as untouchable by version control. A basic .gitignore should exclude wp-config.php, any .env file, and local configuration overrides. But a .gitignore only stops future commits. It does nothing for a key that is already in your history.

Before assuming a repo is clean, search its full history, not just the current working tree:

git log -p -- wp-content/themes/your-theme | grep -i "api_key\|secret\|token"

You can run the same kind of search across the working directory for anything that looks hardcoded:

grep -R -i "api_key\|secret\|token\|password" --include=*.php .

If a search turns up a real credential buried in an old commit, rewriting history to remove it (with a history-rewriting tool built for that purpose) only helps if every clone and fork is also purged, which in practice is rarely guaranteed. The safer and more realistic assumption is: if a real secret was ever committed, it should be treated as burned and rotated, regardless of whether you manage to scrub the history.

Rotation is not optional

Keys that never expire and never change are a liability even if they never technically leak. Contractors come and go with copies of your codebase. Staging environments get cloned and forgotten. Old backups sit in cloud storage indefinitely. A sane rotation habit limits the blast radius of all of that:

  • Generate keys scoped as narrowly as the provider allows. A Cloudflare API token limited to zone-level DNS edits is far safer to leak than a global account API key.
  • Rotate keys on a schedule, even without a known incident, especially for anything with billing or send-mail privileges.
  • Rotate immediately whenever someone with access to the credential (an employee, a contractor, an agency) leaves the project.
  • Keep a simple record of which key does what and where it is used, so rotation does not turn into a guessing game about what will break.

Scoped, short-lived, well-documented keys turn a potential leak from a full account compromise into a contained, quickly fixed inconvenience.

If a key leaks: the response checklist

When you discover a secret has been exposed, whether from a public repo, a shared screenshot, or a support conversation, speed matters more than diagnosis. Work through this order:

  1. Revoke or rotate the credential immediately at the provider, before doing anything else. Every minute the old key is valid is a minute someone else can use it.
  2. Check the provider’s usage or audit logs for activity you do not recognize: unfamiliar IP addresses, unexpected API calls, or usage spikes that do not match your own traffic.
  3. Audit WordPress itself for signs the leak was part of a broader compromise, not an isolated mistake. Look for unexpected admin accounts:
wp user list --role=administrator
  1. Check for plugins you did not install:
wp plugin list
  1. Verify core and plugin files against their known-good checksums to catch tampering:
wp core verify-checksums
wp plugin verify-checksums --all
  1. Force a password reset for any WordPress users with elevated roles, and rotate any other secrets that were stored alongside the leaked one, since they were likely exposed together.
  2. Remove the leaked value from wherever it was found, whether that is a public repo, a shared document, or an old backup, understanding that removal does not undo exposure. That is exactly why step one, revoking the credential, has to happen first.

None of this requires exotic tooling. It requires treating a leaked key the way you would treat a lost house key: change the lock first, then figure out how it went missing.

The takeaway

Secrets do not belong in themes, plugins, or git history, they belong in wp-config.php constants or environment variables that never get packaged or committed. Scope keys narrowly, rotate them on a schedule and whenever access changes, and if one does leak, revoke it first and investigate second. A managed host worth using will keep the server hardened around all of this, but the discipline of where a secret lives and how long it stays valid is squarely on the people who write the code, and it is one of the cheapest security habits you can build.