Login Guard
A WordPress plugin that blocks brute-force and credential-stuffing attacks.
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/henryscat/login-guard/releases/download/v1.4.0/login-guard.zipReadme
Login Guard
A WordPress plugin that blocks brute-force and credential-stuffing attacks against
wp-login.php, wp_signon() and XML-RPC, with a configurable IP lockout policy,
full attempt logging, an admin dashboard, email alerts, trusted / permanently
banned IP lists, and optional two-factor authentication.
- Requires WordPress: 5.6+
- Tested up to: 7.1
- Requires PHP: 7.4+
- License: GPL-2.0-or-later
readme.txtin this repo is the WordPress.org plugin-directory readme. This file is the GitHub-facing version; keep both in sync when you change user-facing behaviour.
Features
- Configurable lockout policy — max failed attempts within a rolling time window, and lockout duration.
- Increasing lockouts — optionally multiply the lockout duration for repeat offenders (1st lockout = base, 2nd = 2×, 3rd = 3×, …), capped at a configurable maximum.
- Automatic permanent ban — optionally ban an IP for good after N lockouts.
- Full attempt logging — every failed and blocked attempt is recorded with IP,
username tried, source (
login/xmlrpc/2fa), user agent and timestamp. - Admin dashboard — stats, a 7-day activity chart, and the top offending IPs.
- Dashboard widget — at-a-glance summary on the main WordPress dashboard.
- Email notifications — alert the admin (or a custom address) on lockout and permanent-ban events.
- Trusted IP list — addresses or CIDR ranges that are never subject to lockouts.
- Banned IP list — always blocked, managed manually or automatically; single addresses or CIDR ranges.
- Username-enumeration prevention — generic "Incorrect username or password" error instead of revealing which field was wrong.
- Disable XML-RPC entirely.
- Block author scans — redirect front-end
?author=Nrequests to the home page so user IDs can't be mapped to usernames. - Restrict REST user endpoints — require login for
/wp-json/wp/v2/usersrequests. - Two-factor authentication (optional, off by default) — TOTP second factor with ten single-use recovery codes. Per-user opt-in, or required by role with a grace period. A wrong code counts toward the same IP lockout policy as a wrong password.
- IP geolocation (optional, off by default) — adds a Location column to
the dashboard and attempt logs showing an approximate country / city, resolved
through a third-party lookup service you choose (
ipwho.is,ipapi.coorip-api.com). Lookups run only on admin page loads, are capped per load, and are cached locally for 30 days. - Log retention — configurable, with automatic daily cleanup.
Coverage
Login Guard hooks the authenticate filter, which is used by the standard
wp-login.php form, wp_signon(), and XML-RPC (xmlrpc.php) — so all three are
protected by the same lockout policy.
Two-factor authentication applies to the interactive wp-login.php sign-in. For an
account with 2FA enabled, XML-RPC password sign-in is refused (there is no way to
prompt for a code). Neither the lockout policy nor 2FA covers Application
Passwords, which remain the supported path for programmatic / REST access.
Two-factor authentication
- Enable the feature under Login Guard → Settings → Two-Factor Authentication.
- Each user turns it on for their own account from Dashboard → Two-Factor Auth: scan the QR code (or type the setup key) in an authenticator app (Google Authenticator, Authy, 1Password, Aegis, …), confirm one code, then save the ten recovery codes. The QR code is drawn on the server as an inline SVG; the secret is never sent to a third-party image service.
- At every later sign-in, after the password is accepted, Login Guard shows a second screen asking for the current 6-digit code (or a recovery code).
Remembered devices. The code screen carries a trust this device checkbox.
When ticked, that browser skips the second factor for Remember this device (days)
(default 30; set to 0 under Settings to remove the checkbox). Each trusted device
is a hashed record in the user's meta, matched against a signed Lax/HttpOnly
cookie; the record and the cookie both carry the expiry. Disabling 2FA, re-enrolling,
or rotating the WordPress auth salt invalidates every remembered device. A user
clears their own from Dashboard → Two-Factor Auth; an administrator clears
another user's from that user's profile screen.
Enforcement. Set one or more roles under Require for roles. Users in those roles who have not set 2FA up are nagged in the admin and, once the setup grace period (default 7 days) elapses, are walked through a mandatory setup step at sign-in before they can continue. A remembered device never skips the mandatory first-time enrolment.
Brute-force integration. A wrong one-time or recovery code is recorded in the
attempt log with source 2fa and counted by the normal lockout engine, so it can
trigger a temporary lockout and (if configured) an automatic permanent ban.
Recovery / lockout. Recovery codes are single-use. An administrator can turn 2FA off for a stuck user from Users → (edit user) → Two-Factor Authentication, or via WP-CLI:
wp user meta delete <user> login_guard_2fa_enabled login_guard_2fa_secret \
login_guard_2fa_recovery login_guard_2fa_last_counter login_guard_2fa_trusted_devices
Storage & keys. The per-user TOTP secret is encrypted at rest with libsodium
secretbox, using a key derived from wp_salt('secure_auth') — or from a
LOGIN_GUARD_2FA_KEY constant if you define one in wp-config.php. Recovery codes
are stored only as salted hashes. Rotating the key or the salt makes existing
secrets unreadable; affected users just re-enrol.
Filter. login_guard_2fa_require_for_noninteractive (default false) — return
true to also refuse non-interactive logins (e.g. REST Basic Auth) for 2FA
accounts instead of letting them through.
Installation
From a release zip
- Download
login-guard.zip. - In WordPress admin, go to Plugins → Add New → Upload Plugin, choose the zip, and click Install Now.
- Activate, then configure under Login Guard → Settings.
From source
Clone into your plugins directory:
cd wp-content/plugins
git clone https://github.com/HenrysCat/login-guard.git
Then activate Login Guard from the Plugins screen.
Building a distributable zip
From the parent of the plugin directory:
zip -r login-guard.zip login-guard -x '*.git*'
The archive must contain a single top-level login-guard/ folder for
Upload Plugin to accept it.
Admin screens
| Screen | Purpose |
|---|---|
| Dashboard | Stats, 7-day activity chart, top offending IPs, recent attempts. |
| Attempt Logs | Searchable log of every failed / blocked attempt. |
| Locked IPs | Currently locked-out IPs, with Unlock / Ban Permanently actions. |
| Trusted & Banned IPs | Manage the allow / block lists (single IPs or CIDR ranges). |
| Settings | Lockout policy, notifications, additional protection, two-factor authentication, geolocation, log retention. |
| Dashboard → Two-Factor Auth | Per-user 2FA enrolment, recovery codes, disable (visible to every role once the feature is on). |
Settings reference
Lockout Policy
| Setting | Default | Notes |
|---|---|---|
| Max failed attempts | 5 | Failures within the window before lockout. |
| Time window (minutes) | 15 | Older failures don't count toward the threshold. |
| Lockout duration (minutes) | 60 | Base block length. |
| Increasing lockouts | on | Multiply duration for repeat offenders. |
| Maximum lockout duration (minutes) | 10080 | Cap for increasing lockouts (1 week). |
| Auto permanent ban | off | Ban after N lockouts. |
Additional Protection
| Setting | Default | Notes |
|---|---|---|
| Prevent username enumeration | on | Generic login error message. |
| Disable XML-RPC | off | Turns off xmlrpc.php entirely. |
| Block author scans | off | Redirects front-end ?author=N to home. Leave off if your theme links to author archives via ?author= rather than /author/slug/. |
| Restrict REST user endpoints | off | 401 for logged-out /wp/v2/users requests. |
| IP detection method | REMOTE_ADDR |
Only change if behind a trusted proxy/CDN — other headers can be spoofed. |
Two-Factor Authentication
| Setting | Default | Notes |
|---|---|---|
| Enable two-factor authentication | off | Master switch. Off = no hooks, no menu item, enforcement inert. |
| Require for roles | none | Users in a checked role must set 2FA up (see grace period). |
| Setup grace period (days) | 7 | How long a required user may sign in with just a password before setup is mandatory. 0 = immediately. |
| Remember this device (days) | 30 | Length of a "trust this device" tick. 0 removes the checkbox and ignores existing trust cookies. Max 365. |
| Trusted IPs skip the second factor | off | When on, a login from a Trusted IP does not prompt for a code. |
IP Geolocation
| Setting | Default | Notes |
|---|---|---|
| Show attacker location | off | Adds a Location column resolved via a third-party service. Sends public IPs from the log to that service. |
| Geolocation service | ipwho.is |
ipwho.is (HTTPS, no key), ipapi.co (HTTPS, no key, 1,000/day), or ip-api.com (HTTP only, non-commercial, 45/min). |
Filters: login_guard_geo_provider, login_guard_geo_ttl_days (default 30),
login_guard_geo_fail_ttl_hours (default 12),
login_guard_geo_max_lookups_per_load (default 5),
login_guard_geo_http_timeout (default 2).
FAQ
Will this lock me out of my own site?
Add your IP to the Trusted IPs list (Login Guard → Trusted & Banned IPs) so
it's never subject to lockouts. A CIDR range works there too — e.g. trust your
whole office network as 203.0.113.0/24.
I'm behind Cloudflare / a load balancer and IP blocking isn't accurate.
Set IP detection method under Settings to match your proxy (e.g.
CF-Connecting-IP for Cloudflare). Only change it if you are actually behind that
proxy — otherwise attackers can spoof the header to bypass blocking.
A user lost their phone. They sign in with one of their recovery codes. If none are left, an administrator disables 2FA for them from the user's profile screen, or via the WP-CLI command above.
Does it store data after uninstall? Only if you leave "Delete all Login Guard data on uninstall" unchecked. Enable it under Settings → Log Retention to remove all tables, IP lists, options and per-user two-factor enrolments on plugin deletion.
Data storage
By default everything is stored in your own database and nothing is sent to any third party except the lockout/ban notification email. Four custom tables are created on activation:
{prefix}_login_guard_attempts— one row per failed/blocked attempt: client IP, username tried, source (login/xmlrpc/2fa), user-agent, UTC timestamp. Pruned automatically per the log-retention setting (default 90 days).{prefix}_login_guard_lockouts— per-IP failure counts and lockout expiry.{prefix}_login_guard_ip_rules— trusted / banned IPs (single addresses or CIDR ranges), with an optional note and the id of the user who added each entry.{prefix}_login_guard_ip_geo— cached geolocation for IPs seen in the log (country, region, city, network operator). Only written when the optional IP Geolocation feature is enabled; pruned by the daily cleanup.
Two-factor authentication adds per-user rows to the standard usermeta table: an
encrypted TOTP secret, hashed recovery codes, the last used one-time-code counter,
the enable timestamp, and — for each browser the user chose to remember — a hashed
trust token with its expiry and user-agent string. The option
login_guard_2fa_enforced_since records when role enforcement began (for the
grace-period calculation). Short-lived transients hold in-progress setup and
login-challenge tokens.
All timestamps are stored in UTC and shown in site time. Settings live in the
login_guard_settings option. The only outbound mail is the lockout /
ban notification to the admin address via wp_mail().
Optional geolocation: when IP Geolocation is enabled, individual public IP
addresses from the attempt log are sent to the selected third-party service
(ipwho.is, ipapi.co, or ip-api.com) to resolve an approximate location.
These lookups happen only while an admin views a Login Guard screen, never during
a visitor's login; private/reserved ranges are never sent; results are cached
locally for 30 days. Review the chosen provider's terms and privacy policy and
your own site's privacy policy before enabling. ip-api.com's free tier is
plain HTTP and permitted for non-commercial use only.
IP addresses and attempted usernames can be personal data under some privacy laws — factor the attempt log into your site's privacy policy.
Changelog
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v1.4.0 | Sep 9, 2026 | login-guard.zip | 0 |