caddy: HTTP3 Client Certificate 421

With the following configuration (hostnames replaced), and accessing over HTTP3, Caddy responds 421 for subdomain1. I think this is a bug because subdomain1 doesn’t have client_auth enabled.

subdomain1.tld {
        respond * 200 {
                body "Hi"
        }
}
subdomain2.tld {
        tls {
                client_auth {
                        mode require_and_verify
                        trusted_ca_cert_file /etc/caddy/ca.crt
                }
        }
        respond * 200 {
                body "Hi2"
        }
}

Without the tls directive accessing over h3 or with the tls directive accessing over h2, both subdomains can be accessed without issue.

With strict_sni_host insecure_off and the tls directive in place, subdomain1 can be accessed, and subdomain2 gets ERR_QUIC_PROTOCOL_ERROR. I suspect the ERR_QUIC_PROTOCOL_ERROR is probably due to my configuration or method of testing rather than Caddy though (chromium --origin-to-force-quic-on=subdomain1.tld:443,subdomain2.tld:443), given that the browser doesn’t bring up the 'Select a certificate` prompt.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 32 (16 by maintainers)

Most upvoted comments

After upgrading quic-go, this problem is no more in latest caddy.

With the following configuration (hostnames replaced)

It is recommended that you avoid redacting public information like hostnames. And if you really want to, you should be using the .example TLD set aside for placeholders.

strict_sni_host is incompatible with http3 because of stdlib tls state handling, that will depend on stdlib fix.

caddy currently will enable strict_sni_host whenever tls_auth is enabled. Maybe we should track which hostname to check for sni matching in this case @mholt.

Yeah - you probably don’t need to enable h2c though, just specify h1 and h2, to disable h3. Enabling h2c only makes sense for port 80, and only if you have a specific need for it. Most don’t, because TLS is trivial with Caddy and h2c is only needed when the client can only do h2 but not h1, e.g. with gRPC.

Agreed, but that’s still better than leaving it off entirely.

What? 🤔

No, HTTP/3 is definitely very, very optional.

Once the ECH spec is finalized, by the way, I hope Caddy will implement support.

I hope the Go standard library will support it! (That’s also what is holding back this issue.)

1. Environment

1a. Operating system and version

Ubuntu 18.04.6 LTS

1b. Caddy version (run caddy version or paste commit SHA)

v2.6.1 h1:EDqo59TyYWhXQnfde93Mmv4FJfYe00dO60zMiEt+pzo=

2. Description

2a. What happens (briefly explain what is wrong)

HTTP/3 requests are responded to with a 421 status code, as if they had an invalid SNI.

2b. Why it’s a bug (if it’s not obvious)

The SNI seems correct, and this only occurs over HTTP/3. I can’t imagine a legitimate reason that this wouldn’t occur over HTTP/2, so I believe the bug lies with Caddy.

2c. Log output

