How to basic_auth for only one specific subdomain?

1. The problem I’m having:

I have multiple subdomains in caddyfile but I want to add basic_auth just to one utm-builder-dev.repina.eu. I tried multiple ways where to put basic_auth but I can’t get it to work just for one specific domain. The Authentication does not get served when visiting the page.

So far I only managed to get it to work on all subdomains by placing the basic_auth block inside *.repina.eu. But that’s not what I want.

I just switched to using caddy, so would appreciate any pointers. There is no example for subdomains in the documentation.

Thank you!

2. Error messages and/or full log output:

no errors

3. Caddy version:

2.9.1

4. How I installed and ran Caddy:

a. System environment:

Installed in LXC on proxmox.

b. Command:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

c. Service/unit/compose file:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

d. My complete Caddy config:

utm-builder-dev.repina.eu {
	tls /etc/caddy/certs/repina_eu-origin-cert.pem /etc/caddy/certs/repina_eu-private-key.key

	basic_auth {
	juronja pass
	}
	reverse_proxy 192.168.84.16:3131
	header -Server
	header {
		# This tells the client to store responses for one week.
		Cache-Control max-age=604800
		# Set the referrer policy to send the origin only when making cross-origin requests.
		Referrer-Policy origin-when-cross-origin
		# Enable HSTS (HTTP Strict Transport Security) to force HTTPS for one year including subdomains.
		Strict-Transport-Security max-age=31536000 includeSubDomains
		# Prevent the site from being embedded in an iframe, mitigating clickjacking.
		X-Frame-Options DENY
		# Prevent MIME type sniffing, mitigating certain security vulnerabilities.
		X-Content-Type-Options nosniff
		# This will deny website access to the listed hardware features for security purposes.
		Permissions-Policy autoplay=(), camera=(), microphone=(), midi=(), usb=()
		# Block XSS attacks to some degree using a report-only CSP.
		Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' cdn.jsdelivr.net fonts.googleapis.com 'unsafe-inline'; img-src 'self' data:; font-src 'self' cdn.jsdelivr.net fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;"
	}
	request_body {
		max_size 100KB
	}
	rate_limit {
		# distributed # only needed if multiple caddy instances
		zone limit_by_ip {
			key {remote_host}
			events 20
			window 10s
		}
	}
}

*.repina.eu {
	tls /etc/caddy/certs/repina_eu-origin-cert.pem /etc/caddy/certs/repina_eu-private-key.key

	header -Server
	header {
		# This tells the client to store responses for one week.
		Cache-Control max-age=604800
		# Set the referrer policy to send the origin only when making cross-origin requests.
		Referrer-Policy origin-when-cross-origin
		# Enable HSTS (HTTP Strict Transport Security) to force HTTPS for one year including subdomains.
		Strict-Transport-Security max-age=31536000 includeSubDomains
	}

	@ha host ha.repina.eu
	handle @ha {
		reverse_proxy 192.168.3.2:8123
		header {
			# This will deny website access to the listed hardware features for security purposes.
			Permissions-Policy autoplay=(), midi=()
			# Block XSS attacks to some degree using a report-only CSP.
			Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: basemaps.cartocdn.com brands.home-assistant.io; font-src 'self'; connect-src 'self' raw.githubusercontent.com; upgrade-insecure-requests;"
		}
		rate_limit /auth/authorize* {
			# distributed # only needed if multiple caddy instances
			zone limit_by_ip {
				key {remote_host}
				events 20
				window 10s
			}
		}
	}

	@dilute host dilute.repina.eu
	handle @dilute {
		reverse_proxy 192.168.84.15:7474
		header {
			# Prevent the site from being embedded in an iframe, mitigating clickjacking.
			X-Frame-Options DENY
			# Prevent MIME type sniffing, mitigating certain security vulnerabilities.
			X-Content-Type-Options nosniff
			# This will deny website access to the listed hardware features for security purposes.
			Permissions-Policy autoplay=(), camera=(), microphone=(), midi=(), usb=()
			# Block XSS attacks to some degree using a report-only CSP.
			Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' cdn.jsdelivr.net 'unsafe-inline'; img-src 'self' data:; font-src 'self' cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;"
		}
		request_body {
			max_size 100KB
		}
		rate_limit {
			# distributed # only needed if multiple caddy instances
			zone limit_by_ip {
				key {remote_host}
				events 10
				window 5s
			}
		}
	}

	# @utm-dev host utm-builder-dev.repina.eu
	# handle @utm-dev {
	# 	reverse_proxy 192.168.84.16:3131
	# 	header {
	# 		# Prevent the site from being embedded in an iframe, mitigating clickjacking.
	# 		X-Frame-Options DENY
	# 		# Prevent MIME type sniffing, mitigating certain security vulnerabilities.
	# 		X-Content-Type-Options nosniff
	# 		# This will deny website access to the listed hardware features for security purposes.
	# 		Permissions-Policy autoplay=(), camera=(), microphone=(), midi=(), usb=()
	# 		# Block XSS attacks to some degree using a report-only CSP.
	# 		Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' cdn.jsdelivr.net fonts.googleapis.com 'unsafe-inline'; img-src 'self' data:; font-src 'self' cdn.jsdelivr.net fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;"
	# 	}
	# 	request_body {
	# 		max_size 100KB
	# 	}
	# 	rate_limit {
	# 		# distributed # only needed if multiple caddy instances
	# 		zone limit_by_ip {
	# 			key {remote_host}
	# 			events 20
	# 			window 10s
	# 		}
	# 	}
	# 	basic_auth {
	# 	juronja pass
	# 	}
	# }
}

5. Links to relevant resources:

Here’s an example to see the structure, you can ignore the UUIDs as I have used a template to generate this:

*.example.com {
	@ee272cce-035e-45e5-a824-dee5d6082adf {
		host test.example.com
	}
	handle @ee272cce-035e-45e5-a824-dee5d6082adf {
		basic_auth {
			User $2y$10$P8jL/SAZGXzuNkQic7rbauIEMoJtlv2YMVn6CciqXGftK6W4kUwT6
		}
		reverse_proxy 192.168.1.1
	}
}
3 Likes

I don’t see any obvious issue with your setup. Here’s a test example I put together to simulate yours:

{
	http_port 8080
}

:8080 {
	basic_auth {
		## Username "Bob", password "hiccup"
		Bob $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
	}
	reverse_proxy localhost:8081
}

## Some test back-end
:8081 {
	respond "Alive!"
}

And the test:

$ curl -I http://localhost:8080
HTTP/1.1 401 Unauthorized
Server: Caddy
Www-Authenticate: Basic realm="restricted"
Date: Sun, 06 Apr 2025 22:12:50 GMT

$ curl http://localhost:8080 -u Bob:hiccup
Alive!

When I check your server, I also get a 401 with the www-authenticate header:

$ curl -I https://utm-builder-dev.repina.eu
HTTP/2 401
...
www-authenticate: Basic realm="restricted"
...

So just a thought: are you testing your Basic Auth in a fresh browser session? Browsers tend to cache Basic Auth login credentials. If you’ve already logged in once and haven’t closed the browser, it might be reusing those credentials silently. Just something to double-check in case that’s what’s going on.

1 Like

All this time I thought nesting basic_auth under handle was my problem.

Thank you @timelordx it’s probably the cache, I was trusting the incognito window, hence never questioned the cache. But just opened edge and there it works. I probably made too many modifications in the last days and should clean up the cache thoroughly and look into incognito settings too.

Gosh, I thought I was going crazy, but a rookie mistake it seems. Thank you for the pointers to both of you. :folded_hands: