Basic Auth #

HTTP Basic Authentication (Basic Auth) is one of the oldest, simplest, and most widely supported authentication methods in the HTTP protocol ecosystem. Although simple, this feature is very efficient for providing the first protection layer on administrative areas, staging servers, internal documentation endpoints, or backend APIs that don’t require complicated user management systems. Caddy provides the built-in basicauth directive, which is very secure by default by requiring advanced bcrypt-based password hashing storage. We’ll discuss in depth the Basic Auth handshake mechanism, the security risks of credential transmission, how to generate secure password hashes, authentication bypass tactics for internal IPs using CIDR notation, reverse proxy interaction, and Basic Auth’s practical limitations to help you decide when to switch to more complex Single Sign-On (SSO) solutions.


How HTTP Basic Auth Works #

The HTTP protocol is connectionless and stateless. To maintain an authenticated state without using cookies or custom JWT tokens, HTTP Basic Auth relies on a standard challenge-response mechanism at the HTTP header level.

Here’s the interaction sequence when a client accesses a page protected by Caddy Basic Auth:

sequenceDiagram
    participant Client as Visitor Browser
    participant Caddy as Caddy Web Server
    participant Backend as Backend Application

    Client->>Caddy: 1. GET /admin/ (No Credentials)
    Caddy-->>Client: 2. HTTP/1.1 401 Unauthorized<br/>WWW-Authenticate: Basic realm="Restricted Area"
    Note over Client: The browser shows a login dialog box
    Note over Client: The user enters their username & password
    Note over Client: The browser encodes the credentials to Base64: username:password
    
    Client->>Caddy: 3. GET /admin/<br/>Authorization: Basic YWxpY2...MTIz
    Note over Caddy: Caddy decodes the Base64 & verifies the bcrypt hash
    Caddy->>Backend: 4. Forward the Request (X-Authenticated-User)
    Backend-->>Caddy: 5. Return the Page Content
    Caddy-->>Client: 6. HTTP/1.1 200 OK (Serve the Content)

The handshake workflow above can be described as follows:

  1. Initial Request: The client browser sends a normal HTTP request to the protected path, e.g., GET /admin/, without including any identity information.
  2. The Caddy Challenge (401 Challenge): Caddy detects that the path requires authentication. Caddy rejects the request by returning the 401 Unauthorized status code along with the WWW-Authenticate: Basic realm="Restricted Area" response header. The realm parameter is an informational string telling the browser which security scope is being accessed.
  3. Client Interaction: The browser catches the 401 response and automatically displays the OS or browser’s built-in login box. The user enters their username and password.
  4. Credential Transmission: The browser combines both texts with a colon separator (username:password) then encodes them into Base64 format. For example, the string alice:secret123 becomes YWxpY2U6c2VjcmV0MTIz. The browser resends the same HTTP request with a new request header: Authorization: Basic YWxpY2....
  5. Verification by Caddy: Caddy reads the Authorization header, extracts the Basic scheme, decodes the Base64 back to the original username and password text, then matches them against the credential database stored in memory. If they match, Caddy processes the request; if wrong, Caddy throws a 401 response again.

Security Risks and the HTTPS/TLS Requirement #

One of the biggest misconceptions about Basic Auth is thinking the sent credentials are safe because they’ve been turned into a random string like YWxpY2U6c2VjcmV0MTIz.

[!DANGER] Base64 is not encryption! Base64 is just an encoding method for converting binary data or special characters into an ASCII string format safe for network transmission. Anyone on the network path (like a malicious ISP provider, public Wi-Fi users on the same network using a packet sniffer, or Man-in-the-Middle attackers) who manages to capture your HTTP packets can decode that Base64 string instantly within milliseconds to get the real password in plaintext form.

Therefore, HTTP Basic Authentication must be used together with HTTPS (SSL/TLS). When HTTPS is active, all HTTP requests — including the Authorization request header — are encrypted before leaving the client computer. Attackers on the external network only see random TLS packets they can’t break.

Fortunately for you, Caddy automatically manages TLS certificates and redirects HTTP traffic to HTTPS by default. However, you must always make sure you access the domain through the https:// protocol and never disable Caddy’s TLS automation in production without an alternative solution (like SSL offloading on a front load balancer).


Creating Password Hashes Using Bcrypt #

To protect the credential database if the Caddyfile configuration file leaks or is read by outsiders, Caddy never stores passwords in plaintext. Caddy requires you to enter password hashes processed with the bcrypt algorithm.

Bcrypt is a one-way hashing algorithm specifically designed for securely storing passwords. Its main advantage is that it’s computationally slow (adaptive hashing) through the cost factor parameter. This makes brute force or rainbow table attacks very expensive and impractical for attackers.

The caddy hash-password Hash Generation Utility #

You can create a bcrypt hash directly from the terminal command line using Caddy’s built-in utility:

# Example 1: Create a hash with the default cost factor (14)
caddy hash-password --plaintext "our-secret-123"

# Example Output:
# $2a$14$8lGvWLMR9jGg2.bSZlAHOeYuI1FjTPExECWQpkLPMH1y0LkJnbEKy

Caddy’s default cost factor is 14. This value takes about 1 to 2 seconds of verification time on modern hardware. If you want to speed up the verification process (e.g., for high-traffic APIs) or slow it down (for extra security), you can adjust the cost (between 4 and 31):

# Example 2: Create a hash with a lower cost factor (12) for faster performance
caddy hash-password --plaintext "our-secret-123" --cost 12

[!TIP] Writing the password directly in the terminal using the --plaintext parameter leaves a history of that password in your terminal logs. To avoid this in production, run the command without extra parameters so Caddy asks for input interactively without displaying characters on screen (no-echo):

caddy hash-password
# Enter password: [Type the password here]
# Confirm password: [Retype the password]
# Output: $2a$14$...

Configuring the basicauth Directive #

After getting the bcrypt hash, you can compose the basicauth directive in your Caddyfile. The basic writing format is as follows:

# ANTI-PATTERN: Storing plaintext passwords directly in the Caddyfile
example.com {
    basicauth {
        # DON'T DO THIS: Caddy will refuse to boot because this isn't a valid bcrypt hash
        alice secret123
    }
}

# CORRECT: Using the bcrypt hash from caddy hash-password
example.com {
    basicauth {
        # Format: [username] [bcrypt_hash]
        alice $2a$14$8lGvWLMR9jGg2.bSZlAHOeYuI1FjTPExECWQpkLPMH1y0LkJnbEKy
        bob   $2a$14$7kFuXLMR9jGg2.bSZlAHOeYuI1FjTPExECWQpkLPMH1y0LkJnbEKy
    }
    
    root * /var/www/html
    file_server
}

Path-Level Protection (Path-Based Authorization) #

By default, if you put the basicauth directive directly inside the site block without specifying a path parameter, Caddy protects the entire website. To limit authentication to only certain areas (e.g., the admin area), you can specify the path directly or leverage the matchers system:

1. Specifying the Path Directly #

You can write the path pattern directly after the basicauth keyword:

