HTTP Basic Auth for WordPress Admin on cPanel

Share
HTTP Basic Auth for WordPress Admin on cPanel

Putting HTTP Basic Auth in front of WordPress admin trips up most people for two reasons: WordPress's rewrite rules eat any wp-login.php protection placed in the wrong spot, and blocking /wp-admin/ wholesale kills admin-ajax.php — breaking WooCommerce, Elementor, heartbeat API, and most plugin AJAX. This tutorial gets both right.

Prerequisites

  • cPanel/WHM server with shell access
  • WordPress installed under a cPanel account
  • Replace USERNAME with the actual cPanel username
  • Replace DOMAIN with the actual domain

1. Create the htpasswd File

Create the password file outside the document root:

mkdir -p /home/USERNAME/.htpasswds/wp-admin && htpasswd -c /home/USERNAME/.htpasswds/wp-admin/passwd USERNAME

Set correct ownership and permissions:

chown USERNAME:nobody /home/USERNAME/.htpasswds/wp-admin/passwd && chmod 640 /home/USERNAME/.htpasswds/wp-admin/passwd

cPanel convention requires /home/USERNAME/.htpasswds/<dirname>/passwd — EA-Apache's suexec/mod_userdir permissions expect this layout.

Use a different password than the cPanel account login.

2. Protect /wp-admin/

Create or edit /home/USERNAME/public_html/wp-admin/.htaccess:

AuthType Basic
AuthName "Restricted Admin"
AuthUserFile "/home/USERNAME/.htpasswds/wp-admin/passwd"
Require valid-user

<Files "admin-ajax.php">
    Satisfy Any
    Order allow,deny
    Allow from all
    Require all granted
</Files>

The admin-ajax.php exclusion is mandatory — blocking it breaks WooCommerce, Contact Form 7, Elementor, heartbeat API, and most front-end JS.

3. Protect wp-login.php

Edit /home/USERNAME/public_html/.htaccess and add the following above the # BEGIN WordPress block (WP rewrite rules will override it otherwise):

<Files "wp-login.php">
    AuthType Basic
    AuthName "Restricted Login"
    AuthUserFile "/home/USERNAME/.htpasswds/wp-admin/passwd"
    Require valid-user
</Files>

ErrorDocument 401 "Authentication Required"

4. Verify

curl -I -u USERNAME:PASSWORD https://DOMAIN/wp-admin/ ; curl -I https://DOMAIN/wp-admin/admin-ajax.php ; curl -I https://DOMAIN/wp-login.php

Expected:

  • /wp-admin/ returns 200 with credentials, 401 without
  • admin-ajax.php returns 400 with no auth prompt
  • wp-login.php returns 401

Things to Watch For

  • Addon/subdomain accounts: docroot may not be public_html. Confirm with uapi --user=USERNAME DomainInfo list_domains.
  • REST API: if plugins use /wp-json/ for admin actions, you may need to whitelist it the same way as admin-ajax.php.
  • WP-Cron (wp-cron.php) lives at root — unaffected here, but if you protect root, exclude it.
  • LiteSpeed Enterprise on cPanel honors .htaccess natively. OpenLiteSpeed does not — use the vhost config instead.
  • mod_security may interfere with repeated 401s — check /usr/local/apache/logs/error_log.
  • 2FA/SSO plugins (Wordfence Login Security, miniOrange) still work — Basic Auth gates first, plugin auth runs second.

Hardening Additions

IP Whitelist (Stronger Than Password)

If you have a static IP:

<Files "wp-login.php">
    Require ip 1.2.3.4 5.6.7.0/24
</Files>

Combine IP + Basic Auth

<Files "wp-login.php">
    AuthType Basic
    AuthName "Restricted"
    AuthUserFile "/home/USERNAME/.htpasswds/wp-admin/passwd"
    Require valid-user
    Require ip 1.2.3.4
    Satisfy Any
</Files>

Satisfy Any means either passes. Satisfy All means both required.

Block XML-RPC

xmlrpc.php bypasses both wp-login and admin-ajax protections for credential attacks:

<Files "xmlrpc.php">
    Require all denied
</Files>

CSF Brute-Force Protection

Edit /etc/csf/csf.conf:

LF_HTACCESS = 5

Restart CSF:

csf -r

Defense in Depth

Install WPS Hide Login to rename wp-login.php — Basic Auth still catches anything hitting the real path.

Removal

To revert:

rm /home/USERNAME/public_html/wp-admin/.htaccess && sed -i '/<Files "wp-login.php">/,/<\/Files>/d' /home/USERNAME/public_html/.htaccess && rm -rf /home/USERNAME/.htpasswds/wp-admin