Sentinel Lite Security releases
wordpress lite security plugin
by Sentinel Lite · github.com/mchuluq/sentinel-lite-security
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/mchuluq/sentinel-lite-security/archive/refs/heads/master.zipAll-in-one WordPress hardening: login protection, multi-factor authentication, a request firewall, server-level hardening, and core file integrity monitoring.
Built for a small footprint. No third-party libraries, no bundled admin framework, no custom database tables, and no outbound requests except the two documented in What talks to the outside.
- Version: 1.1.0
- Requires PHP: 7.4
- License: GPLv2 or later
- Text domain:
sentinel-lite-security
Table of contents
- Installation
- Modules
- GitHub Updates
- Multi-factor authentication
- Server requirements
- Where everything is stored
- What talks to the outside
- Locked out? Recovery
- Extending the plugin
- Architecture
- Uninstalling
- Known limitations
Installation
- Copy the plugin folder to
wp-content/plugins/sentinel-lite-security/. The folder name matters: the WAF's must-use loader resolves the plugin by that exact path. - Activate it from Plugins.
- Open Sentinel Security in the admin menu.
On activation the plugin creates its log and backup directories and schedules
a daily cron event (sls_daily_event). It does not touch wp-config.php
or the site root .htaccess until you ask it to — those only change when you
click Apply Hardening, or when an IP is actually blocked.
One exception: Block PHP in Uploads ships enabled, so a rule is written to
wp-content/uploads/.htaccess on your first admin page load. Turn the module
off before then if you do not want it.
Before you turn anything on
Two settings can lock you out of your own site. Read their sections before enabling them:
- Hide Login Page — needs working pretty permalinks, and any plugin that
hardcodes
wp-login.phpmay break. - Login Limiter with
.htaccessblocking — a blocked IP loses access to the whole site, not just the login page. Add your own IP to the whitelist first.
Modules
Each module is independent and costs nothing when disabled — a disabled module never registers a single hook.
| Module | Default | What it does |
|---|---|---|
| Login Limiter & IP Blocking | on | Rate-limits failed logins, then blocks the IP |
| Disable User Accounts | on | Switch an account off without deleting it |
| Multi-Factor Authentication | per user | TOTP, email one-time codes, WebAuthn |
| XML-RPC | off | Disables the XML-RPC endpoint entirely |
| Cloudflare Turnstile | off | CAPTCHA-style challenge on the login form |
| Hide Login Page | off | Serves the login form from a custom path |
| System Hardening | rule-based | wp-config.php constants + .htaccess rules |
| File Editor Lock | on | Removes the built-in theme/plugin editor |
| Block PHP in Uploads | on | Stops uploaded shells from executing |
| Header Cleanup | on | Strips generator, RSD, WLW, shortlink, emoji |
| Security Headers | on | X-Frame-Options, nosniff, Referrer-Policy, HSTS, CSP |
| Web Application Firewall | off | SQLi/XSS/traversal pattern matching |
| File Integrity Monitoring | on | Daily diff of core files against WordPress.org |
| GitHub Updates | off | Checks a GitHub repo's Releases and offers updates on the Plugins page |
Login Limiter & IP Blocking
Two deliberately redundant layers: a PHP check on the authenticate filter
(works on any server), and an .htaccess deny rule (Apache only, rejects the
request before PHP boots).
Counting uses transients; the blocked-IP set lives in its own option because it is read and mutated on every login attempt.
| Option | Default | Notes |
|---|---|---|
| Max failed attempts | 5 | Per IP |
| Within (minutes) | 15 | Rolling window |
| Block duration (minutes) | 60 | |
Block via .htaccess |
on | Apache only; the PHP block always applies |
| Behind Cloudflare | off | Only enable if you are actually proxied |
| Other trusted proxies | empty | One IP or CIDR per line |
| IP whitelist | empty | IPs and CIDRs; never rate-limited |
The .htaccess block covers the entire site. A locked-out IP cannot reach
the front end either. That is intentional — it stops the attacker before PHP
runs — but it means you should whitelist your own address first.
About "Behind Cloudflare": client IP resolution always starts from
REMOTE_ADDR, the only value a visitor cannot forge. CF-Connecting-IP and
X-Forwarded-For are trusted only when REMOTE_ADDR itself belongs to a
known proxy range. Turning this on when you are not behind Cloudflare would
let any visitor spoof their IP and dodge blocks.
Disable User Accounts
Switches an account off without deleting it — for someone who has left, an account under investigation, or one you suspect is compromised. Manage it from Users: a Status column plus a Disable/Enable row action, or the checkbox on the user's own edit screen. An optional note records why.
A disabled account is made to look like one that never existed. The login form answers with WordPress's own "not registered" message — the actual core string, pulled from the default text domain so the wording and translation match byte for byte. Any other phrasing would tell a prober the username is real but locked, which is the one fact worth hiding. The answer is identical whether the password typed was right or wrong.
Blocking the login form alone would not be enough, so three other doors close with it:
| Door | How it is closed |
|---|---|
| An existing session | All session tokens are destroyed the moment the account is disabled |
| A surviving cookie | determine_current_user rejects the account |
| An Application Password | Same filter — these never pass through authenticate at all |
| Password reset | Behaves exactly as it does for an address that is not registered: same redirect, same confirmation screen, no email |
Two guard rails: you cannot disable your own account, and you cannot disable the last administrator who is still enabled.
Core's
wp_authenticate_username_password()discards an incomingWP_Errorinstead of stopping on it, so a check placed before it would be silently overridden by a correct password. The module therefore removes core's own callbacks for that request, and re-asserts the block afterwards as a safety net. Removing them also means the password is never hashed, so a disabled account takes about as long to reject as one that does not exist.
Cloudflare Turnstile
A challenge on the login form, verified server-side against Cloudflare.
The module distinguishes "Cloudflare said no" from "we could not reach Cloudflare". The first is always a rejection. The second is governed by the If Cloudflare is unreachable setting, which defaults to allow the login to proceed — otherwise a Cloudflare outage becomes a total lockout of your own site, which is a security plugin attacking the site it protects.
Fail-open is never silent: it logs at alert level and raises a dismissible
admin notice, because a fail-open nobody notices is one nobody fixes. Three
consecutive transport failures also open a five-minute circuit breaker, so an
outage does not make every login wait for the full timeout. Addresses on the
login limiter's IP whitelist skip the challenge entirely.
Hide Login Page
Serves wp-login.php's own code from a custom path and 404s the real one.
This is obscurity, not access control. It cuts automated credential-stuffing noise; it does not stop a targeted attacker.
It requires pretty permalinks. With plain permalinks there is no rewrite
sending unknown paths to index.php, so your custom URL never reaches
WordPress and the web server returns its own 404. Set
Settings → Permalinks to any non-plain structure first.
Escape hatch: add this to wp-config.php to reach the normal login page
again:
define( 'SLS_DISABLE_HIDE_LOGIN', true );
System Hardening
The only module that writes to wp-config.php. Nothing happens automatically:
you review a preview and click Apply Hardening, and every write takes a
full backup first and goes through a temp-file + atomic rename, so a failure
mid-write cannot leave a half-written config.
| Rule | Applied via | Notes |
|---|---|---|
| User enumeration (5 toggles) | Runtime hooks | See below; takes effect immediately |
| Hide PHP errors | wp-config.php |
Defines WP_DEBUG_DISPLAY as false |
| Disallow file modifications | wp-config.php |
Defines DISALLOW_FILE_MODS |
User enumeration
Enumeration is several separate surfaces, not one, and they carry different breakage risk — so each is its own toggle.
| Toggle | Default | Effect |
|---|---|---|
Block ?author=N |
on | Answers with 404, not 403, so "no such user" and "user hidden" look identical. Skipped for users who can list_users |
| Hide REST users endpoint | on | Removes /wp/v2/users for anonymous requests only, so the block editor's author selector keeps working |
| Keep authors out of the sitemap | on | Drops the users sitemap provider |
| Generic login errors | off | Makes "unknown username" and "wrong password" identical — the single largest enumeration leak |
| Generic password reset | off | The reset form stops confirming whether an account exists |
The last two are off by default because they trade real usability for the protection: a legitimate user who mistypes gets a vaguer message. They are also the two that actually close the biggest hole, so turn them on if your threat model warrants it.
DISALLOW_FILE_MODS is aggressive. It blocks plugin and theme updates
from the dashboard, not just the file editor. If you only want the editor
gone, use the File Editor Lock module instead — it defines
DISALLOW_FILE_EDIT at runtime, with no file writes and instant reversal.
The .htaccess half denies access to wp-config.php, wp-config-sample.php,
readme.html, license.txt and .htaccess itself, and disables directory
indexes.
Security Headers
X-Frame-Options and X-Content-Type-Options are safe defaults. Two are not,
and are off by default:
- HSTS — browsers will refuse plain HTTP for the whole
max-age(default 180 days). Only enable once every part of the site is permanently HTTPS. The header is only sent on requests that are already HTTPS. - CSP — an incorrect policy breaks scripts, styles and embeds. Start in report-only mode and read the browser console before enforcing.
The default policy is script-src 'self', which means no CDN-hosted script
will load anywhere in wp-admin or wp-login once CSP is enforced. That is
intentional, and it is why this plugin bundles everything locally.
Web Application Firewall
Pattern-matches SQLi, XSS, path traversal, PHP file uploads and known scanner user agents.
When enabled it installs mu-plugins/sls-waf-loader.php so inspection runs
before regular plugins and the theme load. If mu-plugins is not writable it
falls back to the init hook — later, but still functional. The loader
self-disables if the main plugin is deactivated or the module is switched off.
wp-admin requests are exempt (except admin-ajax.php), decided on the URL
path so the exemption cannot be faked with a query string.
Start in log-only mode. Block mode has real false-positive potential: REST requests are inspected, so saving a post that legitimately contains `
Releases
2 releases.
These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.