When Production Security Followed WordPress Home

Archive note, October 2025: The original recovery
involved editing WordPress’s serialized active_plugins
option directly. That worked, but WP-CLI or temporarily moving plugin
directories is safer and easier to audit.

The production database imported successfully. The local site loaded.
I opened /wp-admin, entered credentials that I knew were
valid, and landed back at the login screen.

Resetting the password changed nothing.

The database was not broken. Production security had followed the
database into local development.

Production
assumptions in a local environment

The imported WordPress database carried the active-plugin list and
plugin settings. That included Wordfence and a two-factor authentication
plugin.

Those tools were configured for a public site:

  • Known production URLs
  • Production email delivery
  • IP-based firewall and rate-limit history
  • Two-factor enrollment tied to an existing account
  • Cookies and redirects created for HTTPS

Local WordPress had different URLs, networking, and mail behavior.
The plugins were enforcing policies whose surrounding infrastructure was
missing.

The symptoms were misleading:

  • Credentials appeared to fail silently.
  • Password resets succeeded but did not restore access.
  • The browser returned to the login page.
  • Repeated attempts risked triggering rate limits.

The first instinct was to keep working on the password. The better
question was which code ran after WordPress accepted it.

Confirming the active
plugins

WordPress stores the network-independent active plugin list in the
active_plugins option.

With WP-CLI:

docker exec my-wp wp plugin list

Without WP-CLI, the database reveals the same information:

SELECT option_value
FROM wp_options
WHERE option_name = 'active_plugins';

The value is a serialized PHP array containing plugin paths.

At the time, I manually replaced that serialized value with a shorter
array that omitted the security plugins. It restored access, but it was
brittle. A wrong string length or malformed array can make WordPress
ignore the entire option.

Safer recovery methods

If WP-CLI can bootstrap WordPress, deactivate the specific
plugins:

docker exec my-wp \
  wp plugin deactivate wordfence wp-2fa

If plugin code prevents WP-CLI from loading, skip plugins:

docker exec my-wp \
  wp --skip-plugins plugin deactivate wordfence wp-2fa

Another dependable emergency method is to temporarily rename plugin
directories:

docker exec my-wp mv \
  /var/www/html/wp-content/plugins/wordfence \
  /var/www/html/wp-content/plugins/wordfence.disabled

WordPress will mark the missing plugin inactive on the next request.
Rename it back after access is restored.

These approaches let WordPress maintain its own serialized settings
instead of constructing them by hand.

Local safety is not
“disable everything”

The goal was not to reproduce an insecure production site. It was to
decide which production integrations made sense locally.

Some plugins were merely unnecessary. Others could actively cause
trouble:

  • Security plugins could block login.
  • Backup plugins could create large, redundant archives.
  • CDN plugins could rewrite assets toward production.
  • SMTP plugins could send real email.
  • Webhooks could notify production systems.
  • Analytics could contaminate live reports.

Caching, SEO, and analytics plugins were not automatically harmless
either. Their behavior depended on configuration.

The right approach was an explicit local profile rather than a
universal safe/unsafe list.

Making future imports
predictable

The cleanup eventually belonged in the import workflow.

After importing and rewriting URLs, the script could run:

docker exec my-wp \
  wp --skip-plugins plugin deactivate wordfence wp-2fa

It could also set a local environment type:

define('WP_ENVIRONMENT_TYPE', 'local');

Custom code can use that value to avoid sending external
notifications or loading production-only integrations.

I also learned to preserve a short checklist beside the import
process:

  1. Rewrite URLs.
  2. Disable production security and cache layers as needed.
  3. Block outbound email.
  4. Check webhooks and API credentials.
  5. Clear cookies associated with the production domain.
  6. Verify login before doing theme work.

The lesson

Importing a production database imports behavior, not just
content.

Posts and pages are the obvious payload, but the database also
contains assumptions about networks, identities, external services, and
threat models. A local clone can be technically successful and
operationally unusable.

Direct database access solved the immediate lockout. Understanding
why the lockout happened produced the more durable fix: make environment
cleanup an intentional part of every import.