# Restrict access only to the /admin/ subdirectory
example.com {
    # Only /admin/ paths and their sub-paths require login
    basicauth /admin/* {
        admin $2a$14$adminBcryptHashHere
    }
    
    # The main page and other routes stay open to the public
    root * /var/www/html
    file_server
}

2. Using a Named Matcher for Path Complexity #

If you have several different areas you want to protect with the same credential accounts, you can define a named matcher using the @protected block:

# Securing several endpoints at once
example.com {
    @protected {
        path /admin/*
        path /internal/*
        path /dashboard/*
        path /metrics
    }
    
    basicauth @protected {
        operator $2a$14$operatorHashHere
        manager  $2a$14$managerHashHere
    }
    
    reverse_proxy localhost:3000
}

Custom Realm #

By default, the browser displays a standard message like “Sign in to access this site” in the login dialog box. You can clarify your instance’s identity or give users additional hints by changing the realm configuration:

# Displaying a custom portal identity
example.com {
    basicauth * {
        # Set a custom Realm
        realm "Internal Administration Portal - PT Creative Tech"
        
        admin $2a$14$adminHashHere
    }
    
    reverse_proxy localhost:3000
}

Authentication Bypass for Internal IP Addresses #

In corporate scenarios, you often face the need where DevOps teams inside the office (local/LAN network) must be able to access internal dashboards directly without logging in. However, if the dashboard is accessed from outside the office (public internet), Basic Auth must remain active challenging users.

You can implement this conditional logic by combining IP-based named matchers and IP negation:

# Authentication bypass for the internal office network
example.com {
    # Define our internal office network IP block
    @outsider {
        not remote_ip 192.168.1.0/24 10.0.0.0/8 127.0.0.1/32
    }
    
    # Only challenge basicauth if the request comes from outside (outsider)
    basicauth @outsider {
        developer $2a$14$devBcryptHashHere
    }
    
    root * /var/www/internal-tools
    file_server
}

The mechanism above works instantly: requests from IP 192.168.1.50 don’t match the @outsider rule so they’re directly served by the file server, while requests from the mobile IP 203.0.113.5 match the criteria and are forced to enter a username and password.


Reverse Proxy Integration and Header Forwarding #

When Caddy acts as a gateway in front of a backend application cluster, Caddy can complete the Basic Auth authentication process on its side, then forward the logged-in user’s identity information to the backend in the form of a custom HTTP header.

This frees your backend code from having to handle heavy Bcrypt hashing, while also making user session tracking integration easier:

# Integrated with a Node.js/Go backend API
admin.example.com {
    basicauth {
        alice $2a$14$aliceHashHere
        bob   $2a$14$bobHashHere
    }
    
    reverse_proxy localhost:9000 {
        # Caddy puts the successfully logged-in username in the {http.auth.user.id} variable
        # We send that username via the custom X-Authenticated-User header
        header_up X-Authenticated-User {http.auth.user.id}
        
        # Remove the original Authorization header so the backend doesn't try re-decoding it
        header_up -Authorization
    }
}

On your backend application side (e.g., using the Go programming language), you can directly read the user identity validated by Caddy:

package main

import (
	"fmt"
	"net/http"
)

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
	// Read the header sent by Caddy
	authenticatedUser := r.Header.Get("X-Authenticated-User")
	
	if authenticatedUser == "" {
		// If a bypass is detected, reject the request defensively
		http.Error(w, "Access denied: Must pass through the Caddy Gateway", http.StatusForbidden)
		return
	}
	
	fmt.Fprintf(w, "Welcome to the Dashboard, %s!", authenticatedUser)
}

func main() {
	http.HandleFunc("/admin/dashboard", dashboardHandler)
	http.ListenAndServe(":9000", nil)
}

Storing Hashes Using Environment Variables #

Writing password hashes directly in the Caddyfile isn’t a best practice if that Caddyfile is stored in a public Git repository. Although Bcrypt hashes are safe, publishing the hash gives attackers the opportunity to attempt offline cracking on their own devices.

To keep the hash secret, you should load it dynamically using Environment Variables:

# Loading hashes from environment variables
example.com {
    basicauth {
        # Caddy reads the ALICE_HASH and BOB_HASH environment variables at startup
        alice {env.ALICE_HASH}
        bob   {env.BOB_HASH}
    }
    
    reverse_proxy localhost:8080
}

You can set these environment variable values in Caddy’s systemd service configuration file:

# Run the systemd override edit command for caddy
sudo systemctl edit caddy

# Add the environment file configuration line under the [Service] block:
# [Service]
# EnvironmentFile=/etc/caddy/caddy.env

Then, create the /etc/caddy/caddy.env file with very strict permissions (chmod 600 so only root can read it):

# /etc/caddy/caddy.env
ALICE_HASH=$2a$14$aliceBcryptHashHere...
BOB_HASH=$2a$14$bobBcryptHashHere...

Basic Auth Limitations and Alternatives #

Although Basic Auth is very practical, you must be aware of its limitations before applying it at a large user scale:

1. No Proper Logout Feature #

The browser’s mechanism for handling Basic Auth is storing credentials in the browser’s internal cache and automatically attaching them on every request to that domain. There’s no standard “Logout” button. The credentials only disappear if the user closes all browser windows (browser restart) or clears the login history.

2. No Layered Security (MFA/2FA) #

Basic Auth doesn’t support multi-factor authentication integration (like Google Authenticator OTP codes or SMS).

3. Rigid Interface Appearance #

The login box is entirely controlled by the browser engine. You can’t add a company logo, change button colors, or insert a forgot password link.

Tactical Decision Matrix #

Use Basic Auth if:
  ✓ Protecting a staging environment from search engine indexing (SEO block).
  ✓ Securing monitoring metric endpoints (like Prometheus /metrics).
  ✓ Providing quick protection on team internal file servers.
  ✓ System users are internal and very few in number (< 5 people).

Consider switching to SSO / Forward Auth if:
  ✗ The service is accessed by external users (clients / customers).
  ✗ You need flexible session lifetimes (session timeouts).
  ✗ You must use LDAP, Active Directory, Google Workspace, or Okta integration.
  ✗ You need self-service features (like user registration and password reset).

For a more complete alternative, you can combine Caddy with external authentication solutions like Authelia or Authentik using the Forward Auth pattern (leveraging Caddy’s forward_auth directive).


Summary #

  • Authentication Mechanism: HTTP Basic Auth works by sending Base64-formatted credentials in the Authorization request header after receiving a 401 Unauthorized response from Caddy.
  • Mandatory Bcrypt Hash: Caddy requires Bcrypt-hash-based password storage. Use the caddy hash-password command to generate secure hashes and avoid plaintext writing.
  • HTTPS Protection: Base64 transmission is very easy to intercept. Basic Auth must run over HTTPS/TLS encryption so credentials don’t leak on the network.
  • Matched Path Navigation: Use the @protected named matcher to selectively apply authentication only to specific administrative routes, leaving public routes freely open.
  • Network Bypass: You can exempt login requirements for users on the internal LAN network by combining the not remote_ip matcher with CIDR notation.
  • Upstream Integration: Use the {http.auth.user.id} variable property to forward successfully logged-in user identities to backend servers via custom HTTP headers.
  • Environment Variables: Keep the Caddyfile repository safe by loading Bcrypt hashes through environment variables isolated using a private systemd EnvironmentFile.

← Previous: Security   Next: Rate Limiting →

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