HTTP Basic Authentication is often treated as the default quick-fix for staging environments, internal metric dashboards, and private webhook endpoints. On the surface, the mechanism seems trivial: an incoming Authorization: Basic <base64> header is compared against a list of username-to-hash pairs in an .htpasswd file.
Yet behind this simplicity lies a maze of legacy cryptographic quirks, shell escaping pitfalls, and reverse proxy edge cases that routinely cause production authentication outages or silent security bypasses. Here are the five most common .htpasswd traps and how to avoid them.
1. The 8-Byte Silent Truncation Trap (DES / crypt)
If your deployment scripts invoke legacy htpasswd binaries or use the -d flag (traditional Unix crypt()), passwords are silently truncated after the first 8 bytes.
# Generated with DES crypt:
admin:ab91sU7Xm.YfQ
In this mode, SuperSecretPassword2026! and SuperSec produce the exact same hash. Any attacker who guesses the first 8 characters gains instant access.
The fix: Always use Apache MD5 ($apr1$) or modern Bcrypt ($2y$). Never use DES crypt (-d) or plain unsalted MD5.
2. Algorithm Incompatibilities Across Web Servers
The .htpasswd format supports several distinct hashing algorithms, but server implementations differ significantly:
-
Apache MD5 (
$apr1$): Standard and universally supported across Apache, Nginx, and Traefik. It runs 1,000 iterative rounds of MD5 with an 8-character salt. -
SHA-1 (
{SHA}): Base64-encoded raw SHA-1 digest (e.g.user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=). Crucially, it is completely unsalted. An attacker with access to the hash file can reverse it instantly using precomputed rainbow tables. -
Bcrypt (
$2y$or$2a$): High-security adaptive hash. Supported by Apache 2.4+ and modern Nginx (compiled with OpenSSLcrypt_r), but older microservices and lightweight embedded proxies may fail to parse it.
If you need to quickly inspect existing hashes or generate compatible APR1 and SHA credentials without installing apache2-utils locally, you can use the Nutilz htpasswd generator to inspect hash formats and generate multi-user configurations client-side.
3. Delimiter Collisions and Dollar-Sign Expansion
The .htpasswd file uses a strict colon-delimited format: username:password_hash.
-
Username Colons and Spaces: If a username contains a colon (
dev:ops) or leading whitespace, parsers treat the first colon as the delimiter, immediately corrupting the hash field. -
Bash & Docker Compose
$APR1$Expansion: Because APR1 hashes begin with$apr1$and SHA-512 hashes begin with$6$, passing.htpasswdstrings through Docker Compose environment variables or bash scripts triggers shell variable substitution.
# Broken in docker-compose.yml:
HTPASSWD_CONTENT: "admin:$apr1$salt$hash" # Evaluates $apr1 and $salt as empty variables!
# Fixed (escape dollar signs):
HTPASSWD_CONTENT: "admin:$$apr1$$salt$$hash"
4. Placing .htpasswd Inside the Document Root
A classic configuration vulnerability occurs when .htpasswd is stored in the same directory as public assets (e.g., /var/www/html/.htpasswd). If your web server configuration lacks an explicit block for hidden files, anyone can download your password hashes over HTTP:
# Vulnerable Nginx setup: allows GET /.htpasswd
location / {
root /var/www/html;
auth_basic "Restricted";
auth_basic_user_file /var/www/html/.htpasswd;
}
# Secure configuration: deny access to dotfiles
location ~ /\.(?!well-known).* {
deny all;
access_log off;
log_not_found off;
}
Best practice: Always store .htpasswd outside the web root (e.g. /etc/nginx/.htpasswd or /etc/apache2/.htpasswd).
5. Reverse Proxy Header Stripping and 401 Caching
When running behind API gateways, CDNs, or load balancers (such as Cloudflare, AWS ALB, or Kubernetes Ingress), the Authorization header may be stripped before reaching the origin server.
In Nginx reverse proxies, ensure headers are explicitly forwarded if upstream handles basic auth:
location /internal-api/ {
proxy_pass http://upstream_backend;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
Furthermore, browsers cache HTTP Basic Auth credentials indefinitely for a given realm. To force a logout, your application must respond with an explicit 401 Unauthorized with a different WWW-Authenticate: Basic realm="NewRealm" header.
Summary Checklist
- Avoid DES (
-d) and unsalted SHA-1 ({SHA}). -
Use
$apr1$or$2y$(Bcrypt) for broad compatibility and cryptographic resilience. -
Escape
$symbols in CI/CD pipelines, Docker Compose, and Kubernetes manifests. -
Store
.htpasswdoutside/var/www/and verify dotfile denial rules. - Inspect and validate your hash lines before deployment using tools like Nutilz htpasswd generator to catch syntax and algorithm mismatches early.
Top comments (0)