Server

HTTP Basic Auth with htpasswd on Nginx and Apache

By Prabath Thalangama· October 24, 2026· 3 min read
#nginx#apache#security

Introduction

Basic auth is the quickest way to gate a staging environment, an internal dashboard, or a /metrics endpoint. It's fine for that as long as it's over HTTPS (Basic sends user:pass base64-encoded on every request — trivially readable without TLS) and you use a strong hash in the file.

Create the file

# bcrypt (-B) — the strong choice. -c creates the file (omit to append).
sudo htpasswd -B -c /etc/nginx/.htpasswd alice
# add another user (no -c)
sudo htpasswd -B /etc/nginx/.htpasswd bob
# non-interactive (careful: password in shell history / process list)
sudo htpasswd -B -b /etc/nginx/.htpasswd ci-user "$CI_PASSWORD"

Hash choices: bcrypt ($2y$) — use this. apr1 ($apr1$, MD5) — the old Apache default, still widely supported, far better than nothing. SHA-1 ({SHA}) — unsalted, weak, avoid. Plain crypt — DES, don't.

No htpasswd binary handy? The .htpasswd generator produces apr1 and SHA-1 lines in the browser.

Lock it down:

sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd

Nginx

server {
    # ... TLS config ...

    location / {
        auth_basic           "Staging - authorised users only";
        auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_pass           http://backend;
    }

    # Exempt the health check and ACME challenge
    location = /healthz {
        auth_basic off;
        return 200 "ok\n";
    }
    location /.well-known/acme-challenge/ {
        auth_basic off;
        root /var/www/certbot;
    }
}

Nginx needs to be built with bcrypt support (via the system crypt(3), which all mainstream packages have on Linux). If bcrypt hashes are rejected, the package is old — apr1 will still work.

IP allowlist OR password

"Internal IPs get in automatically, everyone else needs the password":

location /admin/ {
    satisfy any;                     # pass if EITHER check passes
    allow 10.0.0.0/8;
    allow 203.0.113.7/32;
    deny  all;
    auth_basic           "Admin";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

satisfy all (the default) would require both.

Apache

<Directory "/var/www/staging">
    AuthType Basic
    AuthName "Staging - authorised users only"
    AuthUserFile /etc/apache2/.htpasswd
    Require valid-user
</Directory>

# Or restrict to named users:
#   Require user alice bob

IP-or-password in Apache 2.4:

<RequireAny>
    Require ip 10.0.0.0/8
    Require valid-user
</RequireAny>

.htpasswd files placed in a web directory are ignored by Apache config but could be served — keep them outside the docroot regardless.

Verification and troubleshooting

# Should be 401 without creds, 200 with
curl -sI https://staging.example.com | head -1
curl -sI -u alice:secret https://staging.example.com | head -1
  • Always 401 even with correct password — the hash type in .htpasswd isn't supported by the server build (old Nginx + bcrypt), the file path is wrong, or the server can't read it (perms — it runs as www-data, not root). nginx -t won't catch a bad path; check the error log: no user/password was provided vs password mismatch.
  • 401 loop in a browser — cached bad credentials. The realm string changed, or the browser is retrying. Clear site data.
  • Works for / but not sub-paths (Apache) — AllowOverride for .htaccess isn't enabled and the directive is in .htaccess; put it in the vhost.
  • Basic auth header stripped — a proxy in front isn't forwarding Authorization. In Nginx, proxy_pass forwards it by default; a proxy_set_header Authorization ""; somewhere is clearing it.
  • htpasswd: command not found — install apache2-utils (Debian) / httpd-tools (RHEL), or use the generator.
  • Password visible in logs — -b puts it in your shell history and the process list. Prefer interactive, or feed it via stdin: htpasswd -B -i file user.
  • Basic auth over plain HTTP — anyone on the path reads the credentials. Redirect all HTTP to HTTPS first; see An Nginx reverse proxy with TLS.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.