Description
Adds layer of security for your WordPress site. Adds custom login page slug, enables 2FA, removes security issues. Adds remember device, counts login attempts and lock usernames if the password is wrong. Out of band e-mail is also supported – instead of entering codes, your user can use simple login link from within their e-mail client.
WooCommerce
WooCommerce is also supported for 2FA, just enable the plugin and all your customers will be asked to enable two-factor authentication.
List with currently supported features:
- Login redirection – redirects the default wp-login.php to a slug of your choice
- Login attempts – counts the unsuccessful attempts, and locks user if there are too many
- 2FA settings – gives the ability to use two factor authentication and Out Of Band email link
- Remember devices – current device could be remembered for given amount of days and user wont be asked for the 2FA code on it before that (the password is still required)
- Removes XML-RPC from your WordPress site
- Custom shortcode ([wps_custom_settings]) can be used to give the users without access to the dashboard to setup the 2FA
- Backup codes – ten single-use codes per user, the way back in when the phone is lost
- Signed-in sessions – see everywhere an account is signed in, and log sessions out one by one
Login Redirection
You can change the default wp-login.php to slug of your choice. That will prevent most common hacker attacks and will harden your WordPress installation. You can redirect the original wp-login.php to the slug of your choice.
2FA login
Enable two-factor authentication for your WordPress site, and to enforce your website users, or some of them to use 2FA. Next time user logins s/he will be asked to enable the 2FA using their favorite application. Once the process is completed, every time the user logs, s/he will be asked to provide the 2FA code.
Which second step users get after the password:
- TOTP on – a code from the authenticator app. With Out of band e-mail also on, the email link is offered as an alternative.
- TOTP off, Out of band e-mail on – a one-time sign-in link sent to the user’s email address.
- both off – no second step; the password alone signs users in, even with 2FA enabled. Passkeys do not change that: they are a separate way to sign in, not a step after the password.
An emailed link is a weaker second step than TOTP: whoever controls the mailbox can also reset the password.
Setting up the authenticator (at the first login after 2FA is enabled, or on the profile page) shows a QR code, an “Open in authenticator app” button and the setup key with a Copy button. On a phone the QR code cannot be scanned from its own screen: the button opens the otpauth:// link, which iOS and Android hand to the installed authenticator or password app (Google Authenticator, Microsoft Authenticator, 1Password, iOS Passwords, …); the setup key can also be copied and pasted into the app. The code field is marked as a one-time code, so iOS / macOS Passwords and password managers that hold the secret offer the current code at later logins.
On the profile page everything Secured WP adds – remembered devices, the authenticator and passkeys – is grouped in one “Secured WP” panel; the same panel is used by the [wps_custom_settings] shortcode and on WooCommerce’s My Account page.
Login Attempts
This gives you the ability to prevent brute force attacks if the hacker knows the username and tries to guess the password. With this enabled, after the given amount of tries that username is locked for the given amount of time – from the address the tries came from. Somebody guessing from one place cannot lock the real user out everywhere. If the account fails ten times the allowed amount from many addresses, it is locked everywhere, except on devices the user asked to remember. Behind a reverse proxy or CDN, use the wpsec_client_ip filter to supply the visitor’s real address, otherwise every visitor shares the proxy’s address. Attempts are counted before the password or code is checked, with counters that cannot be outrun by sending many requests at once. An account locked by an administrator cannot be used with application passwords either.
Remember device setting
With that, user can use given device for the given amount of days without being asked for the 2FA code. The password is always required – a remembered device never signs anybody in on its own. The devices can be removed or checked from the default user settings page; changing the password or resetting the authenticator forgets them all.
That setting is based on current setting (global) for the current moment, which means that when the day value (in settings) is changed globally, that wont reflect the already set cookies and user devices.
Example: If you set that to 10 days and there is a user which decide to use Remember Device functionality, when you change that value to 15 days, that wont increase the time for that user. Same applies for decreasing the value.
Encryption key for 2FA secrets
Authenticator secrets are stored encrypted (XChaCha20-Poly1305), so a copy of the database alone – a leaked backup, an SQL injection – does not let anyone generate your users’ codes. That only holds if the key is kept somewhere other than the database. The plugin takes the key from the first of these that exists:
- The
WPSEC_ENCRYPTION_KEYenvironment variable. Best choice when your host lets you set environment variables (Docker, most managed hosts, orfastcgi_param WPSEC_ENCRYPTION_KEY "...";in nginx). It always takes priority. - The
WPSEC_ENCRYPTION_KEYconstant in wp-config.php:define( 'WPSEC_ENCRYPTION_KEY', '...' ); - A key stored in the database – only as a fallback. It keeps working when the WordPress salts are changed, but anyone holding a copy of the whole database also holds this key.
The key must be at least 32 random characters, for example the output of openssl rand -hex 32. A shorter value is ignored and reported as an error; users then cannot set up an authenticator until it is fixed.
When none of these exists, the plugin creates a key itself – on activation, the first time the plugin settings page is opened, or the first time one is needed – and tries to add it to the top of wp-config.php, right after the opening <?php (after a declare() line, if the file starts with one):
// BEGIN Secured WP - encryption key for the stored 2FA secrets. Do not change or remove it; see the Secured WP readme.
defined( 'WPSEC_ENCRYPTION_KEY' ) || define( 'WPSEC_ENCRYPTION_KEY', '<64 hex characters>' );
// END Secured WP
The top of the file is used because every wp-config.php starts with <?php, while the “That’s all, stop editing!” line differs between hosts or is missing. The plugin only writes when wp-config.php is writable and does not mention WPSEC_ENCRYPTION_KEY yet, never when the file declares a namespace, and only after PHP itself has parsed the result. It writes in place (owner, permissions and symlinks are kept) and puts the original back if the result does not read back exactly. To stop the plugin from ever editing wp-config.php, add add_filter( 'wpsec_write_wp_config', '__return_false' ); to a must-use plugin.
When wp-config.php cannot be written, the key is stored in the database, and the plugin settings page and Tools > Site Health say so. To move it out of the database, do one of these:
- Make wp-config.php writable, click “Move the key to wp-config.php” on the plugin settings page (or run
wp wpsec move-key), then make the file read-only again. The same key is written to the top of the file and deleted from the database, so nothing needs re-encrypting. - Or copy the key yourself. After confirming your identity, the settings page shows the exact block to paste (
wp wpsec move-keyprints it too when it cannot write). Put it at the top of wp-config.php, right after<?php– anywhere before the line that loads wp-settings.php works – or, where the host manages wp-config.php, in the file the host tells you to use for your own settings. - Or set the
WPSEC_ENCRYPTION_KEYenvironment variable to the value between the quotes in that block.
As soon as the plugin finds the same key in wp-config.php or the environment, it uses it from there – it takes precedence over the database – and deletes the database copy (the next time an administrator opens the admin, or on wp wpsec key-status).
If your deployments replace wp-config.php (git, CI, or a host that manages the file), the block the plugin wrote would be removed on the next deploy. Copy it into the wp-config.php you deploy, or use the environment variable. If the key goes missing while users still have authenticators encrypted with it, the plugin does not create a new key over it: the settings page, the Users screens and Site Health report how many authenticators cannot be read, until the key is restored or those authenticators are reset.
Changing or adding a key: every stored value records which key encrypted it. The plugin reads with any key it still has, and re-encrypts with the current one (the first in the list above) whenever a secret is read.
- Moving from the database key or the wp-config.php constant to the environment variable: just set the environment variable. The old key stays where it is and is still used to read.
- Changing the value of the environment variable or the constant: put the old value into
WPSEC_ENCRYPTION_KEY_PREVIOUS(environment variable or constant) and the new value intoWPSEC_ENCRYPTION_KEY. The previous key is only ever used to read.
To move everything at once, run wp wpsec reseal. Once it reports nothing unreadable, the old key can be removed: delete WPSEC_ENCRYPTION_KEY_PREVIOUS or the old constant, and wp wpsec reseal --drop-database-key deletes the database fallback key.
Never remove or change the only key that encrypted the stored secrets. Those users’ authenticators cannot be read any more: they are told so at login, can use the emailed link or a passkey if enabled, and must have their authenticator reset. Salt changes do not affect the key.
Secrets stored by earlier versions (encrypted with a key derived from the WordPress salts) are read and re-encrypted with the new key automatically. If the salts were changed before the upgrade, those secrets cannot be read and must be reset.
Backup codes
Right after setting up the authenticator, each user gets ten backup codes (XXXXX-XXXXX), shown once, with Copy, Download and Print. Each code signs in once instead of an authenticator code – at login (“Lost your phone? Use a backup code instead”) and when confirming identity for security changes. So a user who lost their phone can sign in, confirm their identity with another code, reset their authenticator on their profile and set it up on the new phone, without an administrator. Codes also work when the stored authenticator cannot be read because its encryption key was lost.
The profile has its own Backup codes section next to the authenticator: how many are left and when they were made, always visible, with “Make backup codes” (it asks the user to confirm their identity first when needed; a new set replaces the old one). Users who set up the authenticator before backup codes existed get a first set, once, at their next login, and every user with two-factor login sees an admin notice when they have no codes or 2 or fewer left – dismissible until that changes. Administrators see a user’s count on their profile, never the codes, and cannot make them for someone else.
Codes are stored as password hashes, independent of the encryption key and the salts, and each can be used exactly once, even by simultaneous requests. Resetting the authenticator retires the codes; the next setup comes with a new set. For support, wp wpsec backup-codes <user> --generate prints a fresh set to hand over.
Signed-in sessions
The Secured WP panel on the profile lists every place the account is signed in right now – browser, IP address, when it signed in and when the session expires – marks “this browser”, and logs sessions out one by one or all others at once. Administrators can do the same for users they may edit (“Log out everywhere”). Logging out single sessions needs WordPress’s own session storage; if a plugin replaces it, only “log out all other sessions” is offered.
Resetting a user’s authenticator
A reset removes the authenticator and its backup codes and forgets the user’s remembered devices; they set up a new one (with new backup codes) at their next login. It is needed when a phone is lost, or when an authenticator cannot be read (its encryption key was lost). The Users screen shows “UNREADABLE – reset required” in the Secured WP Status column for those.
- Users screen: “Reset authenticator” under the user’s name, or select users and choose “Reset authenticator” from Bulk actions. Administrators are asked to confirm their identity first.
- Multisite: the authenticator belongs to the user across the whole network, and WordPress lets only network administrators edit other users. The reset is offered on Network Admin > Users, and on a site’s Users screen only to network administrators – site administrators do not get it.
- Profile page: another administrator can reset it there too.
- The user themselves, with a backup code: sign in with one, confirm identity with another, then “Reset authenticator” on the profile.
- WP-CLI:
wp wpsec reset-authenticator <user>..., orwp wpsec reset-authenticator --unreadablefor every authenticator that cannot be read.
WP-CLI
All commands are under wp wpsec; wp help wpsec <command> shows the built-in help. They act on the whole installation – on multisite the key and the users’ authenticators are network-wide, so --url makes no difference. Anyone who can run WP-CLI already controls the site, so the commands do not ask for identity confirmation the way the admin screens do.
wp wpsec key-status
Shows where the encryption key comes from (environment variable, wp-config.php constant, or database), how many keys are available for reading, how many authenticators are stored (and how many are still in the pre-2.5 format), and warns about a misconfigured key, a key kept in the database, or authenticators encrypted with a key that is no longer available. If the database still holds a copy of a key that is now also set in wp-config.php or the environment, the database copy is deleted and the command says so.
wp wpsec move-key
Moves the key stored in the database to the top of wp-config.php, between the “BEGIN Secured WP” and “END Secured WP” lines, then deletes the database copy. It is the same key, so nothing needs re-encrypting. wp-config.php must be writable while the command runs; it can be made read-only again afterwards. If the file cannot be written, the command …
Screenshots







