1. The problem I’m having:
Hello,
I want to create a Caddyfile with mTLS. The difficulty is that I need mTLS for every path, expect /test. This path should be the only path not protected by mTLS. All sites should use my own tls certs in /example/site.cer /example/site.key.
2. Error messages and/or full log output:
I cannot find any documentation to accomplish this.
3. Caddy version:
v2.8.4
4. How I installed and ran Caddy:
docker: caddy:2-alpine:latest
a. System environment:
docker: 24.0.5
b. Command:
docker-compose up -d
c. Service/unit/compose file:
default compose file
d. My complete Caddy config:
not existing.
5. Links to relevant resources:
1 Like
Howdy @user81738912,
This should be doable, but you should note that TLS is negotiated before path comes into play. It’s not really possible for Caddy to change its mTLS behaviour based on what URI the client wants.
So, you’ll need to use a mode that allows for mTLS and non-mTLS clients to connect, and then handle requests based on which URI they want and whether they did authenticate themselves.
verify_if_given
is the mode that allows clients to optionally authenticate, but requires a valid client certificate if they do authenticate themselves. When you set that mode, you can trust that Caddy has discarded inauthentic mTLS requests before they reach your HTTP handling logic and the only clients left are valid-mTLS and non-mTLS.
Then you can handle non-mTLS requests to non-/test
endpoints by issuing a 403 response (or any alternative rejection handling at your preference).
An expression matcher (see: Request matchers (Caddyfile) — Caddy Documentation) can be used to test whether any of the {tls_client_*}
placeholders (see Caddyfile Concepts — Caddy Documentation) exist in order to differentiate non-mTLS clients.
Something like this UNTESTED expression, maybe:
@unauthenticated `!path('/test') && !type({tls_client_issuer}) == string`
respond @unauthenticated 403
1 Like
Thanks for the reply!
I created this Caddyfile:
https://example.com:443 {
tls /certs/fullchain.cer /certs/excample.com.key
client_auth {
mode request
trusted_ca_cert_file /mtls/ca-1-cert.pem
trusted_ca_cert_file /mtls/ca-2-cert.pem
}
}
@unauthenticated `!path('/test') && !type({tls_client_issuer}) == string`
respond @unauthenticated 403
reverse_proxy localhost:8001
}
And got this Error:
Error: loading initial config: loading new config: loading http app module: provision http: server srv0: setting up route handlers: route 14: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading matcher modules: module name 'expression': provision http.matchers.expression: compiling CEL program: ERROR: <input>:1:21: found no matching overload for '!_' applied to '(type(any))'
| !path('/test') && !type(caddyPlaceholder(request, "http.request.tls.client.issuer")) == string
| ....................^
Additionally, how do I get the string mentioned?
Thanks a lot!
Ahh, whoops. Can’t invert with !type()
, what I wanted was to invert type() == string
(i.e. it isn’t a string). But we can do that instead with !=
like so:
@unauthenticated `!path('/test') && type({tls_client_issuer}) != string`
Use the literal word string
there. It’s not an actual string, it’s a type.
What we’re doing is checking if http.request.tls.client.issuer
is a string or not. If it’s a string, it means it was replaced with a value. If it’s not a string, we know it hasn’t actually been set. It’s basically checking “does this placeholder exist”.
1 Like
There a no errors in the log, but now every site except /test get a 403. The /test path works perfectly fine without mTLS.
Hmm. It’s not grouping the path and type checks (which are boolean) into one boolean and then comparing its type to not a string (which would always be true), is it?
Maybe `!path('/test') && (type({tls_client_issuer}) != string)`
(adding the parentheses around the type comparison) might work better?
1 Like
I have now this Caddy file:
https://example.com:443 {
tls /certs/fullchain.cer /certs/excample.com.key {
client_auth {
mode request
trusted_ca_cert_file /mtls/ca-1-cert.pem
trusted_ca_cert_file /mtls/ca-2-cert.pem
}
}
@unauthenticated `!path('/test') && (type({tls_client_issuer}) != string)`
respond @unauthenticated 403
reverse_proxy localhost:8001
}
now all paths are 403.
I’m not sure what’s going on, then, because even without mTLS configured at all that expression should work just fine for /test
.
~/Projects/caddy
➜ caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
~/Projects/caddy
➜ cat Caddyfile
http:// {
@unauthenticated `!path('/test') && (type({tls_client_issuer}) != string)`
respond @unauthenticated 403
}
~/Projects/caddy
➜ curl -i localhost/test
HTTP/1.1 200 OK
Server: Caddy
Date: Sun, 08 Sep 2024 23:42:28 GMT
Content-Length: 0
~/Projects/caddy
➜ curl -i localhost/foo
HTTP/1.1 403 Forbidden
Server: Caddy
Date: Sun, 08 Sep 2024 23:42:29 GMT
Content-Length: 0
1 Like