WP Manifestindependent plugin directory
manifest / security / sentinel-lite-security

Sentinel Lite Security releases

wordpress lite security plugin

by Sentinel Lite · github.com/mchuluq/sentinel-lite-security

0stars
0forks

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

All-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

  1. 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.
  2. Activate it from Plugins.
  3. 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.php may break.
  • Login Limiter with .htaccess blocking — 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 incoming WP_Error instead 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.

Tag
Published
1.1.0 latest
Aug 26, 2026 15d ago
Aug 26, 2026 15d ago

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.