Installation
Manual Installation
- Download the “secured-wp.zip” file with the plugin to a location of your choice
- Upload “secured-wp.zip” by going yo plugins -> Upload plugin and then select the plugin location from step one
- Activate the plugin through the \”Plugins\” menu in WordPress.
Install from within WordPress
- Go to Plugins -> Add new
- Search for “Secured WP”
- Install and activate the plugin through the “Plugins” menu in WordPress.
FAQ
-
Can I disable some of the modules
-
Every single module can be enabled/disabled from its settings tab.
-
Can I exclude some user
-
Yes – go to users page – select users by pressing the check box next to the username, and from the drop down menu select the action you want to perform and click Apply.
Reviews
Contributors & Developers
“Secured WP” is open source software. The following people have contributed to this plugin.
ContributorsTranslate “Secured WP” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
2.5.0
Security review: one-time TOTP consumption across enrollment/login/confirmation, 160-bit secrets for new enrollments, atomic passkey request limits and challenge consumption, atomic profile-enrollment limits, fail-closed database errors and authenticator persistence, escaped lockout emails, proper XML-RPC 2FA errors, safe byte-buffer deserialization, and CLI-only diagnostic scripts.
Performance: one indexed query per counter read; expired-counter cleanup runs on hourly WP-Cron instead of random login requests. Visit the main-site admin once after deployment to schedule cleanup.
Existing authenticator secrets remain valid. A previously accepted code cannot be reused: wait for the next code before authenticating again.
New: backup codes – ten single-use codes per user, shown once after setting up the authenticator, accepted at login and for identity confirmation, replaceable from the profile; self-service recovery after a lost phone or a lost encryption key. wp wpsec backup-codes.
New: signed-in sessions on the profile – browser, IP, sign-in and expiry times, “this browser”, log out one session or all others.
Security: passing a user to the plugin’s User helpers no longer silently makes it the “current” user, and an empty or unknown user resolves to nobody rather than to the logged-in user. Stored settings are type-checked on every read. “Delete data on uninstall” now also removes passkey credentials, remembered devices and old attempt counters.
Removed: unused code and third-party libraries (Bacon QR Code, DASPRiD Enum, ParagonIE, PSR, Spomky, Symfony), the legacy users-list class, the unused JIT asset compilers, the unused password validator and the settings Export/Import tab that had no handler; the plugin no longer defines a global trigger_deprecation() function. Namespace WPSEC\Mosules\Views renamed to WPSEC\Controllers\Modules\Views.
Security: authenticator secrets are stored only after a code from them verifies; a secret shown during a login that was not finished is never used.
Security: an enrolled authenticator secret is no longer shown on the profile page, to the user or to administrators – reset and enroll again instead.
Security: Remember devices skips only the 2FA code; it no longer signs the browser in without a password.
Security: login attempts are locked per source address, with an account-wide backstop that remembered devices get past; the counter no longer re-locks right after a lock ends; administrator emails are sent once per lock period.
Security: the custom login slug no longer leaks through /wp-admin/options.php (CVE-2021-24917).
Security: with only Out of band e-mail enabled, users now get the emailed sign-in link as the second step. Before, the password alone signed them in.
Security: login, code and identity-confirmation limits are counted atomically and before checking, so simultaneous requests can no longer each get the full allowance (before, a burst of parallel code guesses was limited only by the number of PHP workers).
Security: an administrator’s lock also stops application passwords.
Security: authenticator secrets are encrypted with a key kept outside the database (environment variable or wp-config.php, database only as a fallback) instead of one derived from the salts. Changing the salts no longer breaks every login; an unreadable secret is reported instead of crashing the login page. The key is written at the top of wp-config.php (not above a “stop editing” line, which some hosts do not have); a key that had to be stored in the database can be moved with a button or wp wpsec move-key, or copied by hand, after which the database copy is deleted. No new key is created over a lost one. WP-CLI commands: wp wpsec key-status, move-key, reseal, reset-authenticator.
Security: “Reset authenticator” on the Users screens (row and bulk action, network Users screen on multisite); the status column flags authenticators that cannot be read. Bulk actions ignore user IDs that are not real users (a 0 used to act on the administrator running it).
Fixed: the setup QR code did not scan – the format information was written to the wrong cells, and a link too long for the code was silently cut short. Codes now match a reference encoder bit for bit. The built-in encoder (no third-party library) now covers all QR versions 1-40 (up to 2331 bytes, previously 213), so long and non-Latin site names get a working code; dense codes get a “Show a larger QR code” link so a phone can scan them; the QR markup is 6-7 times smaller. If a QR code still cannot be drawn, the page says so and offers the “Open in authenticator app” button and the setup key instead of failing.
Authenticator setup: “Open in authenticator app” button (otpauth:// link) and a Copy button for the setup key; the code field supports one-time-code autofill; the authenticator app now shows the account name next to the site name.
Profile page: Secured WP’s options are grouped in one branded panel (also in the shortcode and WooCommerce My Account).
Security: a WPSEC_ENCRYPTION_KEY line commented out in wp-config.php is no longer used; wp-config.php is not written when the disk is nearly full.
Fixed: a module setting stored as something other than true/false (a hand edit, an import) no longer causes a fatal error on every page.
2.4.0
Update to WP 7.1
2.3.2
Small update – added option to remove styles for gutenberg (core WordPress)
2.3.1
Small update – added option to remove styles for classical themes (core WordPress)
2.3.0
Maintenance update. Tested up to WP 6.9. Added jquery scripts removal logic.
2.2.4
blueprint live preview fixes.
2.2.3
Bug fixes related to login attempts.
2.2.2
Updated TOTP library.
2.2.1
PHP 8 fixes.
2.2.0
Updated libs and fixed deprecations.
2.1.1
Small bug fixes with redirection
2.1.0
Removed all jQuery dependency when custom page (or post) with shortcode is used for user’s settings manipulation. Fixed lots of bugs
2.0.3
- Missing class fix, uninstall script fix
2.0.2
- Added missing constants file
2.0.1
- Fixed bugs and problems, added blueprint.json
2.0.0
- Most of the plugin has been rewritten
1.0.0
- Initial release.
