What a .htpasswd file actually is
A .htpasswd file is a flat text file with one account per line, in the form username:hash. There is no header, no schema, and no ordering requirement. The colon is the field separator, which is why a username may never contain one. When a request arrives with an Authorization: Basic header, the server base64-decodes the credentials, walks the file until it finds a matching username, and runs the supplied password through whatever algorithm the stored hash declares. The prefix on the hash is what selects the algorithm — $apr1$ means Apache MD5, $2y$ means bcrypt, {SHA} means unsalted SHA-1, and a bare 13-character string means traditional crypt(3) DES.
That prefix-driven dispatch is the reason a single file can mix algorithms freely. You can add a bcrypt line to a file full of $apr1$ lines and Apache will verify both correctly. It also means migrating is painless: rewrite one line at a time as users change their passwords, with no flag day.
The algorithms, and which to actually use
| Prefix | Algorithm | Salted | Verdict |
|---|---|---|---|
$2y$ | bcrypt | Yes | Best choice. Tunable cost, slow by design. Requires Apache 2.4+ / nginx built with crypt support. |
$apr1$ | Apache MD5 (APR1) | Yes, 8 chars | Safe fallback. 1000 MD5 rounds. Understood by every httpd, nginx, and Caddy build in existence. |
{SHA} | Unsalted SHA-1 | No | Legacy only. Identical passwords produce identical hashes; rainbow tables apply. |
{plain} / bare | Plaintext or crypt(3) DES | No / 2 chars | Never. DES crypt silently truncates the password to 8 characters. |
Read the full explanation of htpasswd generator results ↓Show less ↑
bcrypt is the right answer where your server supports it, and this page deliberately cannot generate it. bcrypt has no implementation in the browser standard library, and producing a real $2y$ hash would mean loading a third-party crypto bundle. Rather than ship something that might be subtly wrong, the tool offers the two algorithms it can compute exactly — and this page tells you the one command that produces bcrypt on any machine with the Apache utilities installed.
APR1 is a genuine second-best rather than a token gesture. It salts with 8 random characters and runs a 1000-iteration MD5 construction, which defeats rainbow tables outright and makes offline cracking meaningfully more expensive than a single hash. Its weakness is speed: modern GPUs try APR1 candidates far faster than bcrypt at a sane cost factor. For a staging site, an internal dashboard, or a temporary maintenance gate, APR1 is fine. For anything protecting real user data, use bcrypt and a long password.
How to read what the tool gives you
An APR1 line looks like deploy:$apr1$k3mQ8vZ1$OyVy2G0hXk9Jc0K7ZQhZE. — three dollar-delimited fields after the username: the magic string, the 8-character salt, and the 22-character digest in Apache's own base64-like alphabet (note it uses . and /, not + and /). Generate the same password twice and you will get two completely different strings. That is the salt doing its job, not a bug, and both will verify.
A SHA-1 line looks like deploy:{SHA}8rFPaOuZX6yzocNSh7d41b14VRE= — the literal token {SHA} followed by standard base64 of the raw 20-byte digest. Because there is no salt, two users with the same password get byte-identical hashes, which leaks information to anyone who reads the file.
Wiring it up
Store the file outside your document root. If .htpasswd sits next to index.html and the server is misconfigured for even a moment, it is downloadable. /etc/nginx/.htpasswd or /etc/apache2/passwd/site are conventional. Lock the permissions to 640 owned by root with the web-server group — the chmod calculator will show you exactly who that lets in.
location /admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}<Directory "/var/www/html/admin">
AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/passwd/site
Require valid-user
</Directory>The command-line equivalents
Everything this page does has a one-line shell equivalent, and on a server you should generally prefer it — there is no browser, no clipboard, and no copy-paste error in the loop. The -n flag prints the line to stdout instead of writing a file; -B selects bcrypt and -C sets its cost.
# bcrypt at cost 12 — the recommended option, printed to stdout
htpasswd -nbB -C 12 deploy 'correct horse battery staple'
# Apache MD5 (APR1), the same algorithm this page implements
htpasswd -nbm deploy 'correct horse battery staple'
# APR1 without Apache installed — openssl generates its own random salt
openssl passwd -apr1 'correct horse battery staple'
# Reproduce a specific hash to check a tool's output against a known salt
openssl passwd -apr1 -salt abcdefgh 'secret123'
# -> $apr1$abcdefgh$aQ26yFH6V5G5PJBY/utXg/
# Create or append to a real file (-c CREATES and TRUNCATES — omit it to append)
htpasswd -B /etc/nginx/.htpasswd deploy
htpasswd -D /etc/nginx/.htpasswd olduser # delete a user-c flag truncates the file. Running htpasswd -c to add a second user is the single most common way people delete every existing account. Only use -c for the very first user, then never again.Common failures and their fixes
- The browser prompts endlessly and never accepts the password. Almost always a bad path in
AuthUserFile/auth_basic_user_file, or a file the web-server user cannot read. Check the error log — Apache logs "could not open password file" plainly.sudo -u www-data cat /etc/nginx/.htpasswdproves readability. - A password with a `$` or `!` in it fails from the shell but works in the browser. Your shell expanded it. Single-quote the password when passing it to
htpasswd -b, or omit-band let the tool prompt you interactively. - Authentication works on one server and fails on another. You generated bcrypt on a host whose nginx was not built against a crypt implementation supporting
$2y$. Fall back to$apr1$, which every build understands. - Everyone can log in as everyone. A duplicate username. The first matching line wins and later ones are silently ignored — grep the file for duplicates before debugging anything else.
- The password stopped working after exactly 8 characters. You produced a crypt(3) DES hash (no
$prefix at all). DES crypt truncates. Regenerate with-Bor-m. - Auth prompt appears on assets, breaking the page. Basic auth applies per-location. Either protect the parent path so CSS and JS are covered too, or explicitly exclude them — a half-protected page loads unstyled.
Choosing the password itself
The hash algorithm sets the cost per guess; the password sets how many guesses are needed. A 12-character random password from a 90-character alphabet is roughly 78 bits — enough that even a fast algorithm like APR1 is not brute-forceable. A dictionary word with a digit appended is crackable against bcrypt at cost 12 in minutes. Generate the password with the password generator, which uses the browser CSPRNG and shows the entropy in bits so you can see what you are getting.
One last note on scope: Basic auth is an all-or-nothing gate with no session, no logout, and no rate limiting. Browsers cache the credentials for the rest of the session, so "logging out" means closing the browser. It is excellent for keeping crawlers and casual visitors out of a staging site, and a poor substitute for a real application login.