WordPress Security Hardening Checklist (30+ Steps)
A no-nonsense checklist for locking down your WordPress site. Server config, file permissions, login security, database hardening, and monitoring.
Why Another Security Checklist?
Because most of them are either too vague (“keep your site updated”) or too deep in the weeds for anyone who isn’t a sysadmin. This one sits in the middle. It’s the actual list we work through when hardening a client’s WordPress site — whether it’s a fresh install or something that’s been running for years without anyone looking under the hood.
Every item here is something you can do yourself if you’re comfortable editing config files and using SSH. If you’re not, that’s fine too — just hand this list to your developer or get in touch with us.
Let’s get into it.
Section 1: Server-Level Hardening
This is the foundation. You can install every security plugin in the world, but if your server config is sloppy, none of it matters.
1. Keep PHP Updated
Running PHP 7.4 in 2026 is like leaving your front door wide open. Old PHP versions stop getting security patches, and attackers know exactly which vulnerabilities to exploit.
Check your version:
php -v
You should be on PHP 8.2 or higher. If your host doesn’t support it, switch hosts. Seriously.
2. Harden php.ini
A few lines in your php.ini make a big difference. These disable functions that attackers love to abuse:
disable_functions = exec, passthru, shell_exec, system, proc_open, popen, curl_multi_exec, parse_ini_file, show_source
expose_php = Off
display_errors = Off
log_errors = On
allow_url_fopen = Off
allow_url_include = Off
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
The expose_php = Off line stops PHP from broadcasting its version in HTTP headers. No reason to give attackers free recon.
Note: allow_url_fopen = Off can break some plugins and theme features that fetch remote data. Test before you commit to it, and if something breaks, consider leaving it on but adding compensating controls elsewhere.
3. Set Correct File Permissions
This comes up constantly. Wrong file permissions are one of the most common ways attackers escalate access after getting a foothold.
# Directories should be 755
find /path/to/wordpress/ -type d -exec chmod 755 {} \;
# Files should be 644
find /path/to/wordpress/ -type f -exec chmod 644 {} \;
# wp-config.php gets locked down further
chmod 600 wp-config.php
# .htaccess also needs protection
chmod 644 .htaccess
Some hosting environments need 644 for wp-config.php instead of 600 — depends on whether PHP runs as your user or as www-data. Test it after changing. If the site breaks, bump it to 644.
4. Disable Directory Listing
If someone navigates to yoursite.com/wp-content/uploads/, they shouldn’t see a file browser. Add this to your root .htaccess:
Options -Indexes
One line. Takes two seconds. Prevents directory browsing across your entire site.
5. Disable XML-RPC
XML-RPC was useful back when people blogged from desktop apps. Now it’s mostly used for brute force amplification attacks and DDoS. Unless you’re specifically using it (Jetpack needs it, for example), shut it down.
Add to .htaccess:
<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>
Or via Nginx:
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
6. Block PHP Execution in Uploads
Your wp-content/uploads/ directory should never execute PHP files. If an attacker manages to upload a PHP shell disguised as an image, this stops it from running.
Create a .htaccess file inside wp-content/uploads/:
<Files "*.php">
Order Deny,Allow
Deny from all
</Files>
Do the same inside wp-content/uploads/ and wp-includes/ if you want to be thorough.
7. Add Security Headers
These go in your .htaccess or Nginx config and tell browsers how to handle your content:
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
The Strict-Transport-Security header forces HTTPS. Only add it after you’ve confirmed SSL is working properly — if you mess this up, browsers will refuse to load your site over HTTP for a year.
8. Hide the WordPress Version
WordPress outputs its version number in the HTML head by default. Remove it:
// Add to functions.php or a custom plugin
remove_action('wp_head', 'wp_generator');
Also remove the version from RSS feeds:
add_filter('the_generator', '__return_empty_string');
And strip version query strings from scripts and stylesheets:
function remove_version_query_strings($src) {
if (strpos($src, 'ver=')) {
$src = remove_query_arg('ver', $src);
}
return $src;
}
add_filter('script_loader_src', 'remove_version_query_strings', 15, 1);
add_filter('style_loader_src', 'remove_version_query_strings', 15, 1);
Section 2: WordPress Configuration
The wp-config.php file is the single most important file in your WordPress install. Treat it accordingly.
9. Move wp-config.php Up One Directory
WordPress will automatically look for wp-config.php one directory above your web root. Moving it there means it can’t be accessed via a browser, even if your server misconfigures PHP processing.
mv /var/www/html/wp-config.php /var/www/wp-config.php
No code changes needed. WordPress handles this natively.
10. Regenerate Security Salts
Salts are used to hash passwords and authentication tokens. If your site has been around for a while (or you inherited it from someone else), regenerate them.
Grab fresh salts from the official generator:
https://api.wordpress.org/secret-key/1.1/salt/
Paste them into wp-config.php, replacing the existing salt block. This will force all logged-in users to re-authenticate, which is a good thing if there’s any chance credentials have been compromised.
11. Disable File Editing in the Dashboard
WordPress ships with a built-in code editor that lets admins modify plugin and theme files from the dashboard. If an attacker gets admin access, this is the first thing they use. Kill it:
define('DISALLOW_FILE_EDIT', true);
Add that to wp-config.php. You can still edit files via FTP or SSH — you just can’t do it from the WordPress admin.
12. Disable Plugin and Theme Installation from Dashboard
Take it a step further. If you don’t need to install plugins or themes from the dashboard, block that too:
define('DISALLOW_FILE_MODS', true);
This disables the file editor, plugin/theme installer, and even automatic updates. Use this on production sites where changes should only happen through a deployment pipeline.
13. Turn Off Debug Mode
This should go without saying, but we see live sites with debug mode on more often than you’d think:
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
If WP_DEBUG is set to true and WP_DEBUG_DISPLAY is on, error messages get printed to the screen — sometimes revealing file paths, database details, and other information that helps attackers map your installation.
14. Force SSL on Admin and Logins
define('FORCE_SSL_ADMIN', true);
define('FORCE_SSL_LOGIN', true);
This ensures the login page and admin dashboard always load over HTTPS, even if someone types in an HTTP URL.
15. Limit Post Revisions
Not strictly a security measure, but it keeps your database lean and reduces the surface area for stored XSS payloads hiding in old revisions:
define('WP_POST_REVISIONS', 5);
16. Disable WP-Cron and Use a Real Cron Job
WordPress’s built-in cron system (wp-cron.php) fires on every page load. This is a performance issue and a minor security concern — it can be triggered by external requests.
Disable it in wp-config.php:
define('DISABLE_WP_CRON', true);
Then set up a real system cron:
# Run WP-Cron every 15 minutes via server cron
*/15 * * * * cd /var/www/html && php wp-cron.php > /dev/null 2>&1
Or use WP-CLI:
*/15 * * * * cd /var/www/html && wp cron event run --due-now > /dev/null 2>&1
Section 3: Login Security
The WordPress login page is the most attacked endpoint on any WordPress site. Bots hammer it constantly. Here’s how to shut that down.
17. Limit Login Attempts
By default, WordPress allows unlimited login attempts. That means brute force attacks can try thousands of password combinations without any throttling.
Use a plugin like Limit Login Attempts Reloaded or WP Limit Login Attempts, or implement it at the server level with Fail2Ban:
# /etc/fail2ban/filter.d/wordpress.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
^<HOST> .* "POST /xmlrpc.php
ignoreregex =
# /etc/fail2ban/jail.local
[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 3600
findtime = 600
This bans any IP that hits the login page more than 5 times in 10 minutes for one hour.
18. Enable Two-Factor Authentication
Passwords alone aren’t enough. Enable 2FA for every admin and editor account. No exceptions.
Good plugin options: WP 2FA or Two Factor Authentication (the one by Plugin Contributors). Both support TOTP apps like Google Authenticator and Authy.
If you’re managing multiple sites, consider a solution that works across all of them rather than configuring 2FA per-site.
19. Change the Login URL
Moving /wp-login.php to a custom URL won’t stop a determined attacker, but it eliminates 99% of automated bot traffic hitting your login page.
WPS Hide Login is a lightweight plugin that does this. Set it to something like /client-login or whatever makes sense for your setup. Avoid anything obvious like /admin-login.
20. Enforce Strong Passwords
WordPress 4.3+ generates strong passwords by default, but it still lets users override them with weak ones. Lock that down:
// Force strong passwords for all users
function enforce_strong_passwords($errors, $update, $user) {
if ($update && !empty($_POST['pass1'])) {
$password = $_POST['pass1'];
if (strlen($password) < 12) {
$errors->add('weak_password',
'<strong>Error</strong>: Password must be at least 12 characters.');
}
}
return $errors;
}
add_action('user_profile_update_errors', 'enforce_strong_passwords', 10, 3);
Or use a policy plugin that enforces complexity requirements site-wide.
21. Disable the “admin” Username
If you still have a user account named admin, create a new administrator account with a different username, transfer all content to it, and delete the admin account. Bots target this username by default — why make it easy?
22. Restrict Admin Access by IP (If Practical)
If your team works from static IPs, lock down /wp-admin/ access:
# .htaccess inside /wp-admin/
<Files "*.php">
Order Deny,Allow
Deny from all
Allow from 203.0.113.10
Allow from 198.51.100.20
</Files>
This doesn’t work well for teams with dynamic IPs or people who work from coffee shops. Use it where it makes sense.
23. Protect wp-login.php with HTTP Authentication
Add an extra layer of authentication before anyone even sees the WordPress login form:
# In root .htaccess
<Files wp-login.php>
AuthType Basic
AuthName "Restricted Access"
AuthUserFile /path/to/.htpasswd
Require valid-user
</Files>
Generate the .htpasswd file:
htpasswd -c /path/to/.htpasswd yourusername
This adds a browser-level password prompt in front of the WordPress login. Bots can’t get past it.
Section 4: Database Hardening
Your database holds everything — posts, user credentials, settings. Locking it down is straightforward.
24. Change the Default Table Prefix
WordPress defaults to wp_ as the table prefix. Every SQL injection script on the planet targets wp_users and wp_options. Changing the prefix adds a layer of obscurity.
For new installs, set this in wp-config.php before running the installer:
$table_prefix = 'zcd_29_';
Pick something unique. Letters, numbers, an underscore at the end.
For existing sites, this is trickier — you need to rename every table in the database and update references in wp_options and wp_usermeta. Use a plugin like Brozzme DB Prefix or do it manually:
-- Example: rename core tables (do this for ALL tables)
RENAME TABLE wp_posts TO zcd_29_posts;
RENAME TABLE wp_options TO zcd_29_options;
RENAME TABLE wp_users TO zcd_29_users;
-- ... repeat for every table
-- Update references in options table
UPDATE zcd_29_options SET option_name = REPLACE(option_name, 'wp_', 'zcd_29_')
WHERE option_name LIKE 'wp_%';
-- Update references in usermeta table
UPDATE zcd_29_usermeta SET meta_key = REPLACE(meta_key, 'wp_', 'zcd_29_')
WHERE meta_key LIKE 'wp_%';
Then update $table_prefix in wp-config.php to match. Back up your database before doing any of this.
25. Create a Dedicated Database User
Don’t use the root MySQL/MariaDB user for WordPress. Create a dedicated user with only the permissions WordPress needs:
CREATE USER 'wp_zencore'@'localhost' IDENTIFIED BY 'a-strong-random-password-here';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
ON wordpress_db.* TO 'wp_zencore'@'localhost';
FLUSH PRIVILEGES;
WordPress needs CREATE, ALTER, INDEX, and DROP for plugin installations and updates. If you’ve disabled file mods (step 12), you can potentially remove those permissions too — but test thoroughly because some plugins run database migrations during normal operation.
26. Remove the Default “admin” Account (and Rogue Accounts)
Log into your database and check for accounts that shouldn’t be there:
SELECT ID, user_login, user_email, user_registered FROM wp_users;
Look for:
- The default
adminusername - Accounts you don’t recognize
- Accounts with admin-level privileges that should be editors or subscribers
Check user roles:
SELECT u.user_login, um.meta_value
FROM wp_users u
JOIN wp_usermeta um ON u.ID = um.user_id
WHERE um.meta_key = 'wp_capabilities'
AND um.meta_value LIKE '%administrator%';
27. Clean Up Transients and Orphaned Data
Over time, your database accumulates garbage — expired transients, orphaned post meta, old revisions. Clean it up:
-- Delete expired transients
DELETE FROM wp_options WHERE option_name LIKE '%_transient_%'
AND option_name LIKE '%_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP();
-- Delete orphaned postmeta
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;
Or use WP-CLI:
wp transient delete --expired
A cleaner database means fewer places for malicious data to hide, and it runs faster too.
Section 5: Plugin and Theme Security
Plugins and themes are the number one attack vector for WordPress sites. Not WordPress core — the ecosystem around it.
28. Update Everything, Regularly
Set a schedule. Weekly at minimum. WordPress core, all plugins, all themes. Every update you skip is a known vulnerability sitting on your production site.
WP-CLI makes this easy to automate:
# Update everything
wp core update
wp plugin update --all
wp theme update --all
If you’re worried about updates breaking things (reasonable concern), maintain a staging environment and test there first. But don’t let “we need to test first” become an excuse to never update. We’ve seen sites running plugins with known vulnerabilities for months because updates kept getting deferred.
29. Delete Inactive Plugins and Themes
An inactive plugin is still on your server. Its files can still be accessed. If it has a vulnerability, it can still be exploited — even if it’s deactivated.
# List inactive plugins
wp plugin list --status=inactive
# Delete them
wp plugin delete plugin-name
Same goes for themes. Keep your active theme and a default WordPress theme as a fallback. Delete everything else.
30. Audit Plugin Permissions and Reputation
Before installing any plugin, check:
- Last updated: If it hasn’t been updated in over a year, be cautious. If it’s been two years, walk away.
- Active installations: Low install counts aren’t inherently bad, but popular plugins get more security scrutiny.
- Support forum: Are issues being responded to? Are there unresolved security reports?
- Developer history: Google the developer name. Have they had security incidents before?
Run a plugin security audit on your existing plugins. Tools like Patchstack or WPScan can flag known vulnerabilities in your installed plugins and themes.
# If you have WPScan installed
wpscan --url https://yoursite.com --enumerate vp,vt
31. Use Child Themes
If you’ve customized your theme directly, those changes get wiped every time the theme updates. This leads to people not updating their theme, which leads to security vulnerabilities sitting unpatched.
Use a child theme. Put your customizations there. Update the parent theme without worrying about losing your changes.
32. Remove Theme and Plugin Editors
We already covered DISALLOW_FILE_EDIT in step 11, but double-check it’s actually working. Navigate to Appearance > Theme Editor in the dashboard. You should see a message that file editing is disabled. If you see a code editor, something’s wrong.
33. Don’t Use Nulled (Pirated) Themes or Plugins
This should be obvious, but we still see it. Nulled themes and plugins are almost always injected with backdoors. That “free” premium theme will cost you far more when your site is compromised.
There’s no shortcut here. Pay for your software or use free alternatives from the official WordPress repository.
Section 6: Monitoring and Backups
Hardening is about prevention. Monitoring is about detection. You need both.
34. Set Up File Integrity Monitoring
File integrity monitoring watches your WordPress files and alerts you when something changes. If wp-login.php suddenly gets modified, you want to know about it immediately — not three months later.
Wordfence includes file integrity checking. So does Sucuri Security. For a server-level solution:
# Create a baseline hash of all WordPress files
find /var/www/html -type f -exec md5sum {} \; > /root/wp-baseline.txt
# Compare against the baseline (run via cron daily)
find /var/www/html -type f -exec md5sum {} \; > /tmp/wp-current.txt
diff /root/wp-baseline.txt /tmp/wp-current.txt > /tmp/wp-changes.txt
# If changes are found, email yourself
if [ -s /tmp/wp-changes.txt ]; then
mail -s "WordPress File Changes Detected" you@email.com < /tmp/wp-changes.txt
fi
This is a bare-bones approach. Dedicated monitoring tools give you better reporting and fewer false positives, but this works in a pinch.
35. Monitor Uptime
If your site goes down, you want to know before your clients do. Use an uptime monitoring service — UptimeRobot (free tier is fine for most sites), Pingdom, or Better Uptime.
Set up monitors for:
- Your homepage
- Your login page
- Your admin dashboard
- Any critical landing pages
Configure alerts to hit your phone, not just email. Email notifications for site-down events are useless if you don’t see them for hours.
36. Implement a Backup Strategy
Backups aren’t optional. They’re the only thing standing between you and total data loss.
Follow the 3-2-1 rule:
- 3 copies of your data
- 2 different storage types (local + cloud)
- 1 copy offsite
Automate it:
# Full site backup via WP-CLI and mysqldump
#!/bin/bash
SITE_DIR="/var/www/html"
BACKUP_DIR="/backups/wordpress"
DATE=$(date +%Y-%m-%d)
# Database backup
wp db export "$BACKUP_DIR/db-$DATE.sql" --path=$SITE_DIR
# File backup
tar -czf "$BACKUP_DIR/files-$DATE.tar.gz" $SITE_DIR
# Sync to remote storage
rclone sync $BACKUP_DIR remote:wordpress-backups
# Clean up backups older than 30 days
find $BACKUP_DIR -mtime +30 -delete
Run this daily via cron. For high-traffic sites with lots of content changes, consider running database backups every few hours.
Test your backups. A backup that can’t be restored isn’t a backup. At least once a quarter, spin up a staging server and restore from backup to make sure everything works.
37. Set Up a Web Application Firewall (WAF)
A WAF sits in front of your site and filters malicious traffic before it reaches WordPress. Options:
- Cloudflare (free tier has basic WAF rules, Pro gets you the full ruleset)
- Sucuri Firewall (WordPress-specific, good at blocking known attack patterns)
- ModSecurity with the OWASP Core Rule Set (self-hosted, free, more work to configure)
At minimum, get Cloudflare’s free plan running. It stops a surprising amount of garbage traffic.
38. Log and Review Login Activity
Install a logging plugin that records login attempts — both successful and failed. WP Activity Log does this well. Review logs weekly and look for:
- Failed logins from unusual IP addresses or geographic regions
- Successful logins at odd hours
- Admin-level actions you didn’t perform
- User account creation you didn’t initiate
Set up email alerts for administrator logins so you know in real time when someone accesses the dashboard.
39. Set Up Malware Scanning
Regular malware scans catch problems early. Options:
- Wordfence runs scans from inside WordPress
- Sucuri SiteCheck is a free remote scanner (doesn’t catch everything since it only scans what’s publicly visible, but it’s a good first layer)
- ClamAV for server-level scanning
# Scan WordPress directory with ClamAV
clamscan -r /var/www/html --infected --log=/var/log/clamav/wp-scan.log
Schedule scans daily. Review the logs.
Bonus: Quick Wins You Can Do Right Now
If you’ve read through this list and feel overwhelmed, start with these five. They take less than 30 minutes combined:
- Update everything — core, plugins, themes. Right now.
- Delete inactive plugins and themes. If you’re not using it, remove it.
- Check your admin users. Remove any you don’t recognize.
- Add
DISALLOW_FILE_EDITtowp-config.php. - Enable 2FA for all admin accounts.
Those five steps alone eliminate a huge percentage of common WordPress attacks.
The Bottom Line
Security isn’t a one-time task — it’s maintenance. You change your oil regularly, you run security updates regularly. Build these checks into your monthly workflow, or hand it off to someone who will.
The biggest mistake we see is people treating security as something they’ll “get to eventually.” Eventually usually means after a breach, and cleaning up a hacked site costs 10x more in time and money than preventing it.
If you want us to run through this checklist on your site, we do full WordPress security audits and hardening as part of our maintenance plans. We’ll assess your current setup, fix what needs fixing, and set up ongoing monitoring so nothing slips through.
Get in touch — we’ll take a look and give you an honest assessment of where you stand.
Need help with your WordPress site?
We can help with the stuff covered in this post. Message us and we'll figure it out.