Skip to content
Back to Blog
Security

How to Create an .htpasswd File (HTTP Basic Auth Guide)

Create an .htpasswd file with bcrypt or apr1, configure HTTP Basic Auth on Apache, nginx, Docker and Kubernetes, then lock it down. Hands-on 2026 guide.

11 min read

How to Create an .htpasswd File (HTTP Basic Auth Guide)

An .htpasswd file is the server-side credential store for HTTP Basic Authentication: a plain text file where each line is one username:hash pair. To create an .htpasswd file, you generate that hashed line and save it somewhere your web server can read. There are three ways to do it:

  • The htpasswd command (from apache2-utils / httpd-tools), the canonical tool.
  • openssl passwd, already installed almost everywhere, no extra package.
  • In your browser: use the htpasswd generator to create an entry locally with zero installs and nothing sent over the network.

This guide goes past the one-liner. We cover how the Basic Auth handshake works, how to create the file three ways, which of the five hash formats to pick, how to wire it into Apache, nginx, Docker, Kubernetes, Caddy, and Traefik, and how to lock it down so you don’t ship a credential file that anyone can download.

What Is an .htpasswd File?

Every line in an .htpasswd file holds one user’s credentials as a colon-separated pair. The username is stored as-is; the password is stored only as a one-way hash, so the plaintext is never written to disk. The anatomy of a single bcrypt line looks like this:

admin    :    $2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│             │
└─ username   └─ hash (algorithm prefix $2y$ + cost + salt + digest)

The username comes first, then a single :, then the hash. A username can be up to 255 bytes and must never contain a colon, since the colon is the field separator. The hash carries its own algorithm marker as a prefix ($2y$ for bcrypt, $apr1$ for Apache MD5, {SHA} for SHA-1), so the server knows how to verify it with no extra configuration.

For multiple users, you add one line per user:

admin:$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
alice:$2y$10$3bQ8xY7tLp2mZ0xW5cR4fO9vK1jH6sD2nG8aQ5wE3rT7uI4oP1cm
bob:$apr1$mZ0xW5cR$4fK1jH6sD2nG8aQ5wE3rT2

Algorithms can be mixed in the same file. The server reads each line, detects the format from its prefix, and verifies against whatever was used. Here two bcrypt users and one apr1 user coexist without issue.

.htpasswd vs .htaccess

These two get confused constantly because they travel together on Apache, but they do different jobs. .htaccess is Apache’s per-directory configuration file. It holds directives, including the ones that turn on Basic Auth and point at your credential store. .htpasswd is the credential database: just the username:hash lines, no configuration.

In short: .htaccess decides that a directory requires a login and where to find the user list; .htpasswd is that user list. nginx doesn’t use .htaccess at all. Its Basic Auth configuration lives in the server or location block of the main config, but it reads the same .htpasswd credential format.

How HTTP Basic Authentication Works

HTTP Basic Authentication is a challenge-response handshake built into the HTTP spec. Once you know the three steps, most later troubleshooting becomes obvious:

  1. The client requests a protected resource with no credentials.
  2. The server replies 401 Unauthorized and includes a WWW-Authenticate: Basic realm="..." header. This is the challenge.
  3. The client retries the request with an Authorization: Basic <base64(user:password)> header. If the credentials match a line in the .htpasswd file, the server returns the resource.

That’s the entire protocol. There’s no login form, no session cookie, no token. Every subsequent request carries the same header.

The 401 / WWW-Authenticate challenge

The WWW-Authenticate header does two things. Its Basic token tells the client which scheme to use, and its realm string labels the protection space. Browsers show the realm text in the login dialog (“The site says: …”) and use it as the cache key: credentials entered for one realm are reused for other URLs in the same realm, so the user isn’t prompted again on every page.

Here is the raw exchange, captured with curl -i:

$ curl -i https://example.com/admin/
HTTP/2 401
www-authenticate: Basic realm="Restricted Area"

