Every time I slap HTTP Basic Auth on a staging site to keep clients and Google from peeking early, I run into the same headache: Gutenberg breaks. You hit “Save draft” or publish, the editor hangs on an infinite spinner, and your browser console spits out 401 Unauthorized errors. Because Apache intercepts incoming requests before WordPress even boots, your REST API calls fail silently in the background.
Here is how to lock down your staging environment in .htaccess while selectively bypassing Basic Auth for /wp-json/, scheduled cron tasks, and webhooks without poking a massive hole in your security.

The Core Problem With Basic Auth and the WordPress REST API
Standard Apache Basic Auth (handled by mod_auth_basic) challenges every incoming request for an Authorization: Basic header before PHP or WordPress ever executes.
Modern WordPress relies heavily on the REST API. The Gutenberg block editor constantly talks to /wp-json/wp/v2/ endpoints to autosave drafts, populate taxonomy pickers, and render block previews. When Apache gates the entire domain, those background JavaScript fetch() calls fail because the browser doesn’t automatically attach your Basic Auth credentials to internal API requests.
You have probably run into this exact error in your browser console:
{ "code": "rest_cannot_read", "message": "Sorry, you are not allowed to access this resource.", "data": { "status": 401 }
}Server-to-server calls suffer the same fate. If you’re testing Stripe webhooks or remote publishing tools, those services get slapped with a hard Apache 401 response. If you followed our guide to set up WordPress staging on Hostinger and push changes, slapping blanket password protection on your staging folder will immediately stall your testing workflow.
Step 1: Generate Your .htpasswd Credentials
Apache needs an external .htpasswd file containing your username and hashed password. Keep this file outside of your public web root (above public_html or html) so no one can download it directly in a browser.
SSH into your server and switch to a secure directory one level above your public root (like /home/username/ or /var/www/):
cd /home/u123456789/
htpasswd -c .htpasswd staginguserThe prompt will ask you to enter and confirm your password. The -c flag tells the utility to create a new file. If you need to add a second user later, make sure you drop the -c flag so you do not overwrite existing credentials:
htpasswd .htpasswd seconddeveloperLock down the file permissions so only your web server user (usually www-data or your host user account) can read it:
chmod 644 /home/u123456789/.htpasswdStep 2: Configure .htaccess to Exclude wp-json
Open the .htaccess file in your staging site’s root directory. The trick here is using Apache’s SetEnvIf directive to flag requests hitting the REST API or cron, then conditionally exempting them.
Place this block at the very top of your .htaccess file, right before the standard # BEGIN WordPress section:
# Whitelist REST API and internal calls from Basic Auth
SetEnvIf Request_URI "^/wp-json/" allow_rest=1
SetEnvIf Request_URI "^/wp-cron.php" allow_rest=1 AuthType Basic
AuthName "Staging Environment - Private Access"
AuthUserFile /home/u123456789/.htpasswd Require env allow_rest
Require valid-user # Combine requirements: require valid-user OR allow_rest
Require env allow_rest Require valid-user
Here is how this works: Apache checks the request URI. If the path begins with /wp-json/ or matches /wp-cron.php, it sets an internal environment variable named allow_rest=1. The RequireAny directive tells Apache to grant access if the user supplies valid credentials or if that environment variable is present.
Handling Rewrite Rules and Query Parameter REST Calls
Not every staging site runs pretty permalinks. If your staging instance uses plain permalinks or a plugin accesses the REST API via query parameters like ?rest_route=/, the SetEnvIf Request_URI rule above can miss it because Request_URI doesn’t always evaluate query strings cleanly across different Apache builds.
We can use mod_rewrite to inspect the query string and flag the environment variable reliably:
RewriteEngine On # Check for query param based REST calls (?rest_route=/...) RewriteCond %{QUERY_STRING} rest_route=^/?wp-json [NC] RewriteRule .* - [E=allow_rest:1]
# Check standard REST paths
SetEnvIf Request_URI "^/wp-json/" allow_rest=1
SetEnvIf Request_URI "^/wp-cron.php" allow_rest=1 AuthType Basic
AuthName "Restricted Staging Area"
AuthUserFile /home/u123456789/.htpasswd Require env allow_rest Require valid-user
If you run multiple subdomains under the same hosting account, watch out for parent directory configurations bleeding downward. Take a look at how to fix root .htaccess inheritance on Hostinger subdomains if your staging environment is nested inside a subfolder.
The “Authorization Header Stripped” Trap in FastCGI
This caught me off guard for hours on an Apache and PHP-FPM setup. When running PHP as FastCGI or FPM, Apache strips out the HTTP Authorization header by default before forwarding the request downstream to PHP.
While Apache Basic Auth still works fine, WordPress application-level authentication (like Application Passwords introduced in WP 5.6 or headless JWT tokens) breaks completely because PHP receives an empty $_SERVER['HTTP_AUTHORIZATION'].
The official WordPress REST API Authentication Documentation details why this header is mandatory. To force Apache to pass the header to WordPress, add this rule inside your mod_rewrite block:
RewriteEngine On # Pass Authorization header down to PHP-FPM RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
With that in place, your local test scripts and headless clients can authenticate with WordPress Application Passwords directly through the REST API without running into an Apache roadblock.
Testing Your Setup With cURL
Do not just test this in your regular browser. Browsers aggressively cache Basic Auth credentials during an active session, which masks misconfigurations and makes you think everything is fine.
Run these three cURL commands from your terminal to verify your security rules:
1. Test frontend protection (this should return HTTP/1.1 401 Unauthorized):
curl -I https://staging.example.com/2. Test authenticated access with credentials (this should return HTTP/1.1 200 OK):
curl -I -u staginguser:yourpassword https://staging.example.com/3. Test the REST API unauthenticated (this should return HTTP/1.1 200 OK and output API namespace JSON):
curl -I https://staging.example.com/wp-json/If check #1 gives you a 401, while #2 and #3 return 200, your rules are working. The frontend stays completely locked down from search crawlers and random visitors, while Gutenberg and background integrations keep humming.
Alternative: Whitelisting by IP Address
If opening /wp-json/ to the public on a staging site makes you uneasy, you can restrict access by developer IP address or webhook CIDR blocks instead. This keeps scrapers from pulling public user lists or draft taxonomies from the open API.
Here is how to allow unrestricted REST access exclusively to your office or home IP, while requiring everyone else to submit basic auth credentials:
AuthType Basic
AuthName "Private Developer Staging"
AuthUserFile /home/u123456789/.htpasswd # Allow developer static IP Require ip 203.0.113.45 # Allow local network or office subnet Require ip 198.51.100.0/24 # Allow anyone with credentials Require valid-user
If your team works remotely across dynamic residential IPs, keeping an IP whitelist updated quickly turns into a chore. The URI-based bypass is usually the practical choice. Remember, unauthenticated REST requests still respect WordPress permissions; visitors can only read content that would normally be public on the frontend.
Handling WP-Cron and Automated Background Tasks
WordPress staging sites often need to process scheduled actions, automated imports, or WooCommerce order syncs. If you lock down wp-cron.php behind Basic Auth, those scheduled jobs stall out silently.
If cron jobs keep failing, check our walkthrough on how to replace WP-Cron with a Linux system cron on Hostinger. In that setup, you can either trigger wp-cron.php directly via the CLI or pass your Basic Auth credentials directly into the curl command:
# Trigger cron using HTTP Basic Auth credentials
*/10 * * * * curl -s -u staginguser:yourpassword https://staging.example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1Because we included SetEnvIf Request_URI "^/wp-cron.php" allow_rest=1 in our main .htaccess block, server-side curl calls to wp-cron.php will pass through without needing credentials at all.
Complete Production-Ready .htaccess File
Here is the full .htaccess file putting all these pieces together. Place this at the top of your staging site’s root directory and update the path to your .htpasswd file:
# Pass Authorization Header to PHP-FPM
RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] # Support query string REST API requests RewriteCond %{QUERY_STRING} rest_route=^/?wp-json [NC] RewriteRule .* - [E=allow_rest:1]
# Match standard REST API and Cron paths
SetEnvIf Request_URI "^/wp-json/" allow_rest=1
SetEnvIf Request_URI "^/wp-cron.php" allow_rest=1 # Configure Basic Authentication
AuthType Basic
AuthName "Staging Area"
AuthUserFile /home/u123456789/.htpasswd Require env allow_rest Require valid-user
# BEGIN WordPress
RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L]
# END WordPressFor more details on how RequireAny evaluates conditional authorization blocks, check out the official Apache Module mod_authz_core documentation.
Frequently Asked Questions
Why does Gutenberg throw an “Updating failed” error when I save a post?
Gutenberg sends background fetch() calls to /wp-json/wp/v2/posts/. When Apache returns a 401 challenge, the browser refuses to complete the background call because it doesn’t automatically attach your Basic Auth session credentials. Whitelisting the /wp-json/ path fixes this immediately.
Does whitelisting /wp-json/ expose my staging database to hackers?
No. The WordPress REST API enforces its own internal capability checks. Modifying posts, viewing drafts, or changing settings requires a valid WordPress session cookie or an Application Password. An unauthenticated user hitting /wp-json/ can only see content that would already be public on your frontend.
Can I password protect individual files like wp-login.php instead?
Yes. If you only want to protect the login screen and admin area, wrap the auth directives inside a block. Just keep in mind this leaves your frontend content accessible to search engines unless you also send a noindex header.
What happens if I push this .htaccess file from staging to production?
Your live production site will prompt every visitor for a username and password. Always exclude .htaccess from automated deployment scripts, or set up your deploy pipeline to swap in production-specific configs on release.
Next up: If you run staging database migrations manually, check out our guide on how to safely update WordPress URLs with WP-CLI search-replace without corrupting PHP serialized strings.

