←back to Blog

How to secure a WordPress login page without a plugin using functions.php and .htaccess

How to Secure Your WordPress Login Page Without a Plugin (Real Fixes)

Every WordPress site with a login page gets hit by bots eventually, usually within days of going live. Securing your WordPress login page without a plugin is completely doable with a handful of code snippets and server-level tweaks, and it avoids adding another plugin that needs updates, settings, and a support ticket when it breaks something. This is the same layered approach security plugins automate, just done directly in your theme and server config.

None of this requires a security background. It requires editing two files: functions.php and .htaccess, both of which are already on your server.

What Makes the WordPress Login Page a Target?

WordPress puts its login page at the same predictable address on every single install: /wp-login.php. That consistency is exactly what makes it easy to attack. Bots don’t need to find your login page, they already know where it is, and WordPress allows unlimited password attempts by default, so a script can sit there and guess passwords all day with nothing stopping it.

This is almost never a targeted attack aimed specifically at one site. It’s automated scanning that hits every WordPress install a bot can find, and unpatched or unprotected logins are the ones that eventually get through. A site that’s already been compromised shows different warning signs than a site that’s just under attack, and it’s worth knowing the difference between signs your WordPress site is hacked and simply seeing failed login attempts pile up in a log.

How Do You Stop WordPress Brute Force Attacks Without a Plugin?

You stop WordPress brute force attacks without a plugin by combining four things: limiting failed login attempts, blocking XML-RPC, stopping username enumeration, and restricting access to wp-login.php at the server level. Each one closes a different door, and together they cover most of what a dedicated security plugin does automatically.

Limit Login Attempts With a Few Lines of Code

A basic login-attempt limiter tracks failed logins by IP address using a WordPress transient, then blocks further attempts once a threshold is hit. Add this to your child theme’s functions.php:

function cf_check_login_attempts( $user, $username, $password ) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $key = 'login_attempts_' . md5( $ip );
    $attempts = (int) get_transient( $key );

    if ( $attempts >= 5 ) {
        return new WP_Error( 'too_many_attempts', 'Too many failed login attempts. Try again in 15 minutes.' );
    }
    return $user;
}
add_filter( 'authenticate', 'cf_check_login_attempts', 30, 3 );

function cf_track_failed_login( $username ) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $key = 'login_attempts_' . md5( $ip );
    $attempts = (int) get_transient( $key );
    set_transient( $key, $attempts + 1, 15 * MINUTE_IN_SECONDS );
}
add_action( 'wp_login_failed', 'cf_track_failed_login' );

function cf_clear_login_attempts( $user_login, $user ) {
    $ip = $_SERVER['REMOTE_ADDR'];
    delete_transient( 'login_attempts_' . md5( $ip ) );
}
add_action( 'wp_login', 'cf_clear_login_attempts', 10, 2 );

This locks an IP out for 15 minutes after five failed attempts and clears the counter on a successful login. On a single-server, non-cached setup this works well. On a site behind Cloudflare or a load balancer, $_SERVER['REMOTE_ADDR'] may return the proxy’s IP instead of the visitor’s, so you’d need to read the real IP from a header like HTTP_CF_CONNECTING_IP instead.

Password-Protect wp-login.php With .htaccess

Adding a second, server-level password on top of WordPress’s own login form blocks bots before they ever reach the WordPress login page at all, since they never get to submit a WordPress username or password in the first place. This requires creating a separate .htpasswd file and pointing an .htaccess rule at it.

Generate a username and hashed password with an htpasswd generator, save the result to a file outside your public web root (for example /home/yourusername/.htpasswds/public_html/wp-login/passwd), then add this to the .htaccess file in your WordPress root, above the WordPress rewrite block:

<Files wp-login.php>
AuthType Basic
AuthName "Restricted Access"
AuthUserFile /home/yourusername/.htpasswds/public_html/wp-login/passwd
Require valid-user
</Files>

Visitors now hit a browser-level login prompt before WordPress’s login form even loads. This is one of the most effective single changes available, because it stops automated tools that only know how to talk to WordPress’s own login form.

Block XML-RPC Requests

The xmlrpc.php endpoint lets attackers test hundreds of username and password combinations in a single request through the system.multicall method, which sidesteps most simple login-attempt limits entirely. Unless you’re actively using the WordPress mobile app or a service like Jetpack that depends on it, there’s little reason to leave it open.

Block it at the server level by adding this to .htaccess:

<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>

Or disable it through WordPress itself by adding this filter to functions.php:

add_filter( 'xmlrpc_enabled', '__return_false' );

