---
title: "htpasswd Generator: bcrypt Lines for nginx, Apache, Caddy"
description: "Create htpasswd lines with bcrypt, apr1 or SHA-1 plus nginx, Apache and Caddy basic auth config. Built in your browser; nothing is sent to the server."
image: "https://cdn.sethserver.com/blog-images/htpasswd-generator-social-1789657055.webp"
---

# htpasswd Generator: bcrypt Lines for nginx, Apache, Caddy

Username

Password

Hash format

bcrypt cost

...

Built in this tab. Nothing is sent to the server.

### 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 `nginx` Docker image (Debian) and `nginx:alpine` both 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](https://www.sethserver.com/security/bcrypt-generator.html) 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 `Authorization` header is just base64 of `username: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. `640` owned by `root` with 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 bob` removes Bob the day he leaves.
- **Watch the `-c` flag.** It creates a new file, overwriting whatever was there. Use it once, then leave it off when adding users.
- **Rotate.** Running `htpasswd -B` against 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](https://www.sethserver.com/security/strong-random-password-generator.html) 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](https://www.sethserver.com/security/api-key-generator.html) and store only its SHA-256. For signing sessions and tokens there's the [secret key generator](https://www.sethserver.com/security/secret-key-generator.html).

## 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](https://www.sethserver.com/security/bcrypt-generator.html), [strong random password generator](https://www.sethserver.com/security/strong-random-password-generator.html), [API key generator](https://www.sethserver.com/security/api-key-generator.html), [secret key generator](https://www.sethserver.com/security/secret-key-generator.html), and [passphrase generator](https://www.sethserver.com/security/strong-random-passphrase-generator.html).

-Sethers


```json
{"@context": "https://schema.org", "@graph": [{"@type": "WebApplication", "name": "htpasswd Generator", "url": "https://www.sethserver.com/security/htpasswd-generator.html", "description": "Generate htpasswd lines with bcrypt, apr1 MD5 or SHA-1 in your browser, plus matching nginx, Apache and Caddy basic auth config. Nothing is sent to a server.", "applicationCategory": "DeveloperApplication", "operatingSystem": "Web", "author": {"@type": "Person", "name": "Seth Black"}, "offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"}}, {"@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "Which htpasswd hash format should I use?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "Is this htpasswd generator safe to use with a real password?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "Does nginx support bcrypt in auth_basic_user_file?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "How do I add another user to an existing htpasswd file?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "How do I change a password or remove a user?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "Is HTTP Basic authentication secure?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "What bcrypt cost should I use for an htpasswd file?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}, {"@type": "Question", "name": "Why does the {SHA} hash come out the same every time?", "acceptedAnswer": {"@type": "Answer", "text": "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."}}]}]}
{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "sethserver", "item": "https://www.sethserver.com/"}, {"@type": "ListItem", "position": 2, "name": "Tools", "item": "https://www.sethserver.com/tools/"}, {"@type": "ListItem", "position": 3, "name": "htpasswd Generator: bcrypt Lines for nginx, Apache, Caddy", "item": "https://www.sethserver.com/security/htpasswd-generator.html"}]}
```
