Skip to content

Hardening

Three plugins narrow what an anonymous visitor can learn or reach: instance-security-hardening, instance-security-headers and instance-disable-rest-api.

instance-security-hardening closes the routes that hand out valid usernames, because a username turns password guessing from two unknowns into one.

RuleWhy
/?author=1 probes return a 404The redirect to /author/<login>/ otherwise confirms a valid login
Author rewrite rules are never registeredNo author archives to enumerate in the first place
author_link points at the home pageAuthor links stop appearing in the front end
The users sitemap provider is removedwp-sitemap-users-1.xml is a list of valid logins
/wp/v2/users routes are unregisteredSame list, over REST
Login errors are replaced with one messageCore otherwise says whether the username or the password was wrong
Application passwords are disabledThey authenticate over Basic auth on any request, never expire, and bypass 2FA
The X-Pingback header is removedNothing to advertise, the XML-RPC methods are filtered out

The one error let through is the login limiter lockout, which names no account and has to reach the person it is stopping.

instance-security-headers sends a baseline on front end, admin and login responses:

HeaderValue
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
X-Frame-OptionsSAMEORIGIN
Cross-Origin-Opener-Policysame-origin
X-Permitted-Cross-Domain-Policiesnone
Strict-Transport-Securitymax-age=31536000; includeSubDomains, over HTTPS only

There is deliberately no Content-Security-Policy: a useful policy depends on the embeds, analytics and form scripts a given site loads, so it belongs in the project.

add_filter('instance_security_headers', function (array $headers): array {
$headers['Content-Security-Policy'] = "default-src 'self'; img-src 'self' data:;";
return $headers;
});

instance-disable-rest-api unregisters the namespaces WordPress core ships, so their routes 404 instead of 401, and removes REST and oEmbed discovery from <head>, the HTTP headers and RSD.

Unregistered by default: wp/v2, wp-block-editor/v1, oembed/1.0, batch/v1.

Left alone:

  • wp-site-health/v1 — capability gated, and the Site Health screen needs it
  • your own namespaces — anything the theme registers keeps working

Hand one back when something needs it:

add_filter('instance_disabled_rest_namespaces', function (array $namespaces): array {
return array_diff($namespaces, ['oembed/1.0']);
});