Basic Auth #

When securing web systems, we often need quick access restriction without writing complex authentication code on the backend application side. For example, to protect monitoring dashboards (like Prometheus/Grafana), internal API documentation pages, CMS admin panels, or staging environments.

One of the oldest, simplest, yet very effective methods when applied correctly is HTTP Basic Authentication (commonly called Basic Auth). Nginx supports Basic Auth natively through its built-in ngx_http_auth_basic_module. In this article, we’ll dissect in depth how Basic Auth works at the protocol level, how to create credential files using the htpasswd utility with modern encryption, Nginx access restriction configuration, combining it with IP control using the satisfy logic, and understanding the security limitations of this method before moving to more advanced solutions.

How Does HTTP Basic Authentication Work? #

HTTP Basic Auth is part of the standard HTTP protocol specification (RFC 7617). This mechanism doesn’t require cookies, server-side sessions, or custom HTML login forms. The entire login dialog process is handled directly by the user’s browser.

Here’s a diagram of the HTTP Basic Auth handshake process:

sequenceDiagram
    autonumber
    actor Browser as Browser (Client)
    actor Server as Nginx Server

    Browser->>Server: HTTP GET /admin/ (Without Credentials)
    Server->>Browser: HTTP 401 Unauthorized (Header: WWW-Authenticate)
    Note over Browser: Browser reads the header & shows a login popup dialog
    Note over Browser: User enters username & password
    Browser->>Server: HTTP GET /admin/ (Header: Authorization: Basic ***
    Note over Server: Nginx decodes the Base64 & matches against the .htpasswd file
    Server->>Browser: HTTP 200 OK (Admin Page Opens)

Detailed Flow Explanation: #

  1. Step 1: The user tries to access a protected area (e.g., /admin/) without sending any login information.
  2. Step 2: Nginx detects that the area requires authentication. Nginx rejects the request by sending status HTTP 401 Unauthorized along with the response header WWW-Authenticate: Basic realm="Realm Name". The realm parameter is a text string explaining which area is being accessed.
  3. Step 3: The browser catches the 401 status and the WWW-Authenticate header, stops rendering the page, and shows the browser’s built-in popup dialog asking for username and password.
  4. Step 5: After the user presses the login button, the browser sends the same HTTP GET request again, but this time inserting the Authorization: Basic [credentials] header.
    • Important: The credentials are formatted as username:password then encoded using Base64. For example, if the username is admin and the password is sandi123, the original string is admin:sandi123. Its Base64 encoding is YWRtaW46c2FuZGkxMjM=.
    • The sent header reads: Authorization: Basic YWRtaW...M=.
  5. Step 6: Nginx receives the header, decodes the Base64 string to get the original username and password, then matches the password hash against the list in our server’s credential configuration file.
  6. Step 7: If it matches, Nginx allows the request in and returns the web page with status HTTP 200 OK.

[!WARNING] Because Base64 encoding is not encryption, the string YWRtaW46c2FuZGkxMjM= can easily be decoded back to plaintext by anyone eavesdropping on the network. Therefore, HTTP Basic Auth must run over HTTPS. If run over plain HTTP, user credentials leak instantly on public Wi-Fi networks or through ISPs.


Guide to Creating Credential Files with htpasswd #

To store the list of valid usernames and passwords, Nginx uses the same file format as Apache’s .htpasswd files. We’ll use a command-line tool called htpasswd to manage it.

Step 1: Installing the Apache2 Utils Tools #

The htpasswd utility is bundled in the Apache utility package. Run the following commands to install it:

# On Ubuntu / Debian
sudo apt update
sudo apt install apache2-utils -y

# On CentOS / Rocky Linux / RHEL
sudo dnf install httpd-tools -y

Step 2: Creating a New Credential File #

We’re advised to store this credential file outside the public web document directory (web root) so it can’t be downloaded by the public. The best directory is /etc/nginx/.

Let’s create a new password file by adding the first user (e.g., admin_user):

# The -c flag is used to CREATE (make a new file)
# WARNING: The -c flag will delete an existing file if it's already there!
sudo htpasswd -B -c /etc/nginx/.htpasswd admin_user
  • Why the -B Flag Matters: By default, htpasswd on some operating systems uses the MD5 (apr1) or Crypt algorithm, which has a low security level by modern standards. By adding the -B flag, we force htpasswd to use the bcrypt algorithm. Bcrypt is much safer because it uses a work factor mechanism to fend off mass password-matching (brute-force) attacks.
  • After pressing enter, the terminal will ask us to enter a password for that user twice. The password won’t be displayed on screen while typing.

Step 3: Adding or Removing Additional Users #

If we want to add a second or third user to an existing file, don’t use the -c flag, because it will delete the first user we created. Just type the command without the -c flag:

# Add a second user (without -c)
sudo htpasswd -B /etc/nginx/.htpasswd staff_user

# Remove a user from the file
sudo htpasswd -D /etc/nginx/.htpasswd staff_user

Step 4: Inspecting the Credential File Contents #

We can inspect the .htpasswd file contents using the cat command. Credentials are stored in one-user-per-line format as username:hash_password:

cat /etc/nginx/.htpasswd

The output will look like this:

admin_user:$2y$05$Lw8YwzD2Y.rJgZp...

Notice the $2y$ text at the start of the password hash. This indicates the password is encrypted using the secure bcrypt algorithm.

Step 5: Tightening File Access Permissions #

Nginx needs to read the .htpasswd file to perform verification, but other system users on our server must not be able to read it. Let’s set file ownership to the root user and the Nginx server group (usually www-data or nginx), and restrict its access permissions:

# Change the owner to root and the group to www-data (adjust the group on your OS)
sudo chown root:www-data /etc/nginx/.htpasswd

# Restrict access: Owner can read/write, group can read, others nothing at all
sudo chmod 640 /etc/nginx/.htpasswd

Configuring Basic Auth in Nginx #

Once the credential file is ready, we can apply protection in the Nginx virtual host configuration. We use two main directives:

  • auth_basic: Enables authentication and defines the description text (realm) shown in the browser dialog.
  • auth_basic_user_file: Defines the absolute path to the .htpasswd file we created.

Scenario 1: Protecting the Entire Website #

If we want all domain content (including the main page and all assets) locked behind a password, we place the directives at the server block level:

server {
    listen 443 ssl;
    server_name staging.example.com;

    ssl_certificate     /etc/letsencrypt/live/staging.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/staging.example.com/privkey.pem;

    # Protect the entire site
    auth_basic "Staging Environment - Credentials Required";
    auth_basic_user_file /etc/nginx/.htpasswd;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Scenario 2: Protecting Specific Locations / Directories #

Often we only want to protect certain areas (e.g., the administration folder /admin/ or an internal dashboard /dashboard/), while the main site pages remain freely accessible to the public. We place the directives inside the specific location block:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/html;
    index index.html;

    # Public pages - FREE ACCESS
    location / {
        try_files $uri $uri/ =404;
    }

    # Admin pages - PROTECTED
    location /admin/ {
        auth_basic "Administrator Panel";
        auth_basic_user_file /etc/nginx/.htpasswd;
        
        # Make sure php or proxy_pass inside also inherits this protection
        try_files $uri $uri/ =404;
    }
}

Scenario 3: Turning Off Basic Auth for a Specific Sub-Directory (auth_basic off) #

If we’ve enabled Basic Auth at the server block level (the whole site is locked), but we need one specific sub-directory (e.g., a public image assets folder /images/ or an external system webhook endpoint /webhook/) to be freely accessible without a password, we can turn off Basic Auth in the relevant location block by giving the value off:

server {
    listen 443 ssl;
    server_name app.example.com;

    # Enabled globally across the whole server block
    auth_basic "Restricted Application";
    auth_basic_user_file /etc/nginx/.htpasswd;

    # This endpoint is also locked automatically due to inheritance
    location / {
        proxy_pass http://localhost:3000;
    }

    # Special Webhook API endpoint - FREE ACCESS (Basic Auth turned off)
    location /api/webhook {
        auth_basic off; # Turn off authentication inheritance
        proxy_pass http://localhost:3000/api/webhook;
    }
}

The Satisfy Logic: Combining Authentication with IP Restrictions #

One of Nginx’s most powerful features is the satisfy directive. This directive determines how Nginx treats the combination of two security modules: HTTP Basic Auth and IP-based Access Control Lists (ACL) (the allow and deny directives).

There are two values for the satisfy directive:

  1. satisfy all (Default): The client must meet both conditions. The client must connect from an allowed IP address and enter a correct username & password.
  2. satisfy any: The client only needs to meet one of the conditions. A client coming from a trusted IP is immediately allowed in without a password popup, while clients from other IPs can still get in as long as they have the correct password.

Example Implementation of satisfy any for Collaborative Work Environments #

This scenario is very popular on company staging servers. We want internal developer team members connected via office wired networks or internal VPN connections to access the staging site instantly without the hassle of typing username/password repeatedly. Meanwhile, if they work from outside the office (home IP or public Wi-Fi), they can still log in by entering their credentials.

location /staging/ {
    # Apply the satisfy ANY logic (only one condition needs to be met)
    satisfy any;

    # Condition 1: List of trusted IPs (office internal & VPN)
    allow 10.0.0.0/8;           # Internal VPN Subnet
    allow 192.168.1.0/24;       # Office Wi-Fi Network
    allow 203.0.113.50;         # Office Static Public IP
    deny all;                   # Other IPs must validate the next condition

    # Condition 2: Basic Auth credentials
    auth_basic "Staging Access Outside Network";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://staging_backend;
}

With the configuration above:

  • Requests from IP 203.0.113.50 match the allow 203.0.113.50 rule. Because satisfy any is active, Nginx immediately forwards the request to the backend without showing a login box.
  • Requests from a general home IP (e.g., 182.1.2.3) will pass the IP check and hit the deny all rule (Condition 1 fails). However, Nginx doesn’t immediately block it with a 403 Forbidden status. Nginx switches to checking Condition 2 and shows the login box. If the user enters the correct password, access is still granted.

HTTP Basic Authentication Limitations #

Although HTTP Basic Auth is very easy to use, we must understand some fundamental limitations before relying on it for large-scale system architecture:

  1. No Clean Logout Feature: The HTTP Basic Auth protocol is stateless. Once a user enters a correct password, the browser stores those credentials in its local memory and sends the Authorization header automatically on every subsequent request to that domain. There’s no “Logout” button provided by the browser. To clear the login session, users are forced to close their entire browser window or clear the browser’s browsing history (cache).
  2. User Management Problems: The .htpasswd file is a static local text file on the server. If we have hundreds of users or need centralized login system integration (like LDAP, Active Directory, Google Workspace SSO, or Okta), manually managing .htpasswd files on every Nginx server becomes inefficient.
  3. No Granular Authorization: All users registered in the .htpasswd file have the same access level (all or nothing). We can’t differentiate roles (Role-Based Access Control), e.g., user A can only read while user B can edit.
  4. Brute-Force Vulnerable: The browser login box has no built-in protection against repeated automated login attempts (brute-force). Without additional Rate Limiting configuration (like the limit_req module discussed in the next article), attackers can easily try thousands of password combinations per second.

Advanced Alternative Solutions #

If our application grows large and the limitations above start becoming a problem, we’re advised to migrate to the following solutions:

  • OAuth2 Proxy: A third-party module placed in front of Nginx to verify login tokens from external identity providers (Google, GitHub, Keycloak, Okta) before requests are allowed into Nginx.
  • Nginx Auth Request Module (ngx_http_auth_request_module): Allows Nginx to send an internal subrequest to our custom authentication server to verify whether a request may enter or not before forwarding it to the main destination.

Summary #

  • HTTPS Is Mandatory: Basic Auth passwords are transmitted only with Base64 encoding, without encryption. Always apply Basic Auth over an HTTPS connection so data isn’t intercepted.
  • Use Bcrypt Encryption: When creating credential files with the htpasswd command, always include the -B parameter to secure password storage with the Bcrypt algorithm.
  • Use auth_basic off: We can turn off the inherited login rule on specific sub-directories (like image folders or external webhook API endpoints) with the auth_basic off; directive.
  • The satisfy any Logic: Combine IP control with Basic Auth so internal team members can enter freely without a password, while outside users are still challenged to enter one.
  • Basic Auth for Simple Protection: Remember that Basic Auth has no clean logout feature or granular authorization. Use it only for simple administrative page protection needs, not for our application’s primary user management.

← Previous: HTTP/2   Next: Rate Limiting →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact