How to Harden WordPress Without a Security Plugin

··12 min read
How to Harden WordPress Without a Security Plugin

Here's an uncomfortable truth most WordPress tutorials skip: the average security plugin adds 300 to 800 milliseconds of load time to every page request, and studies of compromised WordPress sites consistently show that the majority were running at least one security plugin at the time of the breach. Sucuri's own remediation reports have put the share of hacked sites running an outdated CMS or plugin north of 50 percent for years. In other words, the thing you installed to protect your site is often just another moving part that can go stale, conflict with your theme, or ship its own vulnerability.

The good news is that most of what a security plugin does can be replicated with a handful of edits to files you already control: your .htaccess (or Nginx config), wp-config.php, file permissions, and a few server-level rules. These changes are faster, they don't call home to a vendor, and they can't be silently deactivated by an attacker who gets a foothold in your admin dashboard.

This guide walks you through how to harden WordPress without a plugin, step by step, with real configuration you can copy and adapt. We'll cover file permissions, disabling code execution, locking down logins, hiding version fingerprints, and setting HTTP security headers. By the end you'll have a leaner, faster, and more defensible install than most plugin-heavy sites.

Key Takeaways

  • Set file permissions to 644 for files and 755 for directories, and lock wp-config.php down to 640 or 600.
  • Disable PHP execution in /wp-content/uploads/ — this single rule blocks the most common post-upload exploit.
  • Move login protection to the server layer with rate limiting and IP rules instead of a PHP-based firewall.
  • Add security headers (Content-Security-Policy, X-Frame-Options, HSTS) at the web server, not in a plugin.
  • Disable the file editor and XML-RPC unless you actively use them.
  • Hardening is a layered process. No single edit is a silver bullet, but together they remove most of the low-effort attack surface.

Why skip the security plugin at all?

Plugins are convenient, and for non-technical site owners they remain a reasonable default. But there are real tradeoffs worth naming honestly.

  • Performance cost: A full-featured security suite hooks into init, scans requests, logs events, and often queries its own database tables on every page load.
  • Attack surface: Every plugin is code running with your site's privileges. Security plugins have shipped critical CVEs of their own.
  • False sense of safety: A dashboard full of green checkmarks feels reassuring, but many "hardening" toggles simply write the same rules you could write yourself, sometimes less completely.
  • Deactivation risk: If an attacker reaches your admin panel, they can disable a plugin firewall in one click. A rule baked into .htaccess or your Nginx config survives that.

The manual approach costs you an afternoon and a willingness to edit config files. If that trade appeals to you, read on. If you'd rather audit what you already run first, our walkthrough on how to audit WordPress plugin vulnerabilities before they get exploited is a good companion piece.

Step 1: Lock down file and directory permissions

Permissions are the foundation. Overly permissive files (like 777) let any process write to your code, which is exactly how attackers plant backdoors.

Connect over SSH and run these from your WordPress root:

  1. Set directories to 755: find . -type d -exec chmod 755 {} \;
  2. Set files to 644: find . -type f -exec chmod 644 {} \;
  3. Lock wp-config.php: chmod 640 wp-config.php (use 600 if PHP runs as your user).
  4. Verify ownership: files should be owned by your deploy user, not root or nobody.

A quick before/after

Say you inherited a site where a previous developer set /wp-content/ to 777 so uploads "just worked." A scanner finds it in seconds. An attacker uploads a PHP shell to /wp-content/uploads/2024/07/ and executes it. After you run the commands above and add the upload rule in Step 2, that same shell lands as an inert .php text file the server refuses to run. Same upload, completely different outcome.

Step 2: Disable PHP execution where it doesn't belong

Your uploads folder should hold images and PDFs, never executable code. Blocking PHP there neutralizes the single most common WordPress compromise: uploading a malicious file and running it.

Apache — create /wp-content/uploads/.htaccess:

<FilesMatch "\.(php|php5|phtml|phar)$">
  Require all denied
</FilesMatch>

Nginx — add inside your server block:

location ~* /wp-content/uploads/.*\.php$ { deny all; }

Do the same for /wp-includes/, which should never serve PHP directly to the public. This one rule blocks a class of attacks that plugin firewalls try to catch after the fact with pattern matching, whereas you're removing the capability entirely.

Step 3: Harden wp-config.php

This file holds your database credentials and controls core behavior. A few additions here go a long way.

  • Disable the file editor: define('DISALLOW_FILE_EDIT', true); stops anyone with admin access from editing theme and plugin code in the browser.
  • Block plugin/theme installation on production: define('DISALLOW_FILE_MODS', true); if you deploy via Git or CI.
  • Force SSL for admin: define('FORCE_SSL_ADMIN', true);
  • Rotate your security keys: regenerate the AUTH_KEY block from the official WordPress secret-key API. Changing these logs everyone out and invalidates stolen session cookies.
  • Set the correct DB table prefix at install time. Changing it later is possible but fiddly.

Also move wp-config.php one directory above your web root if your host allows it. WordPress checks the parent directory automatically, and a file outside the document root can't be served by the web server even if a config mistake exposes .php source.

Step 4: Protect the login and XML-RPC endpoints

wp-login.php and xmlrpc.php are the two endpoints bots hammer relentlessly. On a modest site you'll see hundreds of brute-force attempts per day.

Rate-limit and restrict login

If your admin team works from known IPs, restrict access at the server level. In Apache's .htaccess:

<Files wp-login.php>
  Require ip 203.0.113.5
  Require ip 198.51.100.0/24
</Files>

If your team is mobile and IPs change, that's too rigid. Instead, rate-limit repeated hits at the web server (Nginx's limit_req zone works well) and enforce strong passwords plus two-factor at the account level.

Disable XML-RPC if you don't use it

Unless you use the WordPress mobile app or Jetpack's remote features, xmlrpc.php is pure liability — it enables amplified brute-force attacks via the system.multicall method. Block it:

<Files xmlrpc.php>
  Require all denied
</Files>

When IP-based control makes sense

For sites that face persistent, targeted abuse from specific networks, a dedicated IP-management layer beats hand-editing .htaccess for every new range. This is the one area where a purpose-built tool earns its keep, and WordPress IP Blocker Pro lets you manage allow and block lists (including country and CIDR ranges) without wiring up rules by hand. It's a focused utility rather than a bloated suite, which is the spirit of this whole guide.

Step 5: Remove version fingerprints and directory listings

Attackers automate reconnaissance. The less they learn about your exact versions, the harder it is to match you to a known exploit.

  • Remove the generator meta tag by adding remove_action('wp_head', 'wp_generator'); to your theme's functions.php (a child theme, ideally).
  • Disable directory browsing: add Options -All +FollowSymLinks +MultiViews or simply Options -Indexes to your root .htaccess.
  • Block access to sensitive files like readme.html, license.txt, and wp-config-sample.php, which advertise your version.
  • Protect the .htaccess file itself so it can't be read over HTTP.

None of this stops a determined attacker, but it removes you from the low-hanging-fruit pile that automated scanners target first.

Step 6: Add HTTP security headers at the server

Security headers instruct the browser to enforce protections. Setting them at the server layer is faster than any plugin and applies to every response.

Add these to your Apache .htaccess (inside <IfModule mod_headers.c>) or the Nginx add_header equivalents:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains — forces HTTPS.
  • X-Content-Type-Options: nosniff — stops MIME sniffing.
  • X-Frame-Options: SAMEORIGIN — blocks clickjacking via iframes.
  • Referrer-Policy: strict-origin-when-cross-origin
  • Content-Security-Policy

    Cover image: Africa Endeavor 2010 by US Army Africa, licensed under BY 2.0 via Openverse.

Recent Posts

View all →

Most Popular Software

View all →

Browse by Platform

View all →