HTTP basic authentication puts a browser login box in front of a page before Nginx serves anything. It is a quick way to hide a staging site from the public and search engines, or to add a second lock in front of an admin panel. This guide creates the password file, protects a whole site or one folder, and shows how to skip the prompt for your own IP.
Before you start
- A Linux VPS with Nginx serving your site.
- HTTPS on the site. Basic auth sends the username and password with every request, only lightly encoded. Over plain HTTP anyone on the path can read them. See Free SSL for Nginx with Let's Encrypt.
- SSH access with sudo.
Create the password file
Nginx reads users from a file in the Apache htpasswd format: one username:hashed-password per line. Keep it outside your web root so it can never be downloaded.
Ubuntu and Debian
Install the htpasswd tool:
sudo apt install apache2-utilsAlmaLinux and Rocky Linux
sudo dnf install httpd-toolsAdd users
Create the file with the first user. -c creates a new file, so use it only once:
sudo htpasswd -c /etc/nginx/.htpasswd adminYou are asked for the password twice. Add more users without -c:
sudo htpasswd /etc/nginx/.htpasswd deployWarning: Running htpasswd -c on an existing file replaces it and deletes every other user.
Recent versions of htpasswd hash with a salted MD5 variant by default. For a stronger bcrypt hash, add -B: sudo htpasswd -B /etc/nginx/.htpasswd admin.
Without htpasswd
If you cannot install the tools, openssl can create a compatible entry:
printf "admin:$(openssl passwd -apr1)\n" | sudo tee -a /etc/nginx/.htpasswdThen lock the file down so only root and Nginx can read it. The Nginx worker runs as www-data on Ubuntu and Debian, and as nginx on AlmaLinux and Rocky:
sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswdUse root:nginx on AlmaLinux and Rocky.
Protect a whole site
Add two lines to the site's server block:
server {
server_name staging.example.com;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}Keep your existing lines in the block; only add the two auth_basic lines. The text in quotes is the realm, shown by some browsers in the login box.
For a staging site, also stop search engines indexing anything that slips through:
add_header X-Robots-Tag "noindex, nofollow" always;Protect one folder
Put the directives in a location instead:
location /admin/ {
auth_basic "Admin area";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /index.php?$args;
}Only /admin/ and everything under it asks for a password.
PHP files inside the folder
A common mistake: the folder has PHP files, and the regex location ~ \.php$ block matches them first, so they skip the password. Use ^~ so the prefix wins, and handle PHP inside it:
location ^~ /admin/ {
auth_basic "Admin area";
auth_basic_user_file /etc/nginx/.htpasswd;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}Copy the PHP lines from your site's own PHP block, because the socket path differs between distributions and PHP versions.
For WordPress, protect only the login page:
location = /wp-login.php {
auth_basic "Login";
auth_basic_user_file /etc/nginx/.htpasswd;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}Let trusted IPs skip the password
Use satisfy any with allow and deny. A request passes if it comes from an allowed IP or supplies a valid password:
location ^~ /admin/ {
satisfy any;
allow 198.51.100.20;
allow 10.0.0.0/24;
deny all;
auth_basic "Admin area";
auth_basic_user_file /etc/nginx/.htpasswd;
}Change satisfy any to satisfy all to require both the right IP and a password. If the site is behind Cloudflare, configure real IPs first or the allow list will not match.
Exclude a path from the prompt
Some paths must stay open, for example Let's Encrypt validation or a health check. Turn auth off in a location:
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
}Apply the change
sudo nginx -t && sudo systemctl reload nginxCheck it works
Without credentials you should get a 401:
curl -I https://staging.example.com/HTTP/2 401
www-authenticate: Basic realm="Restricted"With credentials, a 200:
curl -I -u admin https://staging.example.com/curl asks for the password. In a browser, a login box appears; after you sign in the browser remembers it until you close it.
Troubleshooting
- 500 Internal Server Error after enabling: Nginx cannot read the password file. Check the path, owner and permissions, and the error log.
- Password is always rejected: the file was overwritten with
-c, or there is a typo in the username. View the usernames withsudo cut -d: -f1 /etc/nginx/.htpasswd. - PHP pages load without a prompt: the regex PHP location matched first. Use
location ^~and nest the PHP block as shown. - Your IP still gets a prompt with
satisfy any: Nginx sees a different IP, usually a CDN's. Check the access log for the address it records. - Certificate renewals fail: the ACME challenge path is behind auth. Exclude it with
auth_basic off.
Next steps
- Lock admin paths to known IPs only: Allow or block IP addresses in Nginx.
- Slow down password guessing: Rate limit requests in Nginx.
- Run a private staging copy on a Linux VPS.