{"level":"debug","ts":1664046013.5690753,"logger":"events","msg":"event","name":"tls_get_certificate","id":"c5fbdf2f-0814-4e85-be1c-0323fa3e3801","origin":"tls","data":{"client_hello":{"CipherSuites":[60138,4865,4866,4867],"ServerName":"saklad5.com","SupportedCurves":[14906,29,23,24,25],"SupportedPoints":null,"SignatureSchemes":[1027,2052,1025,1283,515,2053,2053,1281,2054,1537,513],"SupportedProtos":["h3"],"SupportedVersions":[23130,772],"Conn":{}}}}
{"level":"debug","ts":1664046013.569236,"logger":"tls.handshake","msg":"choosing certificate","identifier":"saklad5.com","num_choices":1}
{"level":"debug","ts":1664046013.5692725,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"saklad5.com","subjects":["saklad5.com"],"managed":true,"issuer_key":"acme-v02.api.letsencrypt.org-directory","hash":"9118831e6229e304b9326866a978c787d02a3c0b047ff030fe2368d1a5e33149"}
{"level":"debug","ts":1664046013.5692906,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff","remote_port":"60313","subjects":["saklad5.com"],"managed":true,"expiration":1670006168,"hash":"9118831e6229e304b9326866a978c787d02a3c0b047ff030fe2368d1a5e33149"}
{"level":"debug","ts":1664046013.6953883,"logger":"events","msg":"event","name":"tls_get_certificate","id":"08d55306-5f12-471f-8d92-694a22c89437","origin":"tls","data":{"client_hello":{"CipherSuites":[47802,4865,4866,4867],"ServerName":"saklad5.com","SupportedCurves":[6682,29,23,24,25],"SupportedPoints":null,"SignatureSchemes":[1027,2052,1025,1283,515,2053,2053,1281,2054,1537,513],"SupportedProtos":["h3"],"SupportedVersions":[51914,772],"Conn":{}}}}
{"level":"debug","ts":1664046013.6954954,"logger":"tls.handshake","msg":"choosing certificate","identifier":"saklad5.com","num_choices":1}
{"level":"debug","ts":1664046013.6955202,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"saklad5.com","subjects":["saklad5.com"],"managed":true,"issuer_key":"acme-v02.api.letsencrypt.org-directory","hash":"9118831e6229e304b9326866a978c787d02a3c0b047ff030fe2368d1a5e33149"}
{"level":"debug","ts":1664046013.695557,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"192.0.2.0","remote_port":"57879","subjects":["saklad5.com"],"managed":true,"expiration":1670006168,"hash":"9118831e6229e304b9326866a978c787d02a3c0b047ff030fe2368d1a5e33149"}
{"level":"debug","ts":1664046013.6989307,"logger":"http.log.error","msg":"strict host matching: TLS ServerName () and HTTP Host (saklad5.com) values differ","request":{"remote_ip":"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff","remote_port":"60313","proto":"HTTP/3.0","method":"GET","host":"saklad5.com","uri":"/.well-known/security.txt","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15"],"Accept-Language":["en-US,en;q=0.9"],"Accept-Encoding":["gzip, deflate, br"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Priority":["u=0, i"],"Upgrade-Insecure-Requests":["1"]},"tls":{"resumed":false,"version":0,"cipher_suite":0,"proto":"","server_name":""}},"duration":0.001085679,"status":421,"err_id":"88yvk1g40","err_trace":"caddyhttp.(*Server).enforcementHandler (server.go:367)"}

I have the full log, but this is the part that seems specific to the request. If you think there may be more to see, let me know.

I’ve redacted the remote IP addresses, since my current location is irrelevant.

2d. Workaround(s)

Disabling HTTP/3 (or simply opting not to use it when connecting) avoids the issue, as does removing strict_sni_host.

3. Tutorial (minimal steps to reproduce the bug)

  1. Enable HTTP/3 under “Develop → Experimental Features” in Safari 16.0 on macOS.
  2. Visit https://saklad5.com/.well-known/security.txt.

Disabling HTTP/3 will cause the error to vanish.


With other domains excised (it continues after the first site block), the following is my Caddyfile:

{
        debug

        log {
                output file /var/log/caddy/access.log {
                        roll_disabled
                        roll_uncompressed
                }
        }

        auto_https disable_redirects

        acme_ca https://acme-v02.api.letsencrypt.org/directory
        email admin@saklad5.com

        admin unix//run/caddy/caddy-admin.sock

        servers {
                protocols h1 h2 h2c h3
                strict_sni_host
        }
}

(global) {
        @https {
                protocol https
        }
        @unsupported {
                method POST PUT DELETE CONNECT OPTIONS TRACE
        }

        bind 159.223.125.173

        header @https ?Strict-Transport-Security "max-age=63072000; includeSubDomains"
        header ?Content-Security-Policy "base-uri 'none' default-src 'none'"
        header ?Cache-Control "max-age=600; stale-while-revalidate=85800"
        encode zstd br gzip
        error @unsupported 501
}

(files) {
        file_server {
                root /var/www/{args.0}
        }
}

(onion) {
        header @https Alt-Svc "h3=\":443\"; ma=2592000; persist=1, h2=\"{args.1}:443\"; ma=7776000; persist=1"
        header @https Onion-Location "http://{args.0}.{args.1}{http.request.uri.path}"
}

https://saklad5.com http://saklad5.com.{env.ONION__} {
        import global
        import files @
        import onion saklad5.com {env.ONION__}

        bind ::1 2604:a880:400:d0::2204:1

        header @https Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
}

Safari relies on the draft HTTPS DNS record to use HTTP/3, in case that’s relevant. If you don’t feel like manually decoding the wire format, I can give you those records in presentation format.

Given that this is an experimental feature, it is not impossible that the bug is actually Safari’s fault, and the Chrome issue is unrelated.