On a small WordPress site, security is not a project: it is a baseline you lay down once, properly, and then stop thinking about. Three components, half a day of setup, and nearly all automated attacks stop having any effect. Here is that baseline, in deployment order.
Why a minimal baseline rather than a fortress
Most of what hits a small WordPress install is automated noise: bots requesting wp-login.php, scans for known vulnerabilities, attempts to enumerate users through the REST API. None of these attacks targets a specific site — they sweep the entire web. The baseline below cuts that volume. The rest — a targeted attack, a zero-day on a specific extension — cannot be handled with three lines of configuration, and practically never concerns a brochure site.
The best hardening is not the one that blocks everything: it is the one still in place and up to date six months later.
Component 1 — Wordfence, the application layer
Wordfence protects more than five million sites and its free version covers the essentials: application firewall, malware scanner comparing core files against the official repository, rate limiting, brute-force protection and two-factor authentication.
Minimal settings after installation:
- Switch the firewall to Extended Protection, which runs before PHP rather than as a plain plugin.
- Enable two-factor authentication on every administrator account.
- Enable user-enumeration blocking and hide the WordPress version.
- Reduce email alerts to critical ones only: a constant notification stream eventually goes unread.
Component 2 — Fail2ban, the network layer
Wordfence acts at the PHP level. Fail2ban acts earlier, at server level: it reads the Nginx logs and bans the IP address at the system firewall as soon as an attack pattern repeats. The attacker then stops consuming PHP resources at all.
# /etc/fail2ban/filter.d/wp-login.conf
[Definition]
failregex = ^<HOST> -.*"POST /wp-login.php
^<HOST> -.*"POST /xmlrpc.php
ignoreregex =# /etc/fail2ban/jail.d/wordpress.conf
[wp-login]
enabled = true
filter = wp-login
logpath = /home/*/logs/nginx/access.log
port = http,https
maxretry = 3
findtime = 3600
bantime = 3600CF-Connecting-IP through the RealIP module) must be verified before the jail is armed.The filter should always be tested before activation:
fail2ban-regex /home/mysite/logs/nginx/access.log
/etc/fail2ban/filter.d/wp-login.confComponent 3 — Application passwords
Application passwords, native to WordPress, give a script, an integration or an automated routine a revocable credential distinct from the account’s main password. Each integration gets its own; revoking one affects none of the others.
Two operating rules follow: never reuse an application password across integrations, and check that no security extension disables them globally — Wordfence offers that setting, and enabling it silently cuts every REST integration on the site.
Zero-cost additions
| Measure | Effect |
|---|---|
Security headers (X-Content-Type-Options, Referrer-Policy, Permissions-Policy) | Reduced browser-side attack surface |
| Disabling file editing in the admin | Prevents code changes from a compromised account |
| Named administrator accounts, no “admin” user | Removes the default brute-force target |
| Encrypted backups stored off the production server | The only measure that actually repairs after an incident |
What to take away
Three components cover nearly all of the real risk on a small site: an application layer, a network layer and revocable credentials for integrations. Any additional measure should be judged on one question — will it still be maintained in six months?
This is the baseline I deploy on the thirty or so sites I self-host, behind CloudPanel and Cloudflare. The incident that cost me most was not an intrusion: it was the Wordfence setting that disables application passwords, which silently cut every one of my REST routines. — Simon Janvier
