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
USERNAMEwith the actual cPanel username - Replace
DOMAINwith 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/returns200with credentials,401withoutadmin-ajax.phpreturns400with no auth promptwp-login.phpreturns401
Things to Watch For
- Addon/subdomain accounts: docroot may not be
public_html. Confirm withuapi --user=USERNAME DomainInfo list_domains. - REST API: if plugins use
/wp-json/for admin actions, you may need to whitelist it the same way asadmin-ajax.php. - WP-Cron (
wp-cron.php) lives at root — unaffected here, but if you protect root, exclude it. - LiteSpeed Enterprise on cPanel honors
.htaccessnatively. 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