Nginx auth_basic: Basic Authentication Configuration Examples and Hardening
When internal admin pages or monitoring dashboards shouldn’t sit naked on the public internet, but you’re not ready for a full OAuth setup, Nginx’s built-in auth_basic module fills the gap exactly—two lines of configuration plus a password file keep every anonymous request out. This article covers the basic configuration, common hardening combinations, two classic mistakes, and finishes with advanced usage under OpenResty. All configurations were tested inside an nginx/1.31.5 container (the nginx:alpine docker image), and every status code shown comes from a real run.
What auth_basic Is and When to Use It
auth_basic does something simple: it requires a username and password from clients accessing a specified resource, and rejects anyone who can’t produce them. The mechanism is straightforward, which makes it a good fit for non-sensitive data or internal admin environments—dev and test environments, ops backends and monitoring pages, and temporary resources not yet ready to go fully public are all typical spots for it.
Lab Environment
An nginx/1.31.5 container (nginx:alpine), with htpasswd installed via apk add apache2-utils. In production, replace example.com with your own domain and make sure it sits behind HTTPS.
Basic Configuration
Step 1: Create the Password File
Generate it with htpasswd; Nginx is fully compatible with the password file format it produces. htpasswd isn’t part of a default system install, so install it per your distribution and generate:
1 | apk add apache2-utils # Alpine (tested in this article's container) |
Step 2: Modify the Nginx Configuration
Enable authentication with auth_basic in a server or location block, and point auth_basic_user_file at the password file:
1 | server { |
Each of the three directives handles one thing: auth_basic enables authentication and sets the dialog title, auth_basic_user_file points to the password file generated in the previous step, and proxy_pass forwards authenticated requests to the backend service.
After applying the configuration, fire one request for each of the three credential states to verify. Test results (the backend was simulated with a static page):

OpenResty fully inherits Nginx’s auth_basic module—the configuration syntax is unchanged, so it works as-is once you swap server_name for your own domain.
Common Hardening Combinations
Basic authentication is a single door, so in practice you usually stack a few more restrictions on top.
Rate Limiting with limit_req
Use the limit_req module to cap the access rate per IP and blunt brute-force attempts:
1 | http { |
limit_req_zone defines a rate-limiting zone named one (10m is the shared memory that stores per-IP rate state), and limit_req applies it in the location: it allows 5 burst requests and rejects anything over speed immediately.
The effect of rate limiting on “slow brute-forcing” can be measured directly. With rate=1r/s and burst=3, five requests fired back to back:

Once the burst quota was exhausted, the 5th request got a 503 outright.
A Custom Authentication Failure Page
By default, failed authentication returns a 401 Unauthorized error page. To swap in your own page, use the error_page directive:
1 | server { |
error_page 401 /custom_401.html; redirects to the custom page when authentication fails, and internal guarantees the page can only be reached through an nginx internal redirect. In testing, the response body the client received on authentication failure was exactly this custom page.
IP Whitelisting with allow/deny
When your sources are relatively fixed, narrow the IP range with allow/deny first, then layer authentication on top:
1 | server { |
allow 10.99.0.0/16; admits addresses from 10.99.0.0 to 10.99.255.255, and deny all; rejects every other source. In testing, sources outside the whitelist got an immediate 403 (error.log logged access forbidden by rule) without even seeing the authentication dialog:

A Comparison: Two Wrong Ways to Do It
Mistake one: the password file is unreadable to worker processes
If you generate the file with htpasswd as root and casually chmod it to 640, the worker processes (which usually run as the nginx user) can no longer read it:
1 | # Wrong: root:root 640, worker read fails |
The configuration is syntactically perfect, but in testing every request that carried credentials returned 500—the problem isn’t syntax, it’s permissions. When troubleshooting, check error.log first: the run logged [crit] open() "/etc/nginx/.htpasswd" failed (13: Permission denied), pointing straight at the password file itself. After restoring the permissions to 644 or reassigning the file to the worker user, the same credentials immediately returned 200 again.

Mistake two: simulating the backend with return in the same location
When testing whether a configuration takes effect, some people put return 200 "OK"; in the location instead of proxy_pass—and then get a 200 without any credentials, wrongly concluding that auth_basic is broken. The cause is processing order: return runs in the rewrite phase, while auth_basic runs in the access phase after it, so the request never survives long enough to reach authentication. Testing auth_basic requires actual content served in the content phase (proxy_pass or a static file).
Things to Watch Out For
- Credentials are base64-encoded, not encrypted. Basic auth passes credentials through an HTTP header, which amounts to transmitting them in the clear. Put it behind HTTPS without exception, and never use it to protect sensitive information.
- Password file permissions are a hidden mine. Correct syntax doesn’t mean working configuration: when workers can’t read the password file, the whole site returns 500 (verified above). After fixing permissions, run
nginx -tbefore reloading, then curl through the flow once more. - Rate limiting and whitelisting come before authentication. limit_req slows brute-force attempts, allow/deny cuts down the source range, and authentication only does the final check—stack the three layers as needed.
- Switch mechanisms for high-concurrency, large-scale scenarios. Every request reads the password file and computes a hash, which doesn’t pay off at high volume; move to a more secure and efficient authentication mechanism instead.
Advanced Usage on OpenResty
OpenResty supports embedding Lua scripts, which pushes authentication logic up to the level of querying databases or integrating OAuth. Here’s an example that validates a username and password directly in code:
1 | http { |
Note: the original Chinese article showed
ngx.headerWWW-Authenticatehere—the quotes and square brackets were lost in formatting. It has been restored tongx.header["WWW-Authenticate"]per the OpenResty ngx.header API.
The script flow: after grabbing the Authorization header, decode the base64 first (the Basic prefix takes 6 characters, so sub(7) starts from position 7), split out the username and password, compare them, and return 401 on a mismatch.
References
- Nginx Documentation: HTTP Basic Auth
- OpenResty Documentation
- Apache HTTP Server Documentation
- Nginx Documentation: Limit Request
- Nginx Documentation: Error Page
Summary
- auth_basic answers “who may open this page” and suits dev/test environments, internal admin systems, and temporarily protected resources.
- Its credentials are base64-encoded, not encrypted—always sit it behind HTTPS and never use it to protect sensitive information.
- When authentication alone isn’t enough, combine limit_req against brute force, error_page for a custom failure page, and allow/deny to narrow sources as needed.
- For more complex authentication logic, OpenResty’s Lua scripting is a ready-made extension point.
- Verify in the field starting from the basic combination: 401 without credentials, 200 with the right ones—once both curls pass, layer on rate limiting and the whitelist step by step.
This article was rewritten from the author’s CSDN blog posts originally published between 2020 and 2024.