Repository navigation
Hardening
Hardening switches on a set of safe defaults that do not change how the site works for visitors: no code editor in wp-admin, no username discovery, no admin-level role for new accounts, one generic login error, no WordPress version in the page source, XML-RPC off and basic security headers. Each part has its own switch, and a part that Wordfence or wp-config.php already provides is skipped.
Hardening deliberately leaves out a Content Security Policy (it breaks payment iframes unless it is tuned for each site), stripping ?ver= from asset addresses (it breaks cache busting) and any writes to .htaccess.
| Module ID | hardening |
| Default | On |
| Available | Always |
| WP-CLI | None |
| Since | 0.1.0 |
Disable the theme/plugin file editor takes the edit_plugins, edit_themes and edit_files capabilities away from every user, administrators included. WordPress then hides the theme and plugin file editors in wp-admin. Someone who steals an administrator session cannot paste PHP code into a plugin file from the browser.
Block username discovery closes four places where WordPress shows login names:
| Where | Who it applies to | What happens |
|---|---|---|
?author=N scans, such as https://example.com/?author=1
|
Visitors who are not logged in | A 301 redirect to the home page, for any value of author
|
The REST API users endpoint (/wp-json/wp/v2/users) |
Visitors who are not logged in | HTTP 401 with the code rest_user_cannot_view and the message "Sorry, you are not allowed to list users." |
| The users sitemap | Everyone | Removed from the WordPress sitemap |
| oEmbed data | Everyone |
author_name and author_url are removed |
The REST check matches the users controller itself, not the route text, so variants of the address (for example in other letter case) are blocked too. Logged-in users can still use the users endpoint.
The redirect runs before WordPress would turn ?author=1 into /author/<name>/, so a scan does not learn the name from the redirect either.
Attackers who get write access to the database can set the default role for new accounts to Administrator and then register their own account. Keep new accounts out of admin-level roles stops that.
When something reads the default role, SafeHouse checks the stored role. If the role has any of these capabilities, new accounts get Subscriber instead:
manage_options, edit_users, create_users, promote_users, delete_users, install_plugins, activate_plugins, edit_plugins, edit_themes, edit_files, update_core, unfiltered_html, manage_woocommerce
Editor (which has unfiltered_html) and WooCommerce's Shop manager (which has manage_woocommerce) are on that list on purpose: raw HTML can carry scripts, and a Shop manager runs the shop. A role name that does not exist is passed through unchanged.
SafeHouse changes only what WordPress reads, never the stored value. In WP-CLI and on Settings → General WordPress sees the real stored role, so you can see and fix it there.
While Hardening runs, Tools → Site Health has the test "SafeHouse registration check". It reads the stored default role, past the filter above, and shows one of three results:
| Result | Status | When |
|---|---|---|
| "New accounts are set to get a role that can manage the site" | Critical | The default role has one of the capabilities listed above. This shows even when registration is closed. |
| "Anyone can register an account" | Recommended | Anyone can register is on and the role is safe. |
| "Registration is closed" | Good | "Only administrators can create WordPress accounts." |
The critical result explains: "Attackers set this so that they can register their own administrator. Change it in Settings → General and check who changed it." While Keep new accounts out of admin-level roles is on, it adds "Until then SafeHouse gives new accounts the Subscriber role." The recommended result points out that "WooCommerce customer accounts have their own setting and keep working." Both have the link Open general settings.
WordPress tells a wrong username apart from a wrong password, which lets bots find real login names. With Generic login errors on, both get one message:
Error: The username, email address or password is incorrect.
This applies to every login that goes through WordPress's authenticate filter: wp-login.php, the WooCommerce My Account login form and XML-RPC. Only WordPress's errors for an unknown username, an unknown e-mail address and a wrong password (invalid_username, invalid_email, incorrect_password) are replaced. Every other error passes through unchanged, including the Login limits lockout and pause messages, the Bot protection messages and two-factor errors from other plugins.
Hide the WordPress version removes the generator meta tag from the page <head> and the generator line from feeds.
With Disable XML-RPC on:
- every request to
xmlrpc.phpgets HTTP 403 with the plain-text bodyXML-RPC is disabled on this site. - WordPress's XML-RPC switch is turned off, and the pingback methods are removed
- the
X-Pingbackheader and the RSD link in the page<head>are removed
The WordPress mobile app and old desktop editors that use XML-RPC stop working with the site. When Jetpack or WooPayments is active, which need XML-RPC, this part is skipped (see With Wordfence and other plugins).
Basic security headers adds three response headers:
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
HSTS (1 year, this domain only) adds one more, only on requests WordPress sees as HTTPS:
Strict-Transport-Security: max-age=31536000
The HSTS header has no includeSubDomains and no preload, so it applies to this host name only. Once a browser has seen it, that browser refuses plain HTTP to the host for a year, even if you turn the switch off later.
SafeHouse sends these headers from WordPress's send_headers action, which WordPress runs for front-end pages. Responses from wp-admin, wp-login.php and the REST API do not get them from SafeHouse. SafeHouse also skips a header when PHP has already set one with the same name, for example from another plugin.
On SafeHouse → Modules → Hardening, select Details. All settings are switches.
| Setting | Default | What it does |
|---|---|---|
| Disable the theme/plugin file editor | On | Removes the file editors for everyone. |
| Block username discovery | On | "For visitors who are not logged in: ?author=N scans, the REST users endpoint, the users sitemap and author data in oEmbed." |
| Keep new accounts out of admin-level roles | On | "If the default role for new accounts can manage users, plugins, options, the shop or raw HTML (for example after a database attack), new accounts get Subscriber instead." |
| Generic login errors | On | "One message for a wrong username or a wrong password, on wp-login.php and the WooCommerce login form." |
| Hide the WordPress version | On | Removes the generator tag. |
| Disable XML-RPC | On | "xmlrpc.php answers 403. The WordPress mobile app and old desktop editors stop working with this site. Skipped automatically when Jetpack or WooPayments is active." |
| Basic security headers | On | "X-Content-Type-Options, Referrer-Policy and X-Frame-Options (same origin). Pages served from a full-page cache may not get them." |
| HSTS (1 year, this domain only) | Off | "Only on HTTPS. Browsers will refuse plain HTTP for a year, so turn it on only when HTTPS works everywhere on this domain." |
Hardening has no panel and no buttons.
If Site Health shows "New accounts are set to get a role that can manage the site":
-
Go to Settings → General.
This screen shows the real stored role, not the Subscriber role SafeHouse hands out in the meantime.
-
Set the default role for new accounts to the role your site needs, usually Subscriber.
-
Select Save Changes.
Then find out who changed it. If Change alerts runs, the activity log has the change: as option_changed when it was made through WordPress, or as inventory_changed when the hourly check found it, for example after a change straight in the database.
Warning
For a year after a browser sees the HSTS header, that browser refuses plain HTTP to this host name. Turning the switch off does not undo that.
- Check that every page and every asset of the site loads over HTTPS on this host name.
- Go to SafeHouse → Modules → Hardening and select Details.
- Turn on HSTS (1 year, this domain only).
- Select Save Changes.
Behind a proxy or load balancer that ends HTTPS and talks to the server over plain HTTP, WordPress may not see the request as HTTPS. SafeHouse then does not send the header.
A part that something else already provides is locked on the settings page with the note "Handled by , skipped here.", and SafeHouse does not run it:
| Setting | Skipped when | Note shown |
|---|---|---|
| Disable the theme/plugin file editor |
DISALLOW_FILE_EDIT or DISALLOW_FILE_MODS is set to true in wp-config.php
|
"Handled by wp-config.php, skipped here." |
| Block username discovery | Wordfence is active and its option to prevent username discovery is on | "Handled by Wordfence, skipped here." |
| Generic login errors | Wordfence is active, its option to mask login errors is on, and WooCommerce is not active | "Handled by Wordfence, skipped here." |
| Hide the WordPress version | Wordfence is active and its option to hide the WordPress version is on | "Handled by Wordfence, skipped here." |
| Disable XML-RPC | Jetpack is active | "Handled by Jetpack (needs XML-RPC), skipped here." |
| Disable XML-RPC | WooPayments is active | "Handled by WooPayments (needs XML-RPC), skipped here." |
With WooCommerce active, SafeHouse keeps its own generic login errors even when Wordfence masks them, because Wordfence covers wp-login.php only and the WooCommerce login form would still tell unknown users from wrong passwords.
Keep new accounts out of admin-level roles, Basic security headers and HSTS (1 year, this domain only) are never skipped.
Set SHOUSE_IGNORE_OVERLAPS to true in wp-config.php to run every part anyway. That also turns XML-RPC off while Jetpack or WooPayments is active, although they need it.
Only its settings, in the shouse_settings option. Hardening writes no activity log entries and sends no e-mails.
- Only
?author=scans are redirected. Author archives at/author/<name>/still exist and show the author's URL name, which is often the login name. Themes can also print author names on posts. - Editor and Shop manager count as admin-level roles. A site that wants new accounts to become Editors or Shop managers automatically must turn Keep new accounts out of admin-level roles off.
- The security headers and HSTS are sent on front-end pages only, not on wp-admin,
wp-login.phpor REST API responses. Pages served from a full-page cache may not have them. - SafeHouse only sees headers set in PHP. If the web server configuration adds the same headers, a response can carry them twice; turn Basic security headers off in that case.
- There is no Content Security Policy, no
Permissions-Policyand no HSTS for subdomains.
These pages describe SafeHouse 0.2.0 and were last reviewed on 2026-10-07. · Repository · Releases · Changelog · Report a problem with these pages
Search every page and heading in the Pages box.
Start here
Modules
- All modules
- Security: Hardening · Login limits · Bot protection · Install lockdown
- Alerts: Change alerts · Plugin health · Vulnerability alerts
- Performance: LiteSpeed page cache · Cloudflare cache · Redis object cache
- WooCommerce: Omnibus price history
- Site tools: SMTP mail · Header and footer scripts · Duplicate posts and pages · Maintenance mode · Tweaks
Guides
- Behind Cloudflare or a proxy
- Running with Wordfence
- Configure in wp-config
- Deploy with git or CI
- Replacing other plugins
- Upgrade from WPHouse
Reference
Trust
Help