Back to tutorials
Tutorial

WordPress hardening tutorial (2026): Lock down wp-admin, file permissions, and updates on a hosting VPS

WordPress hardening tutorial for VPS: secure wp-admin, disable risky edits, tighten permissions, and add WAF rules safely.

By Anurag Singh
Updated on Oct 14, 2026
Category: Tutorial
Share article
WordPress hardening tutorial (2026): Lock down wp-admin, file permissions, and updates on a hosting VPS

Most WordPress compromises don’t start with a zero-day. They start with an exposed admin surface (wp-admin), sloppy file permissions, and “we’ll update later” habits.

This WordPress hardening tutorial focuses on changes you can apply on a VPS or dedicated server. The goal is tighter security without breaking logins, cron, or a WooCommerce checkout.

The steps below assume Nginx or Apache already serves your site. You’ll adjust web server config, filesystem ownership/permissions, and a few WordPress settings.

Take a snapshot first if your provider supports it. If you host multiple customer sites, apply these changes per vhost and per WordPress install.

Prerequisites and a quick safety checklist

Confirm the basics before you start. This prevents lockouts and makes rollback painless.

  • SSH access to your VPS (non-root sudo is fine).
  • Know your web server: Nginx, Apache, or Apache behind Nginx.
  • Find the document root (examples below use /var/www/example.com/public).
  • Backup your site files and database (or at least take a VPS snapshot).

If you run WordPress for clients on a hosting VPS, isolate sites. Use per-user ownership, resource limits, and scheduled maintenance windows.

A HostMyCode VPS gives you the control to enforce filesystem and web server policies. Those policies are hard to apply consistently on shared hosting.

Step 1: Confirm your WordPress paths and the PHP user

First, confirm where the install lives. Then confirm which user PHP runs as.

That detail determines what “safe permissions” mean on your server.

# Find WordPress from a vhost root (adjust path)
cd /var/www/example.com/public
ls -la

# You should see wp-config.php, wp-content/, wp-admin/