$ curl -i -u admin:s3cret https://example.com/admin/
HTTP/2 200

The Authorization: Basic header

The credential the client sends is base64(username:password). This is the most important security fact about Basic Auth: base64 is encoding, not encryption. Anyone can reverse it, so the credential travels in effectively plaintext form. You can see the round trip yourself:

# Encode the credential the way a browser does
printf 'admin:s3cret' | base64
# → YWRtaW46czNjcmV0

# Anyone who captures the header can decode it instantly
printf 'YWRtaW46czNjcmV0' | base64 -d
# → admin:s3cret

That reversibility is why Basic Auth must run over HTTPS. Without TLS, the password is readable by anyone on the network path. To build or inspect that header by hand, the Base64 encoder/decoder does the same user:password transformation in the browser.

How to Create an .htpasswd File

There are three practical ways to create the file. Pick based on what’s installed and where you want the password to live.

Using the htpasswd command

The htpasswd binary ships in Apache’s utility package. Install it first:

# Debian / Ubuntu
sudo apt install apache2-utils

# RHEL / CentOS / Fedora
sudo yum install httpd-tools

Create the file and its first user. The -c flag means create, and it will overwrite an existing file, so use it only the very first time:

htpasswd -c /etc/nginx/.htpasswd admin
# prompts twice for the password, then writes the file

To add more users, drop the -c so you append instead of clobbering:

htpasswd /etc/nginx/.htpasswd alice

To force bcrypt instead of the platform default, add -B. To print the entry to stdout without touching any file (handy for piping into config or a Dockerfile), combine -b (password on the command line) and -n (no file):

htpasswd -Bbn admin 's3cret'
# → admin:$2y$10$N9qo8uLOickgx2ZMRZoMye...

The flags you’ll actually use:

FlagMeaning
-cCreate a new file (overwrites if it exists); first user only
-BUse bcrypt
-bTake the password as a command-line argument (no prompt)
-nPrint to stdout instead of writing a file
-DDelete the named user from the file

One caveat with -b: the password ends up in your shell history. For one-off production credentials, prefer the prompted form or the browser option below.

Without apache2-utils: using OpenSSL

No htpasswd binary? OpenSSL is on virtually every system and can produce an apr1 hash directly. Wrap it in printf to build a complete line:

printf "admin:$(openssl passwd -apr1 's3cret')\n" >> /etc/nginx/.htpasswd
# admin:$apr1$k3l4Hj9.$qN8vY7tLp2mZ0xW5cR4f.

The apr1 format is portable across both Apache and nginx, which makes this the most dependency-free route on a minimal box.

In the browser: no install, no leak

If you don’t want to install a package, or you’d rather not type a production password into a shell where it lands in ~/.bash_history, generate the entry client-side. The htpasswd generator computes bcrypt, apr1, and SHA-1 hashes entirely in your browser, hands you a ready-to-paste user:hash line plus a matching server config block, and never transmits anything. While you’re there, generate a strong, unique password with the random password generator rather than reusing one.

htpasswd Password Formats Compared

The htpasswd command can emit five formats, and they are not equal. This table is the quick reference for picking one:

FormatPrefixSaltedStrengthUse it for
bcrypt$2y$YesStrongestApache, Docker Registry, Caddy, Traefik
apr1 (Apache MD5)$apr1$YesModeratenginx (portable, safe default)
SHA-1{SHA}NoWeakLegacy compatibility only
crypt (DES)(none)Yes (2-char)Very weakDon’t use
plain(none)NoNoneLocal testing only

A few notes that don’t fit a table cell. bcrypt uses a random 16-byte salt and an adaptive cost factor (default 10, modern recommendation 12), so identical passwords produce different hashes and the work factor scales with hardware. Its one quirk is that bcrypt truncates the password at 72 bytes, so anything longer is silently ignored. apr1 runs 1,000 rounds of salted MD5. That’s far weaker than bcrypt, but both Apache and nginx implement it natively, which is why it’s the portable choice. SHA-1 is unsalted, so identical passwords yield identical digests and rainbow tables apply; keep it for legacy systems only. crypt and plain exist for historical and testing reasons, and neither belongs in production.

