...
Same thing with the htpasswd command
nginx
Apache
Caddy
This htpasswd generator gives you a ready-to-paste line for an Apache or nginx password file, plus the config to use it. I mostly reach for Basic auth to keep search engines and curious people off staging sites and internal dashboards, and I got tired of SSHing into a box just to run htpasswd once.
Every format this page produces was checked the boring way: lines generated by the page's JavaScript went through htpasswd -vb from the official Apache 2.4 image, and through live Apache, nginx and Caddy containers with curl, with right and wrong passwords.
What an htpasswd file is
It's a plain text file with one user per line: the username, a colon, and a password hash.
alice:$2y$10$i87b9pQ8IA9xbs77edEOLO3FZUmdb3u5Xwj3YeT1KypPM2jaflKoS
bob:$apr1$01mc.hrQ$5oUulTIt33wfR.I9Nbsxz.
The hash tells the server which algorithm to use by its prefix: $2y$ (or $2a$, $2b$) for bcrypt, $apr1$ for Apache's MD5 variant, {SHA} for unsalted SHA-1. Usernames can't contain a colon, because the first colon ends the username; htpasswd refuses one, and it refuses usernames over 255 bytes too. nginx's docs also allow comment lines starting with #.
The server compares what the browser sends against that file. That's all Basic auth is: no sessions, no lockouts, no password reset. Simple is the appeal.
Which hash format to pick
| Format | Looks like | Verdict |
|---|---|---|
| bcrypt | $2y$10$... | Use this. Salted and deliberately slow. htpasswd -B. |
| apr1 (MD5) | $apr1$salt$... | Legacy. Salted, but 1,000 rounds of MD5 are cheap to attack. Still what htpasswd uses if you give it no flag. htpasswd -m. |
| SHA-1 | {SHA}... | Weak. Unsalted, fast, and identical passwords give identical lines. Only for old tools that read nothing else. htpasswd -s. |
I left out two things on purpose. Old DES crypt() only looks at the first 8 characters of the password, so it has no business protecting anything. And plaintext passwords in the file (htpasswd -p, which its own help says probably won't work outside Windows and NetWare) are exactly what hashing is meant to prevent. Newer htpasswd builds (2.4.68 in my test) also offer SHA-256 and SHA-512 crypt with -2 and -5; those depend on the system's crypt library, so I stuck to formats I could check everywhere.
Which servers accept which formats
Apache understands all three itself, through its APR library, so a line from this page works on any Apache 2.4 regardless of the operating system. I tested bcrypt, apr1 and SHA-1 lines in httpd:2.4 with both htpasswd -vb and a real protected directory.
nginx is the one with a catch. Its auth_basic_user_file documentation says it handles the apr1 MD5 variant and the {SHA} scheme itself (it also warns plain SHA-1 should not be used), and anything else is passed to the system's crypt() function. So bcrypt support comes from your operating system, not nginx. What I found:
- The official
nginxDocker image (Debian) andnginx:alpineboth accepted$2y$,$2b$and$2a$bcrypt lines, and rejected wrong passwords. - The system
crypt()on CentOS 7 and Debian 10 returned nothing at all for a bcrypt hash, so nginx on systems like those has no way to check a bcrypt line.
Current distributions generally ship a crypt library with bcrypt support, but if you're on something old, or nginx logs crypt_r() failed or password mismatch for a password you know is right, fall back to apr1 and plan an upgrade.
Caddy doesn't read htpasswd files. Its basic_auth directive (called basicauth before Caddy 2.8) takes username hash pairs inline, and the hash must be bcrypt, the default, or argon2id. Caddy refuses plaintext, and caddy hash-password will hash one for you. The bcrypt hash from this page drops straight in; I confirmed a $2y$ line from the generator logs in on the current caddy image.
bcrypt cost matters more for Basic auth
A login form hashes your password once and hands you a session cookie. Basic auth doesn't have sessions. The browser sends the username and password with every request, and the server checks the hash every single time: the HTML, each stylesheet, each image, each API call from the page.
nginx has no cache for this. I measured it on the nginx Docker image with bcrypt lines from this page: a request without credentials came back in about 2 ms, while an authenticated request took about 9 ms at cost 5, 95 ms at cost 10 and 360 ms at cost 12 on my desktop. A dashboard that loads forty assets at cost 12 spends more than ten seconds of CPU on password checks. Apache can cache successful checks with mod_authn_socache, which isn't loaded by default in the official image.
That's why htpasswd -B still defaults to cost 5 while login-form advice says 10 or more. My middle ground is cost 10 with a long random password (the generate password button makes 24 characters, about 148 bits). With that much entropy the password isn't guessable at any cost, so if pages feel sluggish, dropping the cost is a reasonable trade. The bcrypt generator page goes deeper on cost factors in general.
Setting it up in nginx
Create the file outside your web root. nginx has no rule hiding dotfiles, so an .htpasswd inside root can be downloaded like any other file.
sudo htpasswd -B -C 10 -c /etc/nginx/.htpasswd alice
sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd
Or paste the line from this page into that file with your editor. Replace www-data with whatever user your nginx workers run as (on the official Docker image it's nginx). Then protect a location:
server {
listen 443 ssl;
server_name staging.example.com;
location / {
auth_basic "Staging";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:8080;
}
}
sudo nginx -t && sudo systemctl reload nginx
One trap I walked straight into while testing this page: a return directive in the same location runs before the auth check, so location / { auth_basic "x"; auth_basic_user_file ...; return 200 "ok"; } answers every request with 200, wrong password or not. Test with a wrong password before you trust it:
curl -s -o /dev/null -w "%{http_code}\n" https://staging.example.com/ # 401
curl -s -o /dev/null -w "%{http_code}\n" -u alice:wrong https://staging.example.com/ # 401
curl -s -o /dev/null -w "%{http_code}\n" -u alice https://staging.example.com/ # prompts, then 200
Setting it up in Apache
<Directory "/var/www/staging">
AuthType Basic
AuthName "Staging"
AuthBasicProvider file
AuthUserFile "/etc/apache2/.htpasswd"
Require valid-user
</Directory>
Those same five directives work in an .htaccess file if the directory allows it with AllowOverride AuthConfig, though I prefer the main config: it's read once instead of on every request, and it isn't sitting in the web root. The stock Apache config blocks requests for files starting with .ht, which is why the file has that name, but keeping it outside the document root is still the better habit. Reload with sudo apachectl -t && sudo systemctl reload apache2 (the service is httpd on Red Hat family systems).
Setting it up in Caddy
staging.example.com {
basic_auth {
alice $2y$10$i87b9pQ8IA9xbs77edEOLO3FZUmdb3u5Xwj3YeT1KypPM2jaflKoS
}
reverse_proxy 127.0.0.1:8080
}
Or let Caddy hash it: caddy hash-password reads the password from the terminal and prints a bcrypt hash (cost 14, $2a$ prefix, in my test) you can paste into the block. Unlike nginx and Apache, the hashes live in the Caddyfile, so reload Caddy after changing them. Caddy handles HTTPS on its own, which removes the biggest Basic auth mistake from the table.
Keep Basic auth safe
- HTTPS only. The
Authorizationheader is just base64 ofusername:password. Anyone who can see plain HTTP traffic can decode it in one line. RFC 7617 says Basic auth should not protect sensitive information without something like HTTPS. I'd skip the "sensitive" qualifier: no HTTPS, no Basic auth. - File permissions. The file should be readable by the web server and root, and nobody else.
640owned byrootwith the web server's group is typical. A world-readable hash file lets any local user start guessing offline. - One person, one line. Shared logins mean you can't remove one person without changing it for everyone. Each person gets a username, and
htpasswd -D /etc/nginx/.htpasswd bobremoves Bob the day he leaves. - Watch the
-cflag. It creates a new file, overwriting whatever was there. Use it once, then leave it off when adding users. - Rotate. Running
htpasswd -Bagainst an existing username replaces that line. Do it when someone leaves, when a password might have been shared in chat, and on a schedule you'll actually keep. Browsers usually keep sending the credentials they remember until the browser is closed, so expect a fresh login prompt for anyone whose password changed. - Unique, random passwords. There's no lockout, so rate limiting is up to you, and a reused password is one breach away from working here. Use the generator above or the strong random password generator and put it in a password manager.
- Stick to ASCII passwords. RFC 7617 leaves the character encoding unspecified unless the server asks for UTF-8, so a password with accents or emoji may be sent differently by different clients. The generator here only uses ASCII.
- Know what it is not. Basic auth has no second factor and no logout. For anything with real user accounts, use real application auth.
If you're protecting an API used by other programs rather than people, a Basic auth password is the wrong tool; give each client an API key and store only its SHA-256. For signing sessions and tokens there's the secret key generator.
Generating an htpasswd line without this page
With Apache's utilities installed (apache2-utils on Debian and Ubuntu, httpd-tools on Red Hat family systems):
# print a line instead of writing a file (prompts for the password)
htpasswd -nB -C 10 alice
# check a password against an existing file
htpasswd -v /etc/nginx/.htpasswd alice
Without Apache's tools, OpenSSL can make an apr1 line, and Python's bcrypt package can make a bcrypt one:
printf "alice:%s\n" "$(openssl passwd -apr1)" >> .htpasswd
python3 -c 'import bcrypt, getpass; print("alice:" + bcrypt.hashpw(getpass.getpass().encode(), bcrypt.gensalt(10)).decode())'
Both prompt for the password so it doesn't end up in your shell history. Avoid htpasswd -b with the password as an argument on shared machines for the same reason.
htpasswd FAQ
Which htpasswd hash format should I use?
bcrypt. Apache, the official nginx Docker images and Caddy all accept it, and it is the only format of the three here that is deliberately slow. Use apr1 (MD5) only for an old server whose crypt library has no bcrypt support, and {SHA} only when some legacy tool reading the file understands nothing else.
Is this htpasswd generator safe to use with a real password?
The line is built in your browser: bcrypt through the bcryptjs library, SHA-1 through the Web Crypto API, and apr1 through a small MD5 library, with salts from crypto.getRandomValues. The page sends nothing to the server, which you can check in your browser's network tab. If you would rather no web page ever sees the password, run the htpasswd command shown under the output on your own machine.
Does nginx support bcrypt in auth_basic_user_file?
nginx handles apr1 and {SHA} itself and hands other formats to the operating system's crypt() function, so bcrypt works only where the system crypt library supports it. It worked on the official nginx Docker images, both Debian and Alpine, when I tested them, and the crypt() on CentOS 7 and Debian 10 did not recognize bcrypt at all. Test a login after deploying.
How do I add another user to an existing htpasswd file?
Append a new line with a different username, or run htpasswd without the -c flag, for example htpasswd -B /etc/nginx/.htpasswd bob. The -c flag creates a new file and overwrites any existing one, which is the classic way to delete every other user by accident.
How do I change a password or remove a user?
Run htpasswd -B on the file with the same username and it replaces that user's line. htpasswd -D file username deletes the user. You can also edit the file by hand, one user per line. nginx and Apache both read the file on each request, so the change applies without a reload; Caddy keeps hashes in its config, so reload Caddy.
Is HTTP Basic authentication secure?
Only over HTTPS. Basic auth sends the username and password base64 encoded, which is not encryption, on every request, and RFC 7617 says it should not be used to protect sensitive information without something like HTTPS. With TLS and a long random password it is a reasonable lock for a staging site or an internal tool, but it has no lockout, no second factor and no real logout.
What bcrypt cost should I use for an htpasswd file?
Basic auth checks the password on every request, not once per login, so the cost multiplies across every image and script on a protected page. htpasswd defaults to cost 5. I use 10 as a middle ground and measure: if protected pages feel slow, lower it, and keep the password long and random so a low cost does not matter much.
Why does the {SHA} hash come out the same every time?
The {SHA} format is plain unsalted SHA-1 of the password. The same password always produces the same line, a leaked file can be checked against precomputed tables, and SHA-1 is fast to guess. bcrypt and apr1 add a random salt, so they change every time you generate.
Related tools: bcrypt generator, strong random password generator, API key generator, secret key generator, and passphrase generator.
-Sethers