Skip to content

A .htpasswd line, built in your browser

Basic auth for Apache and nginx reads a file of username and hash pairs, one per line. This builds that line, in any of the four formats the two servers accept. The bcrypt option is the same algorithm as the rest of this site, written with the $2y prefix that Apache expects.

Everything below this line runs locally

The strongest option, and the one Apache documents as its bcrypt format. Apache reads it from 2.4.4 onward on every platform. nginx passes the hash to the system crypt(), which knows bcrypt on libxcrypt (Debian 11+, Ubuntu 20.04+, RHEL 8+, Fedora 30+) and on musl, so on any current Linux it works.

Wiring it up

Save the line into a file the server can read, then point the server at it. Put the file somewhere the web root does not reach, otherwise anyone can download your hashes.

Apache

<Directory "/var/www/private">
    AuthType Basic
    AuthName "Restricted"
    AuthUserFile /etc/apache2/.htpasswd
    Require valid-user
</Directory>

nginx

location /private/ {
    auth_basic           "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

The four formats, and what they cost you

bcrypt
The strongest option, and the one Apache documents as its bcrypt format. Apache reads it from 2.4.4 onward on every platform. nginx passes the hash to the system crypt(), which knows bcrypt on libxcrypt (Debian 11+, Ubuntu 20.04+, RHEL 8+, Fedora 30+) and on musl, so on any current Linux it works.
SHA-512 crypt
The fallback when bcrypt is not available. Apache writes it with htpasswd -5 and calls it secure; nginx gets it from glibc, which has had it since 2007, so it works on every Linux including the ones whose crypt() has no bcrypt. Not on Windows, and weaker than bcrypt at the default round count.
apr1 (MD5)
Apache’s own MD5 variant. Weak by modern standards, but it is the one format that works everywhere, including nginx on any platform.
SHA-1
Unsalted and fast, which makes it the worst of the three for passwords. Present only because some older setups still expect it.

The apr1 format is Apache's own variant of md5crypt: a thousand MD5 rounds over the password and salt. That was a reasonable amount of work in 1996 and is not one now, so treat it as compatibility rather than protection, and use bcrypt where you can.

SHA-512 crypt sits between the two. It runs five thousand SHA-512 rounds by default, which you can raise, and it is the only format here besides apr1 that a stock glibc will read. What it does not give you is bcrypt's resistance to being run in parallel on a graphics card, so more rounds buy less than the same effort spent on bcrypt would. Reach for it when bcrypt is not on the table, not instead of it.

Doing it on the command line instead

If you have Apache's tools installed, htpasswd does the same job. The -B flag selects bcrypt and -C sets the cost; -5 selects SHA-512 crypt and -r sets its rounds.

# create the file and add the first user
htpasswd -cB -C 12 /etc/apache2/.htpasswd deploy

# add another user to an existing file
htpasswd -B -C 12 /etc/apache2/.htpasswd ci

# print a line without touching any file
htpasswd -nbB -C 12 deploy 'the password'

# SHA-512 crypt instead, for a server that cannot read bcrypt
htpasswd -nb5 -r 100000 deploy 'the password'

Watch the -c flag. It creates a new file and will overwrite an existing one along with every user already in it.

Questions people ask

Which format should I pick?
Bcrypt if your server supports it, which Apache has done since 2.4.4 and nginx does wherever the system crypt knows about it. SHA-512 crypt is the next best thing and covers the Linux boxes bcrypt misses. Fall back to apr1 only when you need a format that also works on Windows, and avoid SHA-1, which is unsalted and fast.
When would I use SHA-512 crypt over bcrypt?
When the server cannot read bcrypt. nginx passes unknown hashes to the system crypt(), and while every current Linux handles bcrypt there, the crypt() built into glibc itself never did. SHA-512 crypt has been in glibc since 2007, so it works everywhere bcrypt might not, and Apache generates it with htpasswd -5. It is still the weaker of the two at equal effort, so it is a fallback rather than a first choice.
Does nginx support bcrypt in htpasswd?
nginx handles apr1, {SHA} and {SSHA} itself and hands everything else to the system crypt(), so bcrypt depends on the C library rather than on nginx. libxcrypt, which is what Debian 11+, Ubuntu 20.04+, RHEL 8+ and Fedora 30+ ship, supports it, and so does musl on Alpine and the BSDs. The gap is the crypt() built into glibc itself, which never had bcrypt. If you are on something that old, apr1 is the safe choice.
Why does the bcrypt line say $2y and not $2b?
Because Apache will not read $2b. Its documentation defines the bcrypt format as $2y followed by the crypt_blowfish output, and APR only matches $2a, $2x and $2y, so a $2b line fails to authenticate even though the hash behind it is fine. The prefix is a label, not a different algorithm: for any password inside bcrypt’s 72 byte limit, $2b and $2y produce identical digest bytes, so rewriting the marker costs nothing and is what Apache’s own htpasswd -B writes.
Where does the file go?
Anywhere the web server can read and the public cannot. Outside the document root is the point. Common locations are /etc/apache2/.htpasswd and /etc/nginx/.htpasswd, and the server is told the path in its own config rather than finding it automatically.
How do I add a second user?
One user per line, in the same file. Generate another line here and append it. Order does not matter, and a duplicate username means the first line wins, so remove the old one when you change a password.
Is basic auth good enough on its own?
It sends credentials on every request, encoded but not encrypted, so it needs HTTPS to be meaningful at all. Over TLS it is reasonable for locking down a staging site or an internal tool, and it is not a substitute for real user accounts in an application.

Next