Websites & apps

Password protect a site or folder with Nginx basic auth

Add a username and password prompt to a whole site, a staging copy or an admin folder with Nginx basic auth, and let trusted IPs through without a password.

BeginnerLinux server5 min read

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:

Bash
sudo apt install apache2-utils

AlmaLinux and Rocky Linux

Bash
sudo dnf install httpd-tools

Add users

Create the file with the first user. -c creates a new file, so use it only once:

Bash
sudo htpasswd -c /etc/nginx/.htpasswd admin

You are asked for the password twice. Add more users without -c:

Bash
sudo htpasswd /etc/nginx/.htpasswd deploy

Warning: 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:

Bash
printf "admin:$(openssl passwd -apr1)\n" | sudo tee -a /etc/nginx/.htpasswd

Then 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:

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

Use root:nginx on AlmaLinux and Rocky.

Protect a whole site

Add two lines to the site's server block:

Nginx
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:

Nginx
add_header X-Robots-Tag "noindex, nofollow" always;

Protect one folder

Put the directives in a location instead:

Nginx
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:

Nginx
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:

Nginx
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:

Nginx
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:

Nginx
location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
}

Apply the change

Bash
sudo nginx -t && sudo systemctl reload nginx

Check it works

Without credentials you should get a 401:

Bash
curl -I https://staging.example.com/
HTTP/2 401
www-authenticate: Basic realm="Restricted"

With credentials, a 200:

Bash
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 with sudo 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.