WP Manifestindependent plugin directory
manifest / security / login-guard

Login Guard

A WordPress plugin that blocks brute-force and credential-stuffing attacks.

by HenrysCat · github.com/henryscat/login-guard · website

0stars
0forks

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.zip

Readme

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.txt in 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=N requests to the home page so user IDs can't be mapped to usernames.
  • Restrict REST user endpoints — require login for /wp-json/wp/v2/users requests.
  • 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.co or ip-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

  1. Enable the feature under Login Guard → Settings → Two-Factor Authentication.
  2. 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.
  3. 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

  1. Download login-guard.zip.
  2. In WordPress admin, go to Plugins → Add New → Upload Plugin, choose the zip, and click Install Now.
  3. 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

TagPublishedAssetDownloads
v1.4.0 Sep 9, 2026 login-guard.zip 0