Basic Authentication is useful when a staging site, preview build, private download area, or low-traffic admin path needs a quick gate before a full application login exists. To password protect Nginx sites with Basic Authentication, keep the password file outside the document root, apply auth_basic to the exact server or location that owns the request, and verify the result against a real file, proxy, or application route.
Basic Auth sends reusable credentials with each request. Use it behind HTTPS, a VPN, a tailnet, or an IP restriction for sensitive paths. It is not a replacement for application accounts, MFA, audit logging, or authorization inside the app.
Password Protect an Nginx Site with Basic Authentication
A working setup has three parts: a password file that Nginx can read, an auth_basic realm that tells browsers to prompt for credentials, and an auth_basic_user_file path that points to the password file. If Nginx is not installed yet, start with the relevant setup path for Nginx on Ubuntu, Nginx on Debian, or Nginx on Fedora.
| Nginx Item | Context | Purpose |
|---|---|---|
auth_basic | http, server, location, limit_except | Enables Basic Authentication and sets the browser-visible realm, or disables inherited authentication with off. |
auth_basic_user_file | http, server, location, limit_except | Points Nginx to the flat password file containing usernames and hashed passwords. |
satisfy | http, server, location | Controls whether IP rules and authentication must all pass or whether either control can allow access. |
allow and deny | http, server, location, limit_except | Restrict requests by client address before combining the result with Basic Authentication. |
Install the htpasswd Utility
The common tool for creating Basic Auth password files is htpasswd. It comes from Apache HTTP Server utilities, but the generated password file can be used by Nginx when the stored hash format is supported on the target server.
On Debian and Ubuntu systems, install the utility package from the distro repository:
sudo apt update
sudo apt install apache2-utils
On Fedora, CentOS Stream, Rocky Linux, and similar DNF-managed systems, install the Apache tools package:
sudo dnf install httpd-tools
On Arch Linux, the Arch manual page documents htpasswd in the extra/apache package. Install it as part of a full system sync so pacman does not create a partial-upgrade state:
sudo pacman -Syu apache
Verify that the utility is available before creating the password file:
command -v htpasswd
Expected output on most Linux systems:
/usr/bin/htpasswd
Find the Nginx Worker Group
The password file should be readable by Nginx but not world-readable. Identify the non-root worker group before setting permissions:
ps -o user,group,comm -C nginx
Example output from an nginx.org package shows workers running as nginx:
USER GROUP COMMAND root root nginx nginx nginx nginx
Debian and Ubuntu distro packages often use www-data instead. Capture the first non-root worker group into a shell variable so the later commands match your installed package:
nginx_worker_group=$(ps -o user=,group= -C nginx | awk '$1 != "root" { print $2; exit }')
printf 'Nginx worker group: %s\n' "$nginx_worker_group"
If the variable is empty, Nginx is not running or the process name differs on that host. Start Nginx first, inspect the active user directive with sudo nginx -T, or substitute the worker group manually.
Create the Nginx Password File
Create a dedicated auth directory outside every web root, then add the first user. The -c flag creates the file and truncates an existing file, so use it only for the first user in a new password file.
sudo install -d -m 750 -o root -g "$nginx_worker_group" /etc/nginx/auth
sudo htpasswd -B -C 10 -c /etc/nginx/auth/example.com.htpasswd admin
The -B option creates a bcrypt hash, and -C 10 raises the bcrypt cost above the default. Apache documents the allowed bcrypt cost range as 4 through 17; higher values slow both password creation and verification. Keep the value moderate for sites that receive frequent authenticated requests.
Bcrypt compatibility depends on the system crypt() support available to the Nginx build, so use APR1 only as a compatibility fallback after ruling out a wrong username, password, path, or file permission.
Restrict the password file after creating it:
sudo chown root:"$nginx_worker_group" /etc/nginx/auth/example.com.htpasswd
sudo chmod 640 /etc/nginx/auth/example.com.htpasswd
Check the stored username, hash prefix, owner, group, and mode without printing the full hash:
sudo awk -F: '{print $1 ":" substr($2,1,4) "..."}' /etc/nginx/auth/example.com.htpasswd
sudo stat -c '%U:%G %a %n' /etc/nginx/auth/example.com.htpasswd
Example output when bcrypt is used and the worker group is nginx:
admin:$2y$... root:nginx 640 /etc/nginx/auth/example.com.htpasswd
Never place the password file under the site’s document root, such as
/var/www/example.com. A misconfigured static location could expose the file to browsers.
Protect an Entire Nginx Server Block
Place the Basic Auth directives in the server block when the whole site should require a username and password. Use your existing site file, such as /etc/nginx/sites-available/example.com on Debian-style layouts or /etc/nginx/conf.d/example.com.conf on conf.d layouts.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public_html;
index index.html index.htm;
auth_basic "Restricted site";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
location / {
try_files $uri $uri/ =404;
}
}
The realm string after auth_basic appears in the browser’s login prompt. Keep it generic enough that it does not reveal sensitive environment details.
Protect Only One Nginx Location
Place the directives inside a location block when only one path should be private:
location ^~ /private/ {
auth_basic "Private files";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
try_files $uri $uri/ =404;
}
The ^~ prefix is useful when the whole private directory should stay in this prefix location instead of falling through to a later regex location. If your site has several regex handlers, compare the path against Nginx location block priority before assuming the location you edited is the one handling the request.
Test and Reload Nginx Basic Authentication
Test the full Nginx configuration before reloading the service:
sudo nginx -t
A successful syntax test prints:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx only after the syntax test passes:
sudo systemctl reload nginx
Request the protected path without credentials. Use the same hostname and path that should be private. The --noproxy '*' option keeps the loopback check from being sent through a proxy configured in the shell environment:
curl --noproxy '*' -sS -D - -o /dev/null -H "Host: example.com" http://127.0.0.1/ | tr -d '\r' | sed -n '1p;/^WWW-Authenticate:/p'
A protected path should return 401 Unauthorized and a Basic Auth challenge:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Restricted site"
Test the same path with the username. Let curl prompt for the password instead of putting the password in shell history:
curl --noproxy '*' -sS -D - -o /dev/null --user admin -H "Host: example.com" http://127.0.0.1/ | tr -d '\r' | sed -n '1p'
Expected output for a static site is usually a successful response status:
HTTP/1.1 200 OK
If the site redirects HTTP to HTTPS, test the HTTPS virtual host instead. For a DNS-independent origin test, preserve the real hostname with curl --resolve or send the same Host header to the local listener.
Do not prove Basic Auth with a temporary
return 200location. A synthetic return response can answer before the access check, creating a false sense that the protected route was tested. Use the real static file, proxy, FastCGI, or application route that visitors will reach.
Manage Nginx Basic Authentication Users
After the password file exists, add users without -c. Reusing -c rewrites the file and removes existing users.
sudo htpasswd -B -C 10 /etc/nginx/auth/example.com.htpasswd editor
The same command updates the password when the username already exists. Verify a stored username and password when troubleshooting a login loop:
sudo htpasswd -v /etc/nginx/auth/example.com.htpasswd admin
Delete a user when access should be revoked:
sudo htpasswd -D /etc/nginx/auth/example.com.htpasswd editor
A package-manager update refreshes the htpasswd utility. It does not rotate existing passwords or change the Nginx password file. Rotate credentials by updating each user entry and reloading Nginx only when the configuration path or permissions changed.
The
htpasswd -boption puts the plaintext password on the command line. Avoid it for normal administration because shell history and process listings can expose the password while the command runs.
Allow Public Paths or Combine IP Rules
When authentication is set at the server level, use auth_basic off; for public paths such as health checks, static assets, or ACME HTTP-01 challenge files that must remain reachable without a login prompt.
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
root /var/www/example.com/public_html;
try_files $uri =404;
}
For paths that need both a trusted network and a password, combine address rules with Basic Auth. The default satisfy all behavior means the request must pass both controls:
location /admin/ {
satisfy all;
allow 203.0.113.10;
deny all;
auth_basic "Admin area";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
proxy_pass http://127.0.0.1:3000;
}
Use satisfy any only when either condition should be enough, such as allowing a VPN subnet without a password while requiring Basic Auth for everyone else:
location /preview/ {
satisfy any;
allow 10.20.30.0/24;
deny all;
auth_basic "Preview site";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
try_files $uri $uri/ =404;
}
Nginx checks allow and deny rules in order until the first match. Behind a CDN, reverse proxy, or load balancer, confirm the client IP Nginx actually sees before trusting an address allowlist.
Use Basic Authentication Safely with Reverse Proxies
Basic Auth works in front of a reverse-proxied app, but the Authorization header deserves attention. If the upstream does not need the browser’s Basic Auth header, clear it before proxying the request:
location /app/ {
auth_basic "Private app";
auth_basic_user_file /etc/nginx/auth/example.com.htpasswd;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization "";
}
An empty proxy_set_header value tells Nginx not to pass that header to the upstream server. If the application expects its own bearer tokens or Basic Auth credentials, putting Nginx Basic Auth on the same browser-facing route can conflict with the app’s authentication model. In that case, use the app’s login, a separate protected route, or a subrequest-based authentication design instead. For the broader proxy setup, use the Nginx reverse proxy configuration workflow.
Troubleshoot Nginx Basic Authentication
Authentication failures usually come from the wrong location, an unreadable password file, a stale user entry, an unsupported hash on the target host, or another layer in front of Nginx. Start with read-only checks before changing permissions or replacing user entries.
| Symptom | Likely Cause | First Check |
|---|---|---|
| No password prompt appears | The request is hitting a different server or location, or authentication is disabled by inheritance. | Inspect active auth_basic, server_name, and location blocks. |
Correct password still returns 401 | Wrong user entry, wrong password file, unreadable file, or unsupported hash format. | Run htpasswd -v, then retest with curl. |
Nginx returns 500 | Nginx cannot open the configured password file. | Check the error log, file path, and file permissions. |
| Trusted clients are blocked | allow, deny, satisfy, or real-client-IP handling does not match the request path. | Compare the access log client address with the allowlist. |
| Works locally but not publicly | CDN, proxy manager, browser cache, load balancer, or hosting panel owns the public request. | Compare a local host-header request with the public response. |
Nginx Does Not Ask for a Password
Confirm that the active Nginx configuration includes the authentication directives in the server or location that handles the request:
sudo nginx -T 2>&1 | grep -nE '^(# configuration file|[[:space:]]*(server_name|location|auth_basic|auth_basic_user_file)[[:space:];])'
Then test the exact path locally:
curl --noproxy '*' -sS -D - -o /dev/null -H "Host: example.com" http://127.0.0.1/private/ | tr -d '\r' | sed -n '1p;/^WWW-Authenticate:/p'
If the active configuration is missing your directives, check whether the file lives in an included directory. Debian and Ubuntu package layouts often need a symlink in sites-enabled, while Fedora, Rocky Linux, CentOS Stream, and nginx.org layouts commonly load /etc/nginx/conf.d/*.conf. The Nginx server blocks and virtual hosts guide covers include-layout checks in more depth.
Correct Password Still Returns 401
First verify the username and password against the password file itself:
sudo htpasswd -v /etc/nginx/auth/example.com.htpasswd admin
If htpasswd verifies the user but Nginx still returns 401, inspect the file path that Nginx actually uses and retest with curl. Bcrypt is the stronger first choice, but Nginx accepts it only when the build’s crypt() support can verify bcrypt. If the username, password, path, and permissions are correct and Nginx still rejects the bcrypt entry, recreate the affected user with APR1:
sudo htpasswd -m /etc/nginx/auth/example.com.htpasswd admin
sudo htpasswd -v /etc/nginx/auth/example.com.htpasswd admin
Password-file changes are read on the next request, so retest the protected URL after replacing the entry. Reload Nginx only when you changed the Nginx configuration or filesystem permissions.
Use HTTPS and long unique passwords when falling back to APR1. Do not use plaintext, SHA-1, or legacy DES-style crypt hashes for new password files.
Nginx Cannot Read the Password File
A missing or unreadable password file commonly appears as a 500 Internal Server Error or a repeated authentication failure. Check the Nginx error log first:
sudo tail -n 40 /var/log/nginx/error.log
Look for a log phrase that names the password file path and the operating-system error, such as open() "/etc/nginx/auth/example.com.htpasswd" failed (13: Permission denied).
Check each directory and the file mode:
namei -l /etc/nginx/auth/example.com.htpasswd
sudo stat -c '%U:%G %a %n' /etc/nginx/auth /etc/nginx/auth/example.com.htpasswd
Repair the owner and mode with the worker group detected earlier. If this is a new shell, detect the worker group again before changing ownership:
nginx_worker_group=$(ps -o user=,group= -C nginx | awk '$1 != "root" { print $2; exit }')
if [ -n "$nginx_worker_group" ]; then
sudo chown root:"$nginx_worker_group" /etc/nginx/auth /etc/nginx/auth/example.com.htpasswd
sudo chmod 750 /etc/nginx/auth
sudo chmod 640 /etc/nginx/auth/example.com.htpasswd
sudo nginx -t
sudo systemctl reload nginx
else
printf 'Nginx worker group is empty; inspect sudo nginx -T before changing ownership.\n' >&2
fi
On SELinux-enabled systems, normal mode bits can look correct while labels still block access. If the password file lives in an unusual path and the error log shows permission denials, check SELinux labels before loosening file permissions. For visible 403 failures on protected content, use the Nginx 403 Forbidden troubleshooting workflow to separate filesystem permissions, access rules, and SELinux context issues.
IP Rules Bypass or Block the Wrong Clients
Address rules depend on the client IP Nginx sees. Check the access log for the protected path:
sudo tail -n 20 /var/log/nginx/access.log
If the logged address belongs to a CDN, load balancer, or reverse proxy, your allow and deny rules are matching that proxy address, not the visitor. Fix the real-client-IP layer or move the access-control rule to the edge system that owns the public request.
Login Works Locally but Fails Publicly
Compare the origin response with the public response for the same hostname and path:
curl --noproxy '*' -sS -D - -o /dev/null -H "Host: example.com" http://127.0.0.1/private/ | tr -d '\r' | sed -n '1p;/^WWW-Authenticate:/p'
curl -sS -D - -o /dev/null https://example.com/private/ | tr -d '\r' | sed -n '1p;/^WWW-Authenticate:/p'
If the origin returns the expected 401 or authenticated 200 but the public request behaves differently, inspect the CDN, proxy manager, cloud load balancer, container port mapping, hosting panel, or browser cache before changing the Nginx password file.
Remove Basic Authentication from Nginx
Remove the auth_basic and auth_basic_user_file directives from the server or location that no longer needs a login prompt. Set site_file to the active site file first, such as /etc/nginx/sites-available/example.com on Debian-style layouts or /etc/nginx/conf.d/example.com.conf on conf.d layouts:
site_file=/etc/nginx/conf.d/example.com.conf
sudo cp -a "$site_file" "$site_file.bak"
sudo nano "$site_file"
Test and reload after removing the directives:
sudo nginx -t
sudo systemctl reload nginx
Before deleting the password file, confirm that no active Nginx configuration still references it:
sudo nginx -T 2>&1 | grep -F "/etc/nginx/auth/example.com.htpasswd"
If the search returns no references, remove only the password file for that site:
sudo rm -f /etc/nginx/auth/example.com.htpasswd
sudo rmdir /etc/nginx/auth 2>/dev/null || true
Removing the htpasswd utility package is optional. Leave it installed if Apache, another Nginx site, monitoring, or admin scripts still use it. Remove it only when you know it is not shared:
sudo apt remove apache2-utils
sudo dnf remove httpd-tools
On Arch Linux, removing the apache package also removes the Apache HTTP Server package, not just htpasswd. Keep it installed unless you intentionally no longer need that package.
Conclusion
Nginx is enforcing Basic Authentication when unauthenticated requests return 401, valid users can reach the intended route, and public exceptions behave only where you configured them. Keep the password file outside the web root, test every change with nginx -t, and retest the exact hostname and path before trusting a browser result.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><a href="https://example.com">link</a><blockquote>quote</blockquote>