The .htaccess version is the stronger of the two, since it blocks the request before WordPress even loads.

Stop Username Enumeration

Username enumeration is when an attacker requests URLs like yoursite.com/?author=1 and reads the redirect to figure out valid usernames, which they then feed straight into a brute-force attempt. Guessing a password is much easier once the username is already known, so this step removes the first move in that chain.

function cf_block_author_enum() {
    if ( isset( $_GET['author'] ) && ! is_admin() ) {
        wp_die( 'Access denied.', 403 );
    }
}
add_action( 'init', 'cf_block_author_enum' );

add_filter( 'rest_endpoints', function( $endpoints ) {
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
    }
    return $endpoints;
} );

This blocks the ?author= probe and removes the WordPress REST API’s public users endpoint, which otherwise lists every username on the site by default.

Should You Change Your WordPress Login URL?

Changing the login URL away from /wp-login.php reduces the volume of automated scan traffic hitting your site, but it is not a real security fix on its own, it’s traffic reduction. Combined with the other steps above, it’s still worth doing because it cuts down noise in your server logs and shrinks the pool of bots that even find the login form to attack.

A lightweight way to do this without a plugin is a small must-use plugin (a file dropped into wp-content/mu-plugins/) that intercepts requests to a custom slug and routes them to the real login page, while returning a 404 for direct requests to wp-login.php. This is more fragile than a maintained plugin and needs re-checking after major WordPress updates, so treat it as a bonus layer, not a replacement for the .htaccess password protection above.

Is Two-Factor Authentication Worth Setting Up Without a Plugin?

Two-factor authentication is worth setting up because it stops the vast majority of automated account takeovers even when a password is fully compromised, but it’s genuinely difficult to implement correctly without a plugin or a dependency like a TOTP library. Unlike login limiting or XML-RPC blocking, which are a handful of hooks, proper 2FA involves secret generation, time-based code verification, and recovery codes.

This is the one piece of WordPress login security where a small, single-purpose plugin (not a full security suite) is the more practical choice than custom code. Everything else on this list holds up fine without one.

Comparing the No-Plugin Methods

MethodSetup EffortWhat It Blocks
Login attempt limiter (functions.php)LowRepeated password guessing
.htaccess password on wp-login.phpMediumBots that only speak to WordPress’s own login form
Block XML-RPCLowMulticall-based mass login attempts
Stop username enumerationLowUsername harvesting via ?author= and REST API
Change login URLMediumAutomated scan volume (not a real barrier alone)

When Does It Make Sense to Use a Security Plugin Instead?

A security plugin makes sense once you’re managing several sites, need a firewall that updates its rules automatically, or want two-factor authentication without maintaining custom code. The code-based approach above is well suited to a single site where you’re comfortable editing functions.php and .htaccess directly, and it keeps the admin dashboard free of another settings screen.

These aren’t mutually exclusive either. Plenty of sites run the .htaccess password protection and XML-RPC block as a permanent baseline, then add a plugin only for 2FA and firewall rules on top. Whichever route you take, none of it replaces regular backups and keeping WordPress core, themes, and plugins updated, since an outdated plugin is still one of the most common ways a login gets bypassed entirely rather than brute-forced.

If you’d rather not touch functions.php and .htaccess directly, or want someone to confirm the setup is correct across a live site, that’s the kind of thing a WordPress maintenance plan is built to cover on an ongoing basis rather than a one-time fix.

Frequently Asked Questions

Combine a login-attempt limiter, an .htaccess password on wp-login.php, a block on XML-RPC requests, and a stop on username enumeration. Together these close the main paths bots use to guess WordPress passwords automatically.

Yes. Login attempt limiting, XML-RPC blocking, username enumeration prevention, and .htaccess password protection can all be added directly through functions.php and .htaccess with no plugin required. Two-factor authentication is the one exception that’s genuinely easier with a small dedicated plugin.

It reduces automated scan traffic but is not a real barrier by itself, since a determined attacker can still find a custom login URL. It’s a useful add-on to real protections like login limiting and .htaccess password protection, not a replacement for them.

XML-RPC is a WordPress API endpoint at xmlrpc.php that allows remote publishing and can let attackers test hundreds of password combinations in a single request through its multicall method. Blocking it removes one of the fastest routes into a brute force attack.

Neither is universally better. Custom code works well for a single site managed by someone comfortable editing functions.php and .htaccess, while a plugin is more practical for managing multiple sites or for features like two-factor authentication that are hard to build from scratch.

Leave a Reply

Your email address will not be published. Required fields are marked *