APR1 Is Apache's Own Format
The $apr1$ prefix marks Apache's variant of MD5-crypt: a salted, iterated MD5 designed in
the 1990s to be slow enough to resist the hardware of the time. It is what htpasswd
produces by default and what every Apache installation reads without configuration.
An entry is one line:
``
username:$apr1$SALT$HASHEDPASSWORD
`
Protecting a Directory
`apache
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
`
Three things go wrong most often:
1. The .htpasswd file is inside the web root, where it can be downloaded. Put it outside,
or deny access to it explicitly.
2. AllowOverride is not set, so a .htaccess file is ignored entirely and the
directory is not protected at all. Test with a private browser window before assuming.
3. The path is relative. AuthUserFile needs an absolute path.
APR1 Is Not a Modern Password Hash
Iterated MD5 is fast on modern hardware, which is exactly what a password hash must not be.
A GPU rig tries billions of MD5 operations a second, so an APR1 hash of a weak password falls
quickly.
Use it where it belongs: gating a staging site, a metrics dashboard, an internal tool β with
a long random password, over HTTPS, and with no expectation that the hash file could survive
being leaked. For real user accounts use bcrypt, scrypt or Argon2 in your application.
Basic Auth Sends Credentials Every Request
HTTP Basic transmits base64(user:password) in a header on every single request. Base64 is
encoding, not encryption β anyone who can read the traffic can read the password. Over HTTPS
that is acceptable; over HTTP it is a plaintext password on the wire, repeatedly.
Generated in Your Browser
The salt comes from crypto.getRandomValues() and the hash is computed locally. The
password never leaves your machine, which is the whole reason not to use an online generator
that posts it to a server.
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.