
Dec 23, 2025
When you deploy a web application, not all content should be publicly accessible. You might have:
An admin dashboard that only your team should access
A staging environment for testing before production
Internal tools (logs, metrics, deployment systems)
Temporary access for contractors or partners during a specific project
Without access control, anyone on the internet who finds these URLs can access them. Basic Authentication is the simplest way to protect these routes.
Basic Authentication (often called "Basic Auth") works by requiring visitors to enter a username and password before accessing protected content. It's the simplest form of password protection on the web.
Think of it like:
No authentication: Unlocked front door, anyone can walk in
Basic authentication: Locked front door, need a key (username/password) to enter
OAuth/JWT: Complex security system with multiple layers
Caddy makes Basic Authentication trivially simple—just a few lines of configuration. No plugins, no external dependencies, no complex setup.
User visits https://example.com/admin
↓
server checks: "Is /admin protected?"
↓
YES → Server asks for username/password
↓
Browser shows login popup:
Username: [________]
Password: [________]
↓
User enters credentials
↓
Browser sends username + password in HTTP header
↓
Server verifies: Is the password correct?
↓
YES → Allow access to /admin
NO → Show "Unauthorized" error
When you enter credentials in the Basic Auth popup, the browser automatically converts them to a format that's sent to the server.
Plain text version (what you see):
username: admin
password: MySecurePassword123!
What gets sent over the network (Base64 encoded):
Authorization: Basic YWRtaW46TXlTZWN1cmVQYXNzd29yZDEyMyE=
The browser Base64-encodes (not encrypts) the credentials. This is why Basic Auth always requires HTTPS—without encryption, anyone on the network can decode the Base64 and see your password.
Caddy requires passwords to be stored as bcrypt hashes, not plain text. This is a critical security feature.
❌ Storing plain text (DANGEROUS):
admin:MyPassword123
manager:BossPassword456
Problems:
If someone accesses your Caddyfile, they get all passwords
If your server is breached, all passwords are compromised
You can see (and misuse) employee passwords
✅ Storing bcrypt hashes (SECURE):
admin:$2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
manager:$2a$12$N3jZy8R2pLmXqWzYk9vP2.x7S8yH9Q3wKmL4T5rN6sU8vC1dE9g8q
Benefits:
Passwords are mathematically irreversible
Even if someone accesses the Caddyfile, they can't use the hashes
You can't recover or misuse passwords
If your server is breached, attackers get only useless hashes
Bcrypt is a special one-way encryption algorithm:
Hashing: Password → Hash (one-way process, can't reverse)
"MyPassword123" → $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
Verification: Password is hashed again and compared to stored hash
User enters: "MyPassword123"
Caddy hashes it: $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
Caddy compares: Does it match the stored hash?
YES → Access granted
NO → Access denied
Key point: The actual password is never stored or transmitted. Only the hash is stored.
Caddy provides a convenient tool to generate bcrypt hashes:
caddy hash-password
Interactive process:
$ caddy hash-password
Enter password:
*** (your password is hidden as you type)
Confirm password:
*** (confirm by typing again)
Output:
$2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
Copy this hash and paste it into your Caddyfile.
For multiple users:
# For admin user
$ caddy hash-password
Enter password: admin123
Output: $2a$12$abc123...
# For manager user
$ caddy hash-password
Enter password: manager456
Output: $2a$12$def456...
# For viewer user
$ caddy hash-password
Enter password: viewer789
Output: $2a$12$ghi789...
Then use them in your Caddyfile:
basicauth {
admin $2a$12$abc123...
manager $2a$12$def456...
viewer $2a$12$ghi789...
}
If someone leaves the team or a password is compromised:
Generate a new hash: caddy hash-password
Update the Caddyfile with the new hash
Reload Caddy: caddy reload
The old password no longer works
Your Node.js app has an admin panel at /admin that only your team should access:
example.com {
handle_path /admin/* {
basicauth {
admin $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
What this does:
/admin/* routes require username: admin + password
All other routes are publicly accessible
No code changes needed in your Node.js app
Test your app before going to production without exposing it to the internet:
staging.example.com {
basicauth {
dev $2a$12$abc123...
qa $2a$12$def456...
}
reverse_proxy localhost:3001
}
Your whole staging site is protected. Anyone needing access gets a username/password.
Keep your monitoring dashboard, logs, or deployment tools private:
monitoring.example.com {
basicauth {
monitor $2a$12$xyz789...
}
reverse_proxy localhost:9090
}
Only people with the password can see your monitoring system.
Give a contractor access for 3 months:
example.com {
handle_path /contractor/* {
basicauth {
contractor $2a$12$temp123...
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
When the contract ends, remove the contractor user from the Caddyfile and reload.
Protect all routes on a domain. Useful for staging environments or internal sites.
example.com {
basicauth {
admin $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
}
reverse_proxy localhost:3000
}
What this requires:
Username: admin
Password: (whatever you hashed to get that long string)
Testing:
# Wrong password
$ curl https://example.com
# Response: Unauthorized
# Correct password
$ curl -u admin:MyPassword123 https://example.com
# Response: Your app's content
internal.example.com {
basicauth {
admin $2a$12$abc123...
manager $2a$12$def456...
viewer $2a$12$ghi789...
}
reverse_proxy localhost:3000
}
All three users can access the site, but only with their own passwords. At this level, they all get the same access (Caddy doesn't differentiate). For role-based access (admin vs. viewer permissions), you'd handle that in your application code.
dev.example.com {
basicauth {
dev1 $2a$12$abc123...
dev2 $2a$12$def456...
dev3 $2a$12$ghi789...
qa $2a$12$jkl012...
}
reverse_proxy localhost:3000
}
Each team member gets their own username/password. They can still access the same content, but you can track who logged in (from logs).
Only protect certain paths. Most routes are public, but sensitive ones require authentication.
example.com {
handle_path /admin/* {
basicauth {
admin $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
Access behavior:
Public user visits:
/products → ✅ Allowed (no auth)
/about → ✅ Allowed (no auth)
/admin → ❌ Blocked, asks for password
/admin/users → ❌ Blocked, asks for password
Authenticated user visits (enters password):
/admin → ✅ Allowed
/admin/settings → ✅ Allowed
example.com {
# Admin panel protected
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
# API for internal use protected
handle_path /api/internal/* {
basicauth {
api $2a$12$def456...
}
reverse_proxy localhost:3000
}
# Health check endpoint protected
handle_path /health {
basicauth {
monitor $2a$12$ghi789...
}
reverse_proxy localhost:3000
}
# Everything else public
handle {
reverse_proxy localhost:3000
}
}
Access patterns:
| Route | Public? | Auth Required |
|---|---|---|
/products | ✅ Yes | No |
/about | ✅ Yes | No |
/admin/* | ❌ No | Yes (admin user) |
/api/internal/* | ❌ No | Yes (api user) |
/health | ❌ No | Yes (monitor user) |
Protect individual routes within your app:
example.com {
# Protect specific endpoints
handle_path /logs {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
handle_path /metrics {
basicauth {
monitor $2a$12$def456...
}
reverse_proxy localhost:3000
}
# Public routes
handle {
reverse_proxy localhost:3000
}
}
Different authentication for different deployments.
Don't protect dev (faster testing), but protect production:
# Development - no authentication
dev.example.com {
reverse_proxy localhost:3000
}
# Staging - light authentication
staging.example.com {
basicauth {
dev $2a$12$abc123...
}
reverse_proxy localhost:3001
}
# Production - strong authentication
api.example.com {
basicauth {
admin $2a$12$def456...
manager $2a$12$ghi789...
}
reverse_proxy localhost:3002
}
# Dev environment - simple password
dev-api.example.com {
basicauth {
dev dev123
}
reverse_proxy localhost:3000
}
# Production environment - strong password
api.example.com {
basicauth {
admin $2a$12$StrongHashHereVeryLongString...
}
reverse_proxy localhost:3000
}
shop.example.com {
# Public shop - no authentication
handle_path /products* {
reverse_proxy localhost:3000
}
handle_path /checkout* {
reverse_proxy localhost:3000
}
handle_path /account* {
reverse_proxy localhost:3000
}
# Admin panel - protected
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
manager $2a$12$def456...
}
reverse_proxy localhost:3000
}
# Analytics - protected
handle_path /analytics {
basicauth {
analyst $2a$12$ghi789...
}
reverse_proxy localhost:3000
}
# Everything else public
handle {
reverse_proxy localhost:3000
}
}
app.example.com {
# Public landing page
handle_path / {
reverse_proxy localhost:3000
}
# User app - users authenticate via app login
handle_path /dashboard* {
reverse_proxy localhost:3000
}
# Admin panel - HTTP Basic Auth
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
support $2a$12$def456...
}
reverse_proxy localhost:3000
}
# Monitoring - HTTP Basic Auth
handle_path /monitoring/* {
basicauth {
ops $2a$12$ghi789...
}
reverse_proxy localhost:3000
}
}
api.example.com {
# Public API endpoints (no auth)
handle_path /api/v1/public/* {
reverse_proxy localhost:3001
}
# Private API endpoints (Basic Auth)
handle_path /api/v1/private/* {
basicauth {
partner1 $2a$12$abc123...
partner2 $2a$12$def456...
internal $2a$12$ghi789...
}
reverse_proxy localhost:3001
}
# Admin API (strict auth)
handle_path /api/admin/* {
basicauth {
admin $2a$12$jkl012...
}
reverse_proxy localhost:3001
}
}
# ❌ WRONG - Never do this!
basicauth {
admin MyPassword123
manager BossPassword456
}
Why it's dangerous:
Anyone with Caddyfile access gets all passwords
If the file is leaked, all passwords are compromised
You can see (and misuse) employee passwords
No security benefit
Solution: Always use hashed passwords:
# ✅ CORRECT
basicauth {
admin $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
manager $2a$12$N3jZy8R2pLmXqWzYk9vP2.x7S8yH9Q3wKmL4T5rN6sU8vC1dE9g8q
}
Basic Auth sends credentials in Base64 encoding (easily reversible, not encrypted). Without HTTPS, anyone on the network can intercept and decode your password.
# ❌ WRONG - HTTP only (insecure)
example.com {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
An attacker on the same WiFi can see:
Authorization: Basic YWRtaW46TXlQYXNzd29yZA==
# Decodes to: admin:MyPassword
Solution: Always use HTTPS:
# ✅ CORRECT - HTTPS (secure)
example.com {
# Caddy auto-enables HTTPS and gets certificates
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
Caddy automatically enables HTTPS for all domains.
Don't require authentication for routes that should be public:
# ❌ WRONG - Protects everything
example.com {
basicauth {
user $2a$12$abc123...
}
handle {
reverse_proxy localhost:3000
}
}
Now users can't visit your site without a password, which:
Hurts SEO (search engines can't crawl)
Frustrates users
Makes sharing links impossible
Solution: Only protect what needs protection:
# ✅ CORRECT - Only protect admin
example.com {
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
# ❌ WRONG - Can't tell who logged in
basicauth {
user1 $2a$12$abc123...
user2 $2a$12$abc123... # Same hash!
user3 $2a$12$abc123... # Same hash!
}
Problems:
Can't track who accessed what
If one person's credentials are shared, can't revoke just theirs
Audit trail is useless
Solution: Unique passwords for each user:
# ✅ CORRECT
basicauth {
admin $2a$12$abc123... # Only for admin
manager $2a$12$def456... # Only for manager
viewer $2a$12$ghi789... # Only for viewer
}
Each user changes their password independently.
After someone leaves the company, leaving their account active:
# ❌ WRONG - Ex-employee still has access
basicauth {
alice $2a$12$abc123... # Left company 6 months ago
bob $2a$12$def456... # Currently employed
charlie $2a$12$ghi789... # Left yesterday
}
Solution: Remove accounts when people leave:
# ✅ CORRECT
basicauth {
bob $2a$12$def456... # Currently employed only
}
Or if they might need temporary access, immediately invalidate their password by:
Removing the user from Caddyfile
Running caddy reload
Deploy auth rules without testing, and you might lock yourself out:
# ❌ WRONG - You don't know what username/password works!
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
Solution: Test before deploying:
# Generate a password you know
$ caddy hash-password
Enter password: test123
Output: $2a$12$xyz789...
# Test locally
curl -u admin:test123 http://localhost/admin
# Should work!
# Then use in production
See who's accessing your protected routes:
example.com {
log {
output stdout
format json
}
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
# If running as service
sudo journalctl -u caddy -f
# If running in Docker
docker logs -f caddy-container-name
Look for lines like:
{
"request": {
"remote_ip": "203.0.113.10",
"method": "GET",
"uri": "/admin/users",
"proto": "HTTP/2.0"
},
"resp_headers": {
"Authorization": ["Basic ..."]
},
"status": 200
}
This shows who accessed /admin/users and when.
When a password is compromised or someone leaves:
$ caddy hash-password
Enter password: new_secure_password_123
Output: $2a$12$newHashHere...
basicauth {
admin $2a$12$newHashHere... # Updated
manager $2a$12$def456... # Unchanged
}
caddy reload
Zero downtime: Current users stay connected, new users use the new password.
Simply remove their entry:
# ❌ BEFORE - Alice still has access
basicauth {
alice $2a$12$abc123...
bob $2a$12$def456...
}
# ✅ AFTER - Alice removed
basicauth {
bob $2a$12$def456...
}
Then reload:
caddy reload
Alice's old password no longer works.
Protect sensitive routes with both password and IP restrictions:
example.com {
handle_path /admin/* {
# Only from internal network
@internal {
remote_ip 10.0.0.0/8
}
handle @internal {
# Also requires password
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
respond "Access denied" 403
}
handle {
reverse_proxy localhost:3000
}
}
Combined security:
Must be on internal network (IP filter)
AND must know the password (Basic Auth)
Protect login routes with both password and rate limiting:
example.com {
# Public login form
handle_path /login {
rate_limit {
zone login {
key {remote_host}
events 10
window 1m
}
}
reverse_proxy localhost:3000
}
# Admin panel with password
handle_path /admin/* {
basicauth {
admin $2a$12$abc123...
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
# Production website - public
shop.example.com {
reverse_proxy localhost:3000
}
# Admin panel - password protected
admin.example.com {
basicauth {
admin $2a$12$riW1ZFwQ6qxUoRpC6kGUo/Q7yMZeErZvm3Wv0bYl/fSfl.kkVGCzC
manager $2a$12$N3jZy8R2pLmXqWzYk9vP2.x7S8yH9Q3wKmL4T5rN6sU8vC1dE9g8q
}
log {
output stdout
format json
}
reverse_proxy localhost:3001
}
# Staging environment - team access
staging.example.com {
basicauth {
dev $2a$12$abc123...
qa $2a$12$def456...
product $2a$12$ghi789...
}
reverse_proxy localhost:3002
}
# Monitoring - ops team only
monitoring.example.com {
basicauth {
ops $2a$12$jkl012...
}
log {
output stdout
}
reverse_proxy localhost:9090
}
example.com {
# Public routes - no auth
handle_path /products* {
reverse_proxy localhost:3000
}
handle_path /api/public/* {
reverse_proxy localhost:3000
}
# Customer dashboard - app handles login
handle_path /dashboard* {
reverse_proxy localhost:3000
}
# Partner API - Basic Auth
handle_path /api/partners/* {
basicauth {
partner1 $2a$12$abc123...
partner2 $2a$12$def456...
}
reverse_proxy localhost:3000
}
# Internal admin - Basic Auth + IP filtering
handle_path /admin/* {
@internal {
remote_ip 10.0.0.0/8 203.45.67.0/24
}
handle @internal {
basicauth {
admin $2a$12$ghi789...
}
reverse_proxy localhost:3000
}
respond "Admin access restricted" 403
}
# Monitoring - Basic Auth only
handle_path /metrics {
basicauth {
monitor $2a$12$jkl012...
}
reverse_proxy localhost:3000
}
# Everything else public
handle {
reverse_proxy localhost:3000
}
}
When generating password hashes, use strong passwords:
# ❌ WEAK - Easy to guess
$ caddy hash-password
Enter password: admin123
Result: Attackers can guess this in seconds.
# ✅ STRONG - Hard to guess
$ caddy hash-password
Enter password: Y8@kL#mN9pQ$xZ4&bC2!vW6*rT1%sF3
Result: Takes billions of years to guess.
Don't use generic names like "admin":
# ❌ WEAK - Standard username everyone tries
basicauth {
admin $2a$12$abc123...
}
# ✅ STRONG - Unique username
basicauth {
alice $2a$12$abc123...
bob $2a$12$def456...
}
Usernames are sent in clear during authentication, so don't reveal what system they use (don't use "wordpress" for WordPress, "django" for Django, etc.).
Your Caddyfile contains password hashes. Protect it:
# Restrict Caddyfile permissions
sudo chmod 600 /etc/caddy/Caddyfile
# Only root and caddy user can read it
ls -l /etc/caddy/Caddyfile
# -rw------- 1 caddy caddy 2048 Jan 15 12:00 /etc/caddy/Caddyfile
Change passwords monthly or quarterly:
# Generate new password
$ caddy hash-password
Enter password: new_password_2025_01
# Update Caddyfile
# Reload Caddy
caddy reload
Regularly check who's accessing protected routes:
# Check logs for /admin access
sudo journalctl -u caddy | grep "/admin"
# Check for failed authentication attempts
sudo journalctl -u caddy | grep "401\|Unauthorized"
Basic Auth is simple: Caddy makes it trivial to add password protection without any plugins.
Always use hashed passwords: Use caddy hash-password to generate bcrypt hashes, never plain text.
HTTPS is essential: Basic Auth only works safely with HTTPS encryption.
Protect selectively: Only require auth for sensitive routes, keep public routes accessible.
One password per user: Give each person their own username/password for tracking and revocation.
Test before deploying: Verify auth works before pushing to production.
Track access: Enable logging to audit who accesses protected routes.
Change passwords regularly: Update passwords when people leave or periodically rotate them.
Use strong passwords: Make passwords long and random, not easy to guess.
Combine with other security: Layer Basic Auth with IP filtering and rate limiting for maximum protection.
A: Basic Auth is secure when used with HTTPS. However, it's best for:
Internal tools
Staging environments
Temporary access
For public-facing apps, use OAuth 2.0 or session-based authentication in your application.
A: Not directly with Caddy's basicauth directive. For dynamic user management:
Use your application's built-in authentication
Or use OAuth 2.0 providers (Google, GitHub, etc.)
Or add a reverse proxy with authentication module
A: Thousands, but it becomes unwieldy. Each user needs an entry in your Caddyfile:
basicauth {
user1 $2a$12$...
user2 $2a$12$...
user3 $2a$12$...
...
}
For many users, use your application's authentication instead.
A: Users can't recover their password from the hash. Generate a new one:
$ caddy hash-password
Enter password: new_password
Output: $2a$12$newHashHere...
Update the Caddyfile and reload.

29 Dec 2025
Node.js vs Python: Which is Better for Back-End Development?

25 Dec 2025
Top 5 Animated UI Component Libraries for Frontend Developers

24 Dec 2025
Why Most Modern Apps use Kafka