nginx Reads htpasswd Files
nginx's auth_basic_user_file uses the same format Apache does β one user:hash line
per entry. It supports APR1 ($apr1$) and {SHA} on every build; bcrypt support
depends on how nginx was compiled, which is why APR1 remains the safe default here.
Configuration
``nginx
server {
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://app;
}
# Leave health checks and ACME open, or you lock out your own monitoring
location = /healthz { auth_basic off; proxy_pass http://app; }
location /.well-known/ { auth_basic off; root /var/www; }
}
`
The two exceptions matter. A basic-auth gate over the whole server blocks Let's Encrypt
renewals and every uptime probe, and you find out when the certificate expires.
File Placement
`bash
sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd
`
Never place it under a served root. If /var/www/html/.htpasswd exists, someone can
download your hashes and attack them offline at their leisure.
Only Over TLS
Basic auth sends Authorization: Basic base64(user:pass) on every request. Base64 is
encoding, not encryption β anyone on the network path reads the credentials directly. Over
HTTPS it is acceptable for a staging gate or an internal tool; over HTTP it is equivalent to
publishing the password.
What Basic Auth Cannot Do
| Missing | Consequence |
|---|---|
| Logout | The browser caches credentials until it is closed |
| Lockout / rate limiting | Add limit_req yourself, or it is brute-forceable |
| MFA | No second factor exists in the protocol |
| Per-user audit | The access log has the username and nothing else |
It is a gate, not an authentication system. Use it to keep staging out of Google, not to
protect user accounts.Testing It Works
`bash
# Should return 401
curl -I https://staging.example.com/
# Should return 200
curl -I -u admin:password https://staging.example.com/
# What the browser actually sends
printf 'admin:password' | base64
# YWRtaW46cGFzc3dvcmQ= β Authorization: Basic YWRtaW46cGFzc3dvcmQ=
`
That last line is the whole security model: base64, no key, reversible by anyone who sees
the header. Over TLS it is fine; over plain HTTP it is equivalent to publishing the password.
Keeping Machines Out but Letting the Right Ones In
A blanket auth gate breaks things you rely on. Exempt them explicitly:
| Path | Why |
|---|---|
/.well-known/acme-challenge/ | Let's Encrypt renewal fails otherwise, silently, until the certificate expires |
/healthz | Load balancer and uptime probes |
/api/webhooks/ | Third-party callbacks cannot authenticate |
Blocking Crawlers Properly
Basic auth keeps a staging site out of search results, and it is worth pairing with the
other two signals so that nothing leaks through a link:
`
# robots.txt
User-agent: *
Disallow: /
`
`
X-Robots-Tag: noindex, nofollow
`
Note that robots.txt alone does not prevent indexing β it prevents crawling, and a
URL linked from elsewhere can still appear in results without a snippet. The noindex
header is what actually removes it.
CI and Scripts
`bash
# curl
curl -u "$STAGING_USER:$STAGING_PASS" https://staging.example.com/
# Playwright
await context.setHTTPCredentials({ username, password });
``
Keep the credentials in secrets, never in the repository β a password committed to git stays in the history after you delete it.