# Check your PHP-FPM pool user (common on Nginx + PHP-FPM)
# Ubuntu/Debian example:
sudo grep -R "^user\s*=\s*" /etc/php/*/fpm/pool.d/*.conf | head

# If Apache with mod_php, the effective user is typically www-data.
ps aux | egrep "apache2|httpd" | head

Typical setups look like this:

  • Nginx + PHP-FPM: the pool user is often www-data or a per-site user like example.
  • Apache + PHP-FPM: similar to Nginx; PHP runs as the pool user.
  • Apache + mod_php: PHP runs as the Apache user (often www-data), which gives you fewer hardening options.

Step 2: Lock down wp-admin without breaking real users

Attackers focus on wp-admin for brute-force logins, session abuse, and plugin upload attempts. Reduce who can reach it.

The key idea: add friction before WordPress even loads.

Option A (recommended): allow only your office/VPN IPs to reach wp-admin

If you have a stable IP (or you always come from a VPN egress IP), IP allowlisting is the cleanest win.

Nginx (inside the server block for the site):

location ^~ /wp-admin/ {
  allow 203.0.113.10;   # your admin IP
  allow 198.51.100.0/24; # office range (example)
  deny all;

  index index.php;
  try_files $uri $uri/ /wp-admin/index.php?$args;
}

# admin-ajax is used by themes/plugins; don't block it
location = /wp-admin/admin-ajax.php {
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # adjust
}

Apache (in the vhost or an appropriate .conf; avoid .htaccess if you can):

<Location "/wp-admin/">
  Require ip 203.0.113.10
  Require ip 198.51.100.0/24
</Location>

# Keep admin-ajax accessible
<Location "/wp-admin/admin-ajax.php">
  Require all granted
</Location>

Reload your web server after changes:

sudo nginx -t && sudo systemctl reload nginx
# or
sudo apachectl configtest && sudo systemctl reload apache2

Option B: add HTTP Basic Auth to /wp-admin (works even if your IP changes)

Basic Auth adds a second checkpoint before WordPress runs. It doesn’t replace strong WordPress passwords or 2FA.

It does cut down automated noise quickly.

# Create a password file (Ubuntu/Debian package: apache2-utils)
sudo apt-get update
sudo apt-get install -y apache2-utils

sudo htpasswd -c /etc/nginx/.htpasswd-wpadmin adminuser

Nginx:

location ^~ /wp-admin/ {
  auth_basic "Restricted";
  auth_basic_user_file /etc/nginx/.htpasswd-wpadmin;

  index index.php;
  try_files $uri $uri/ /wp-admin/index.php?$args;
}

Apache:

<Location "/wp-admin/">
  AuthType Basic
  AuthName "Restricted"
  AuthUserFile /etc/apache2/.htpasswd-wpadmin
  Require valid-user
</Location>

One caveat: some mobile apps and external services don’t play nicely with Basic Auth. If you depend on those, stick with IP allowlisting.

Or protect only /wp-login.php.

If you also want edge-level protection against bots, pair this with sensible rate limits. HostMyCode has a practical walkthrough here: Nginx rate limiting to stop bots without breaking real users.

Step 3: Protect wp-login.php and stop username enumeration

If you can’t restrict wp-admin by IP, you can still reduce abuse. Focus on login friction and information leaks.

Rate-limit wp-login (Nginx)

Add this to /etc/nginx/nginx.conf inside the http {} block:

limit_req_zone $binary_remote_addr zone=wp_login:10m rate=10r/m;

Then add the limit inside your site’s server block:

location = /wp-login.php {
  limit_req zone=wp_login burst=20 nodelay;
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # adjust
}

Test and reload:

sudo nginx -t && sudo systemctl reload nginx

Block author scans that leak usernames

Attackers still use /?author=1 scans to discover usernames. It’s not the only enumeration technique, but it’s common.

It’s also easy to shut down.

Nginx:

if ($args ~* "author=\d+") { return 403; }

Apache (in vhost):

RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)author=\d+(&|$) [NC]
RewriteRule ^ - [F]

Don’t stop at WordPress rules. You also need sane TLS, headers, and safe deploy defaults.

If you haven’t reviewed those on your VPS, use this web server hardening tutorial for Nginx + PHP-FPM. Apply the pieces that match your stack.

Step 4: Set correct ownership and file permissions (the safe baseline)

Permissions decide whether a minor plugin flaw becomes a full takeover. Aim for a boring baseline.

WordPress should read what it needs and write only where it must.

You also want updates to work without constant permission fixes.

Pick an ownership model

  • Single-site VPS: create a dedicated user (recommended) and run PHP-FPM as that user.
  • Multi-site VPS: give each site its own user and its own PHP-FPM pool.
  • Shared hosting: you usually inherit the provider’s model; prioritize wp-config.php protection and disabling file edits.

Example: your deploy user is example, and PHP-FPM runs as example.

# Set ownership to the site user
sudo chown -R example:example /var/www/example.com

# Directories: 755; files: 644
sudo find /var/www/example.com/public -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/public -type f -exec chmod 644 {} \;

Harden wp-config.php and disable write access where possible

# Tighten wp-config.php
sudo chmod 600 /var/www/example.com/public/wp-config.php

If PHP runs as the same user that owns the files, a compromised PHP process can still write to the codebase. For higher assurance, separate “deploy” from “runtime.”

A stronger pattern looks like this:

  • WordPress files owned by a deploy user.
  • PHP-FPM runs as a separate runtime user with read-only access.
  • wp-content/uploads writable by runtime user; everything else read-only.

That redesign takes planning, but it pays off on high-value sites and reseller servers.

If you’re running cPanel with CloudLinux, you can also cut blast radius with per-account isolation and limits; see cPanel resource limits with CloudLinux LVE + PHP-FPM.

Step 5: Disable risky WordPress behaviors (file editors, XML-RPC, untrusted uploads)

These are small switches. Each one removes a common escalation path.

Disable the theme/plugin editor

Edit wp-config.php and add:

define('DISALLOW_FILE_EDIT', true);

This blocks the built-in editor. Attackers like it because it makes dropping PHP trivial.

Control plugin/theme installs in production

If your workflow is “deploy via Git/CI or SFTP,” you can block dashboard installs and updates:

define('DISALLOW_FILE_MODS', true);

Only do this if you’re disciplined about updates via your deploy process. Otherwise you’ll accumulate stale plugins.

Disable XML-RPC if you don’t need it

If you don’t use Jetpack, the WordPress mobile app, or external publishing, disable XML-RPC at the web server.

Nginx:

location = /xmlrpc.php { return 403; }

Apache:

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

Step 6: Add a lightweight WAF layer (ModSecurity or equivalent rules)

You don’t need an enterprise WAF to get value. A basic ruleset can catch obvious payloads.

It also reduces drive-by exploit traffic and keeps logs usable.

If you run cPanel/WHM, ModSecurity with OWASP CRS is a solid baseline. You can tune it per domain. Follow HostMyCode’s guide: enable OWASP CRS in WHM without breaking WordPress.

On a plain Nginx VPS without ModSecurity, focus on:

  • Rate limits for wp-login.php and expensive endpoints.
  • Strict upload handling (next step).
  • Blocking PHP execution in upload directories.

Step 7: Prevent PHP execution in uploads (a high-impact hardening win)

Many compromises end the same way. An attacker uploads a PHP file into wp-content/uploads and runs it.

Block execution in that directory to cut off a common endgame.

Nginx: deny PHP under uploads

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

Apache: deny PHP under uploads

If you allow per-directory config, create:

# /var/www/example.com/public/wp-content/uploads/.htaccess
<FilesMatch "\.php$">
  Require all denied
</FilesMatch>

Reload, then verify the rule behaves the way you expect:

curl -I https://example.com/wp-content/uploads/test.php
# Expect 403 (or 404 if file not present)

Step 8: Secure updates without turning your site into a maintenance project

Updates should happen quickly, but not at random. Pick a cadence, stick to it, and watch for failures.

Recommended update policy for 2026

  • Core minor releases: auto-apply (security and maintenance updates).
  • Core major releases: apply after staging test or a low-traffic window.
  • Plugins/themes: auto-update only for trusted vendors; otherwise schedule weekly updates with a quick functional check.

Enable core security updates (wp-config.php)

define('WP_AUTO_UPDATE_CORE', 'minor');

Use WP-CLI for controlled updates (and logs)

If WP-CLI isn’t installed, you can add it on Ubuntu/Debian like this:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp

Then run updates from the site root:

cd /var/www/example.com/public
sudo -u example wp core check-update
sudo -u example wp plugin update --all
sudo -u example wp theme update --all

For reliability, schedule updates with a real scheduler and keep logs. If your site still depends on pseudo-cron, fix that first.

Use this WP-Cron replacement tutorial to reduce missed tasks and avoid traffic-time spikes.

Step 9: Tighten SSL and admin cookies (quick checks you can do now)

If you already use HTTPS, make sure WordPress never serves admin sessions over HTTP. This matters for edge cases and misrouted requests.

Add to wp-config.php:

define('FORCE_SSL_ADMIN', true);

Then run two quick checks:

  • Visit https://example.com/wp-admin/ and confirm it never downgrades.
  • Check for mixed content warnings on the homepage and login page.

If you need a clean TLS setup and renewal automation on a VPS, follow: SSL certificate setup and renewal troubleshooting.

Step 10: Add monitoring so you notice compromises early

Hardening works best with visibility. Add a little monitoring now.

That way, “something feels off” becomes an alert you can act on.

What to alert on

  • Unexpected spikes in 404/403 on /wp-login.php and /xmlrpc.php
  • New PHP files created under wp-content/uploads (should be zero)
  • Sudden CPU/RAM jumps during low traffic
  • Outbound connections from PHP to unusual hosts (often indicates a web shell)

For VPS-friendly monitoring with dashboards and alerts, use the HostMyCode guide: Netdata + uptime checks for VPS/dedicated servers.

Step 11: A repeatable hardening checklist (copy/paste for each site)

  • Restrict /wp-admin/ by IP or Basic Auth; keep admin-ajax.php accessible.
  • Rate-limit /wp-login.php and block obvious username enumeration.
  • Set directories to 755 and files to 644; protect wp-config.php (600).
  • Disable dashboard file editor (DISALLOW_FILE_EDIT).
  • Block XML-RPC if unused.
  • Deny PHP execution in wp-content/uploads.
  • Enable minor core auto-updates; do weekly plugin/theme updates via WP-CLI.
  • Confirm SSL admin, fix mixed content, automate renewals.
  • Add alerts for login storms, file changes, and resource spikes.

Where HostMyCode fits (and when to choose managed vs unmanaged)

If you want full control over Nginx/Apache rules, PHP users, and per-site isolation, start with a HostMyCode VPS.

It’s the right foundation because you can enforce ownership, permissions, and rate-limit/WAF policies.

If you don’t want to spend time tuning PHP-FPM pools, firewall rules, and update windows, managed VPS hosting is a better fit.

You keep the performance and isolation benefits. Routine patching and baseline security are handled alongside you.

A VPS gives you security controls that shared hosting usually can’t: stricter permissions, tighter web rules, and updates that run on your schedule. If you’re ready to harden properly, start with a HostMyCode VPS, or choose managed VPS hosting if you want help keeping the stack patched and stable.

FAQ

Will blocking XML-RPC break WooCommerce?

Usually not. WooCommerce doesn’t require XML-RPC. Jetpack, the WordPress mobile app, and some third-party integrations might.

If you’re not sure, block it temporarily and watch your logs for errors.

Should I disable plugin installs with DISALLOW_FILE_MODS?

Only if you have a reliable update path (WP-CLI under a deploy user, Git-based deploys, or a control panel workflow). Otherwise you’ll delay updates and raise your risk.

What permissions should wp-content have?

Keep most of wp-content read-only, and allow write access to wp-content/uploads. If a plugin needs a writable cache directory, grant access to that directory only—not the whole tree.

Do I need a WAF for WordPress on a VPS?

Not strictly, but it helps. A tuned ruleset can block common exploit probes and reduce brute-force traffic.

If you use cPanel/WHM, ModSecurity with OWASP CRS is a practical baseline.

What’s the fastest way to reduce brute-force login attempts?

Restricting wp-admin by IP is the biggest win when it’s feasible. If it isn’t, rate-limit wp-login.php, use strong admin passwords, and enforce 2FA with a reputable plugin.