The $2a$ / $2b$ / $2y$ prefixes

You’ll see bcrypt hashes start with $2a$, $2b$, or $2y$. They’re the same algorithm and produce equivalent, interchangeable hashes; the version letters are leftovers from old bug fixes in how certain libraries handled high-bit characters and string length. Apache’s htpasswd emits $2y$, and Caddy, Traefik, and Docker Registry all verify it correctly.

For a deeper comparison of bcrypt against modern alternatives, the bcrypt vs Argon2 vs scrypt guide covers how these password-hashing algorithms differ in cost, memory hardness, and threat model.

Configuring Basic Auth on Your Server

A credential file does nothing on its own; your server has to be told to require it. Here are six platforms.

Apache (.htaccess)

Drop this into a .htaccess file in the directory you want to protect (or into a <Directory> block in your vhost):

AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user

AuthName is the realm string the browser shows; AuthUserFile is the absolute path to your credential file; Require valid-user accepts any user listed in it.

nginx (auth_basic)

nginx puts the configuration in a location or server block; there is no .htaccess:

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

Reload with nginx -s reload. Use the apr1 format here. nginx delegates bcrypt verification to the system crypt(), which fails on many builds (more on that in troubleshooting), whereas it verifies apr1 internally on every platform.

Docker Registry & Kubernetes ingress-nginx

A private Docker Registry’s htpasswd backend accepts only bcrypt. Generate the entry, mount it, and point the registry at it:

# Generate a bcrypt entry into a file
htpasswd -Bbn admin 's3cret' > auth/htpasswd

# Run the registry with that file
docker run -d -p 5000:5000 \
  -v "$(pwd)/auth:/auth" \
  -e REGISTRY_AUTH=htpasswd \
  -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:2

For Kubernetes ingress-nginx, store the file as a Secret and reference it with annotations:

kubectl create secret generic basic-auth --from-file=auth=./auth/htpasswd
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-type: basic
    nginx.ingress.kubernetes.io/auth-secret: basic-auth
    nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"

Note the Secret key must be named auth, since ingress-nginx looks for exactly that key.

Caddy & Traefik

Both expect bcrypt hashes. Caddy uses the basic_auth directive (paste the bcrypt hash, not the plaintext):

