caddy
caddy copied to clipboard
HTTP3 + `strict_sni_host` always results in `403` status
I use curl -I --http3 https://localhost/index
when strict_sni_host off, status is 200 from reverse_proxy when strict_sni_host on, status is 403 from caddy
my Caddyfile:
{
auto_https disable_redirects
servers {
protocol {
experimental_http3
strict_sni_host
}
}
}
localhost {
reverse_proxy localhost:3000
}
What's in your logs? Caddy will log an error when SNI does not match the HTTP Host. It will read something like "strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ".
https://github.com/caddyserver/caddy/blob/a056fcd7ba64b6f313f08ec7a48536c726ca432b/modules/caddyhttp/server.go#L280-L299
I use caddy run, and use Caddyfaile
{
debug
auto_https disable_redirects
servers {
protocol {
experimental_http3
strict_sni_host
}
}
log {
output stderr
}
}
localhost {
reverse_proxy localhost:3000
}
but no any logs output for this request
header:
HTTP/3 403
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000
@francislavoie
Try adding this to your site block:
handle_errors {
respond "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}"
}
I think the error is being passed through the error handler chain, which ends up dropping the error message without being logged by default 🤔
localhost {
handle_errors {
respond "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}"
}
reverse_proxy localhost:3000
}
still nothing output, can you test it?
in addition, how can I use http3 only without lowering to http/2?
I don't have a build of curl with HTTP/3 for Windows right now. I was able to get Firefox to connect over HTTP/3 one time, but it seems to want to pin itself to HTTP/2 😞
I think the reason handle_errors isn't printing anything is because the error route has a localhost host matcher (see the output of caddy adapt --pretty to see what I mean) when using the Caddyfile, so to get around that I'm trying to use this JSON config with the matcher removed to see if I can get the error to appear:
{
"logging": {
"logs": {
"default": {
"writer": {
"output": "stderr"
},
"level": "DEBUG"
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Foo",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Code:{http.error.status_code} StatusText:{http.error.status_text} Message:{http.error.message}",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
},
"automatic_https": {
"disable_redirects": true
},
"strict_sni_host": true,
"experimental_http3": true
}
}
}
}
}
Run Caddy with caddy run --config caddy.json with the above, and try again. Hopefully that should get further.
😢still nothing output
~ /usr/local/opt/curl/bin/curl -I --http3 https://localhost
HTTP/3 403
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000
😢still nothing output
~ /usr/local/opt/curl/bin/curl -I --http3 https://localhost HTTP/3 403 server: Caddy alt-svc: h3=":443"; ma=2592000,h3-34=":443"; ma=2592000,h3-32=":443"; ma=2592000,h3-29=":443"; ma=2592000
NEW FOUND:
/usr/local/opt/curl/bin/curl --http3 https://localhost
Code:403 StatusText:Forbidden Message:{http.error.message}
Alright welp, that wasn't too useful.
I guess next thing to try, if you don't mind making some code changes and building Caddy, you can add this just before the fmt.Errorf line in modules/caddyhttp/server.go to force Caddy to print out that message:
fmt.Printf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ", r.TLS.ServerName, hostname)
Building Caddy is easy, just clone the repo, cd cmd/caddy then run go build. Then run with ./caddy run etc.
Alright welp, that wasn't too useful.
I guess next thing to try, if you don't mind making some code changes and building Caddy, you can add this just before the
fmt.Errorfline to force Caddy to print out that message:fmt.Printf("strict host matching: TLS ServerName (%s) and HTTP Host (%s) values differ", r.TLS.ServerName, hostname)Building Caddy is easy, just clone the repo,
cd cmd/caddythen rungo build. Then run with./caddy runetc.
🤣 I download binary from host directly without sources
Interesting, while scanning through our old issues, I found https://github.com/caddyserver/caddy/issues/4097 which is the same issue (strict_sni_host is implicitly enabled when client auth is enabled).
With caddy config
{
"apps": {
"http": {
"servers": {
"srv0": {
"strict_sni_host": true,
"experimental_http3": true,
"automatic_https": {
"disable_redirects": true
},
"listen": [
":443"
],
"logs": {},
"errors": {
"routes": [
{
"handle": [
{
"body": "{http.error}",
"handler": "static_response"
}
],
"terminal": true
}
]
},
"routes": [
{
"handle": [
{
"body": "hello,world!\n",
"handler": "static_response"
}
],
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
},
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
}
}
The response is (client: quic-go)
{id=3vhxga5h9} caddyhttp.(*Server).enforcementHandler (server.go:292): HTTP 403: strict host matching: TLS ServerName () and HTTP Host (localhost) values differ
In caddy log, server_name of the request is also missing
{"request": {"remote_addr": "127.0.0.1:61399", "proto": "HTTP/3", "method": "GET", "host": "localhost:443", "uri": "/", "headers": {"Accept-Encoding": ["gzip"], "User-Agent": ["quic-go HTTP/3"]}, "tls": {"resumed": false, "version": 0, "cipher_suite": 0, "proto": "", "proto_mutual": true, "server_name": ""}}
Thanks @xiruizhao! That's helpful. I don't know why SNI would be empty though, that's pretty strange.
@marten-seemann I hate to always call you for help when it's HTTP/3 related :sweat_smile: but does this tell you anything? Could you think of a reason why SNI would be empty for HTTP/3 requests but not for HTTP/2 etc? Is Caddy doing something wrong that would prevent that from being kept?
This is probably due to the fact that we can't fill the tls.ConnectionState on HTTP/3 requests, see https://github.com/lucas-clemente/quic-go/issues/2879. Any idea how to best proceed here?
Hmm. Should we just skip strict_sni_host for HTTP/3 then? 🤔
What do you think @mholt?
We should probably not skip it because it's a security feature:
https://github.com/caddyserver/caddy/blob/ce5a45db458166ff71cb776d70fd63e28196aafa/modules/caddyhttp/app.go#L170-L185
But I have no idea what to say otherwise, I don't understand HTTP/3 well enough to suggest anything :disappointed:
The right fix would be to fix crypto/tls, such that one can call ConnectionState() at any time during the handshake. Unfortunately, that's not an easy thing to do.
Alternatively, we could fill in a fake tls.ConnectionState, that at least sets the ServerName.
What parts of ConnectionState aren't necessarily ready yet? Are PeerCertificates going to be in there by that point?
The code in question in Caddy is here:
https://github.com/caddyserver/caddy/blob/a056fcd7ba64b6f313f08ec7a48536c726ca432b/modules/caddyhttp/server.go#L280-L299
So clearly r.TLS is set, so there is a ConnectionState but it just seems incomplete?
No, PeerCertificates would be something that we'd need to get from crypto/tls.
Now that https://github.com/quic-go/quic-go/pull/3636 is merged and released, we might be able to fix this one.
Actually, I think this is fixed for free (no additional code changes). I can't replicate the problem with the latest version of quic-go. I get successful responses with strict_sni_host turned on (and the Host is correctly matching SNI), whereas it fails on v2.6.2.
Oh that is music to my ears
I'm glad to hear that. Making ConnectionState work during the handshake was no fun! (Code: https://github.com/quic-go/qtls-go1-20/commit/352e42f14f1bb2ddc88be1305c6f9d25d49916bd)
Thank you so much for your effort, @marten-seemann -- it is very, very much appreciated!