There’s a particular kind of relief I feel every time I put a staging site behind a password before a client accidentally shares the link on social media. HTTP Basic Authentication is the simplest way I know to slap a password prompt in front of a directory, an admin panel, or an entire site, and Nginx supports it natively without any extra modules. It’s not the most sophisticated authentication mechanism in the world, but for internal tools, staging environments, and quick access control, it’s still one of my go-to solutions.
In this guide, I’ll cover how Basic Authentication works, how to set it up in Nginx from scratch, how to protect specific locations instead of an entire site, how to combine it with IP whitelisting, and the security trade-offs I always keep in mind before relying on it.
How HTTP Basic Authentication Works
Basic Authentication is defined in the HTTP specification itself. When a client requests a protected resource, the server responds with a 401 Unauthorized status and a WWW-Authenticate header. The browser then prompts the user for a username and password, encodes them in Base64, and resends the request with an Authorization: Basic <encoded-credentials> header.
I want to be clear about one thing: Base64 is encoding, not encryption. Anyone intercepting the traffic can decode the credentials trivially. That’s why Basic Auth should always run over HTTPS — I’ll cover that requirement again in the security section, because it’s the single most common mistake I see.
Requirements
- Nginx installed and running
- The
apache2-utilspackage (Debian/Ubuntu) orhttpd-toolspackage (RHEL/CentOS), which provides thehtpasswdutility - Root or sudo access
- Ideally, SSL/TLS already configured on your site (I cover that in a separate article, but I’ll note where it matters here)
Step 1: Install htpasswd
Nginx doesn’t ship its own password file generator, so I use the Apache utility, which works fine independent of whether Apache itself is installed.
On Ubuntu/Debian:
sudo apt update
sudo apt install apache2-utils
On RHEL/CentOS/Fedora:
sudo dnf install httpd-tools
Step 2: Create the Password File
I create a dedicated directory to store credential files outside the web root, so they’re never accidentally served as static content:
sudo mkdir -p /etc/nginx/.htpasswd
Then I generate the first user with the -c flag, which creates a new file (I only use -c the first time — using it again overwrites the whole file):
sudo htpasswd -c /etc/nginx/.htpasswd/passwords ahmad
I’ll be prompted to enter and confirm a password. The tool hashes it using bcrypt or MD5-based crypt, depending on version, and stores it in the file — never in plaintext.
To add additional users without wiping the existing ones, I drop the -c flag:
sudo htpasswd /etc/nginx/.htpasswd/passwords another_user
I can verify the file’s contents (hashes only, never readable plaintext):
cat /etc/nginx/.htpasswd/passwords
ahmad:$apr1$eS3f9K2j$Hk8pQz...
another_user:$apr1$xY29fL0p$Qm3rTd...
Step 3: Configure Nginx to Require Authentication
Now I edit my site’s configuration file, typically found at /etc/nginx/sites-available/example.com on Debian-based systems or /etc/nginx/conf.d/example.com.conf on RHEL-based systems.
Protecting the Entire Site
server {
listen 80;
server_name example.com;
root /var/www/example.com;
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd/passwords;
location / {
index index.html;
}
}
The auth_basic directive sets the realm name, which is the message shown in the browser’s login prompt. auth_basic_user_file points to the password file I created earlier.
Protecting a Specific Directory Only
More often, I only want to lock down one section — say, /admin — while leaving the rest of the site public:
server {
listen 80;
server_name example.com;
root /var/www/example.com;
location / {
index index.html;
}
location /admin {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd/passwords;
}
}
Excluding a Specific Path from Authentication
Sometimes I want everything behind a password except one endpoint — for example, a health check that a monitoring service needs to hit without credentials:
location /health {
auth_basic off;
return 200 "OK";
}
location / {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd/passwords;
}
Setting auth_basic off; inside a more specific location block overrides the broader restriction, since Nginx matches the most specific location block first.
Step 4: Test and Reload
As always, I validate the syntax before touching the live service:
sudo nginx -t
If it comes back clean:
sudo systemctl reload nginx
Testing Basic Authentication
I open the protected URL in a browser — I should immediately see a native login popup rather than the page content. I can also test from the command line with curl:
curl -I http://example.com/admin
Without credentials, I get:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Admin Area"
With credentials:
curl -I -u ahmad:mypassword http://example.com/admin
HTTP/1.1 200 OK
Complete Example Configuration
Here’s a fuller example combining Basic Auth with a health-check exception and HTTPS (assuming SSL is already configured):
server {
listen 443 ssl;
server_name staging.example.com;
root /var/www/staging;
ssl_certificate /etc/letsencrypt/live/staging.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/staging.example.com/privkey.pem;
location /health {
auth_basic off;
return 200 "OK";
add_header Content-Type text/plain;
}
location / {
auth_basic "Staging Environment — Authorized Access Only";
auth_basic_user_file /etc/nginx/.htpasswd/passwords;
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name staging.example.com;
return 301 https://$host$request_uri;
}
Combining Basic Auth with IP Restrictions
For sensitive internal tools, I sometimes stack Basic Auth on top of IP whitelisting for defense in depth — a visitor needs to be on the right network and know the password:
location /admin {
allow 203.0.113.10;
deny all;
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd/passwords;
}
By default, Nginx satisfies both conditions (satisfy all), which is what I want here. If I wanted either condition to be sufficient on its own, I’d add satisfy any;.
Managing Users Over Time
To change a password, I just re-run htpasswd without -c for the existing file:
sudo htpasswd /etc/nginx/.htpasswd/passwords ahmad
To delete a user:
sudo htpasswd -D /etc/nginx/.htpasswd/passwords another_user
I always reload Nginx after editing the password file to be safe, even though Nginx re-reads it per request rather than caching it in memory.
Troubleshooting Common Issues
Getting a 403 Forbidden instead of a login prompt. This usually means Nginx doesn’t have read permission on the password file. I check ownership and permissions:
sudo chown root:www-data /etc/nginx/.htpasswd/passwords
sudo chmod 640 /etc/nginx/.htpasswd/passwords
Login prompt appears but valid credentials are rejected. Double-check I didn’t accidentally regenerate the file with -c and wipe out the user, or that I’m not testing with trailing whitespace in the username. I also confirm the file path in auth_basic_user_file matches exactly — a stale symlink or typo is a common culprit.
Basic Auth not applying to a location I expect it to. Remember that Nginx location matching picks the most specific block. If I have auth_basic off; in a broader block that I didn’t intend to affect, it can leak into locations I didn’t expect.
Password prompt keeps reappearing after entering correct credentials. This is almost always a browser caching quirk or an incorrect realm string causing repeated challenges — I test with curl -u to rule out browser-side issues first.
Security Considerations
- Always serve Basic Auth over HTTPS. Since credentials are only Base64-encoded, not encrypted, anyone sniffing unencrypted HTTP traffic can trivially recover the username and password. I never deploy Basic Auth on a plain
http://site. - Store the
.htpasswdfile outside any publicly served directory. If it’s ever accidentally exposed through misconfiguration, at least it’s not literally sitting in the web root. - Use
bcrypthashing when generating passwords if yourhtpasswdversion supports it, via the-Bflag:
sudo htpasswd -B -c /etc/nginx/.htpasswd/passwords ahmad
- Basic Auth has no built-in rate limiting or lockout — pair it with
limit_reqto slow down brute-force attempts against the login prompt. - Rotate credentials periodically, especially for shared staging environments where multiple team members know the password.
- Don’t rely on Basic Auth as your only layer for anything genuinely sensitive — it’s a good gatekeeper for staging sites and internal dashboards, not a replacement for proper application-level authentication with sessions, MFA, and audit logging.
Performance Tips
- Basic Auth adds negligible overhead — Nginx checks the password file on each request, and for reasonably sized files (a handful to a few hundred users), this is effectively instant.
- For very large user bases, Basic Auth via flat file becomes unwieldy to manage — that’s a sign to move to a proper authentication backend (OAuth, an identity provider, or an
auth_requestsetup that defers to an application). - Keep the password file lean; don’t let it accumulate stale test accounts over time.
Real-World Use Cases
- Protecting a staging or development environment before it goes live, so it doesn’t get indexed by search engines or stumbled on by the public.
- Locking down monitoring dashboards like Grafana, Prometheus, or a custom internal status page.
- Adding a quick password layer to an admin panel while the application’s own authentication is still being built.
- Restricting access to internal documentation or API playgrounds to team members only.
- Providing client preview access to a site under development without deploying full user account infrastructure.
Best Practices
- Never deploy Basic Auth without HTTPS in front of it.
- Keep the
.htpasswdfile outside the document root and set restrictive file permissions. - Use bcrypt hashes when available instead of the older MD5-based crypt format.
- Combine with IP allowlisting for an extra layer on genuinely sensitive endpoints.
- Document who has credentials and rotate them when team members change or a staging environment goes public.
- Treat Basic Auth as a stopgap, not a permanent authentication strategy for production applications with real user accounts.
Wrapping Up
Basic Authentication in Nginx is refreshingly simple to set up, and that simplicity is exactly why I still reach for it constantly — for staging sites, internal tools, and anything that needs a quick password gate without building out a full login system. The two things I never skip are serving it over HTTPS and keeping the password file out of the public web root. Get those right, generate your .htpasswd file, and you’ve got a password wall running in under five minutes.