Password protection (basic auth)

Put a whole site, a single folder or one file behind an HTTP password prompt, an IP allow-list, or both. Reqad ships this as a ready-made snippet in the Advanced Config editor - you paste it, adjust two lines and save.

Where it lives

Go to Accounts, click Manage on the account's row, then Advanced settings at the bottom of the dialog. On the account page open the Advanced Config tab and pick the nginx vhost panel - this is the account's real /etc/nginx/conf.d/<domain>.conf. The Snippets button inserts Restrict by IP / password at the cursor, already filled in with this account's home directory.

The snippet is inserted, not applied. It drops config text at the cursor so your existing vhost is never overwritten - you still choose where it goes, edit the path and the IPs, and press Save & reload.

1. Create the password file

nginx does not create users for you - it reads a password file, one user:hash line per person. Open the panel's Terminal (or connect over SSH) and create one in the account's home directory with OpenSSL:

printf 'alice:%s\n' "$(openssl passwd -6)" > /home/<user>/.htpasswd

openssl passwd -6 prompts for the password twice and prints a SHA-512 hash - nothing is typed on the command line, so the password stays out of your shell history. Add more users later with >> instead of >, which appends rather than replacing the file:

printf 'bob:%s\n' "$(openssl passwd -6)" >> /home/<user>/.htpasswd

To change someone's password, delete their line and add it again. To revoke access, delete the line - the change takes effect on the next request, with no reload needed.

Then make sure nginx can read it - nginx workers run as the nginx user, and account home directories are 0711 so they can traverse but not list them:

chmod 644 /home/<user>/.htpasswd

The file holds hashes, not passwords. If you prefer it unreadable to other accounts, use chown <user>:nginx plus chmod 640 instead. Keep it in the home directory, outside public_html - the generated vhost also denies any URL starting with a dot, so .htpasswd is never web-served.

2. Insert the snippet

Place the cursor inside the server { listen 443 ssl; ... } block - the port 80 block only redirects to HTTPS, so there is nothing to protect there. Insert Snippets → Restrict by IP / password:

location /admin/ {
    # allow these IPs without a password; everyone else must log in
    satisfy any;
    allow 203.0.113.10;
    allow 192.0.2.0/24;
    deny  all;
    auth_basic           "Restricted area";
    auth_basic_user_file /home/<user>/.htpasswd;
}
  • location /admin/ - the URL prefix being protected. Change it to the path you want.
  • satisfy any - either check is enough: a listed IP gets in silently, everyone else is asked for a password. Use satisfy all to require both.
  • allow / deny - the IP allow-list. Replace the example addresses with your own; single IPs or CIDR ranges both work.
  • auth_basic - the text the browser shows in the login prompt.
  • auth_basic_user_file - the file from step 1.

For a password with no IP exceptions, delete the satisfy, allow and deny lines. For an IP allow-list with no password, delete the satisfy and both auth_basic lines.

3. Protecting a path that runs PHP

Read this before protecting a PHP folder. The snippet as pasted has no PHP handler. A protected location handles the request on its own, so a .php file inside it would be sent to the browser as a download of its source code instead of being executed. Use the form below whenever the protected path contains PHP.
location ^~ /admin/ {
    satisfy any;
    allow 203.0.113.10;
    deny  all;
    auth_basic           "Restricted area";
    auth_basic_user_file /home/<user>/.htpasswd;

    try_files $uri $uri/ =404;

    location ~ \.php$ {
        try_files       $uri =404;
        include         /etc/nginx/fastcgi_params;
        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass    php-fpm-<user>;
    }
}

The nested block runs PHP and inherits the password and IP rules from its parent. ^~ tells nginx to stop at this prefix and not fall through to other rules in the file. Copy the fastcgi_pass line verbatim from the location ~ \.php$ block already in your vhost - it points at this account's own PHP-FPM pool.

Protect the whole site

Put just the two auth_basic lines at the top of the existing location / { ... } block. Everything nested inside it - PHP, static files, uploads - inherits them, and the site is locked with one change:

location / {
    auth_basic           "Restricted area";
    auth_basic_user_file /home/<user>/.htpasswd;

    # ... the rest of the block, unchanged ...

Handy for a staging site or a launch that isn't public yet.

Don't put it in the server block. It would work, but Reqad issues and renews Let's Encrypt certificates with certbot's nginx plugin, which answers the validation request from this same vhost - a server-wide password makes that request return 401 and the certificate fails. Inside location / the challenge path stays reachable. See SSL / TLS.

Protect a single file

To protect one entry point - a WordPress login page, for example - match it exactly and give it its own PHP handler:

location = /wp-login.php {
    auth_basic           "Restricted area";
    auth_basic_user_file /home/<user>/.htpasswd;

    include         /etc/nginx/fastcgi_params;
    fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass    php-fpm-<user>;
}

If you protect all of /wp-admin/ instead, exclude admin-ajax.php - front-end features call it without a login, and a password there breaks them. Add the same block for location = /wp-admin/admin-ajax.php with auth_basic off; in place of the two auth lines.

4. Save and reload

Press Save & reload. Reqad validates the file with nginx -t before writing it, so a typo is refused instead of taking the site down; if the live test fails anyway, the previous version is put back automatically and the error is shown. On success nginx is reloaded and the protection is live immediately.

Every save keeps a copy of the version it replaced. Version history next to the Snippets button lists the last 5, with a preview and a one-click restore - that's also the quickest way to remove the protection again.

Troubleshooting

  • The prompt never accepts the password. Check the auth_basic_user_file path, and that the line really reads user:$6$... - a stray space around the colon, or a hash pasted in from somewhere else, will never match. Regenerate the line with openssl passwd -6. Bcrypt hashes ($2y$) do not work: nginx reads these through the system crypt(), which handles SHA-512 but not bcrypt.
  • 403 instead of a password prompt. The deny all matched and there was no auth_basic to fall back to - either you removed the auth lines but kept deny, or satisfy any is missing.
  • The browser downloads a .php file. The protected location has no PHP handler - use the nested form above.
  • Everyone gets 500 or 403 after a correct password. Look for open() "/home/<user>/.htpasswd" failed (13: Permission denied) in /var/log/nginx/<domain>_log and fix the file permissions from step 1.
  • The IP allow-list never matches. Behind Cloudflare or another proxy, nginx sees the proxy's address, not the visitor's. Allow-list on the proxy instead, or restore the real client IP first.
Good to know. The account is served over HTTPS, so basic auth credentials are encrypted in transit. It is a solid gate for staging sites and admin folders, but it is a single shared password - it does not replace the application's own accounts. See Security for what Reqad hardens by default.

Need a hand? Register for early access or contact us.