example.com {
    basic_auth /admin/* {
        admin $2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
    }
}

Traefik uses a basicauth middleware, with user:bcrypt-hash pairs (escape any $ for your config format):

http:
  middlewares:
    admin-auth:
      basicAuth:
        users:
          - "admin:$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"

Once an endpoint is protected, verify it works from the command line. The cURL command builder assembles the -u user:pass request for you so you can confirm both the 401 and the authenticated 200.

Security Best Practices

Basic Auth is simple, which makes the few ways to misuse it easy to spot.

  • Always serve over HTTPS. The credential is reversible base64, so plain HTTP exposes the password on the wire. Terminate TLS in front of any protected endpoint.
  • Store the file outside the web root. If .htpasswd sits in a served directory, one misconfiguration lets someone download it. Keep it somewhere like /etc/nginx/.htpasswd, set chmod 640, and make it owned by the web server’s user (www-data, nginx) so the server can read it and other accounts cannot.
  • Use strong, unique passwords. Give each account its own high-entropy password from the random password generator, and don’t reuse one. To understand what “strong enough” means in bits, the password entropy explainer breaks down the math.
  • Know its limits. Basic Auth has no logout and no session: the browser caches the credential per realm until you close it, and resends it on every request. For a broader checklist on hashing, headers, and validation, see our web security best practices.

Troubleshooting Common Errors

nginx: crypt_r() failed (22: Invalid argument)

This is the most common nginx Basic Auth failure, and it always means the same thing: nginx tried to verify a bcrypt ($2y$) hash on a libc that doesn’t include the Blowfish scheme, typically Alpine’s musl or an older glibc. The fix is to regenerate the entry as apr1, which nginx verifies internally on any platform:

printf "admin:$(openssl passwd -apr1 's3cret')\n" > /etc/nginx/.htpasswd
nginx -s reload

Switching to a base image whose libc supports bcrypt also works, but apr1 is the simpler, portable solution.

401 even with the right password

When you’re certain the password is correct but still get a 401, walk this checklist in order:

  1. File path. Confirm AuthUserFile / auth_basic_user_file points at the actual file (absolute path, no typos).
  2. Permissions. The web server user must be able to read the file. Check with sudo -u www-data cat /etc/nginx/.htpasswd.
  3. Line endings / encoding. A file edited on Windows can carry \r characters that corrupt the hash. Run file .htpasswd and dos2unix it if needed.
  4. Stale browser cache. The browser caches credentials per realm. Test in a private/incognito window to rule out a remembered old password.
  5. Hash mismatch. Verify the stored hash matches the password: paste both into the htpasswd generator’s verify mode to confirm before blaming the config.

When NOT to Use Basic Auth

Basic Auth is the right tool for a narrow set of jobs: a staging site, an internal admin path, a CI artifact endpoint, a metrics dashboard, a private registry. It’s zero-dependency and takes two minutes to set up.

It is the wrong tool for a product login. There’s no logout, no password reset, no rate limiting, no account lockout, and no MFA. The browser caches the credential and resends it on every request until it closes. For anything users sign into, reach for sessions, OAuth, or OIDC instead. Respecting that boundary is what keeps Basic Auth useful where it fits.

FAQ

What does a single line in a .htpasswd file actually contain?

A username:hash pair separated by a colon. The hash starts with an algorithm prefix ($2y$ for bcrypt, $apr1$ for Apache MD5, {SHA} for SHA-1), followed by the salt and the digest. The plaintext password never appears in the file.

What’s the difference between .htpasswd and .htaccess?

.htaccess is Apache’s per-directory configuration file; it turns on Basic Auth and points at the credential store. .htpasswd is that credential store, holding the username:hash lines. nginx uses the .htpasswd format but configures auth in its server/location blocks, not .htaccess.

How do I add, change, or remove a user in an .htpasswd file?

To add or change a user, run htpasswd /path/.htpasswd username without -c; if the user exists, their hash is updated. To remove one, run htpasswd -D /path/.htpasswd username. Only use -c for the very first user, since it overwrites the whole file.

How does the browser remember Basic Auth credentials, and how do users log out?

The browser caches credentials keyed by realm and resends them automatically on every matching request. There is no standard logout: the only ways to clear them are closing the browser or wiping its cache. That missing logout is one reason Basic Auth doesn’t suit product authentication.

Can I use the same .htpasswd file for both Apache and nginx?

Yes, as long as the hash format is supported on both. apr1 (Apache MD5) is natively verified by Apache and nginx everywhere, so it’s the safest shared choice. bcrypt works on Apache but on nginx depends on the system crypt(), which fails on Alpine/musl builds.

Is HTTP Basic Authentication still relevant in 2026?

Yes. As a lightweight gate on top of HTTPS, covering internal tools, staging environments, private registries, and monitoring endpoints, it’s still practical and dependency-free. Just don’t mistake it for user-facing product authentication, which needs sessions, resets, rate limiting, and MFA that Basic Auth can’t provide.

Reviewed by the Go Tools team: every command, configuration block, and hash format in this guide was checked against Apache htpasswd (apache2-utils) and OpenSSL reference output.

Tags: htpasswd basic-auth http-authentication nginx apache bcrypt security

Related Articles

View all articles