fix(caddyfile): stop blanking every forwarded TLS/request header - #3124
Merged
Conversation
Each x-tls-* / x-client-ip / x-method / x-remote-* header_up was written as a
two-line pair: the value line plus a `header_up X "^{placeholder}$" ""` guard
intended to blank the header when the placeholder didn't expand. The guard was
self-defeating — Caddy expands placeholders inside the search argument too, so
the regex always equalled the value just set and every stable field was
replaced with "". In production this blanked x-tls-resumed, x-tls-version,
x-tls-cipher, x-tls-server-name, x-client-ip, x-method, x-remote-host, etc. on
100% of requests (verified across 200k recent sessions: x-tls-resumed was ""
or absent on every one). Only x-duration-ms survived, because its value ticks
up between the two evaluations so the regex no longer matched — which is the
tell that identified the mechanism.
Collapse each pair to a single line. Every placeholder is a valid Caddy v2.8.4
shorthand or {http.request.*} field (verified against shorthands.go /
replacer.go) that resolves to its value or an empty string, never a literal,
so the guard is unnecessary.
Also fixes two placeholders that were invalid: {http.request.tls.public_key}
and {http.request.tls.public_key_sha256} do not exist (only the client-cert
namespace has public_key). Left as-is with the guard removed they would have
forwarded a literal; repointed to {http.request.tls.client.public_key[_sha256]},
which resolve to "" unless the client presents a certificate.
After this, x-tls-resumed carries the real TLS session-resumption bit
(true/false) and the other TLS/connection fields populate as intended.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every
x-tls-*/x-client-ip/x-method/x-remote-*header forwarded to the provider was written as a two-lineheader_uppair — the value line plus aheader_up X "^{placeholder}$" ""guard meant to blank the header when the placeholder didn't expand:The guard is self-defeating: Caddy expands placeholders inside the search argument too, so
^{http.request.tls.resumed}$becomes the same value line 1 just set, the regex always matches, and the header is replaced with"".Impact
Every stable TLS/connection field was blanked on 100% of requests —
x-tls-resumed,x-tls-version,x-tls-cipher,x-tls-server-name,x-client-ip,x-method,x-remote-host,x-remote-port. Verified against 200k recent production sessions:x-tls-resumedwas""or absent on every single one.The smoking gun:
x-duration-msuses the same pattern but survives, because its value ticks upward between the two evaluations, so the regex no longer matches. That's what identified the mechanism.req.TLSis fine (chaddy'shandler.gogates onreq.TLS.HandshakeCompleteand populatesx-tls-clienthello), and the placeholders are valid — the values were purely being wiped by the guard.Fix
Collapse each pair to a single line. Every placeholder is a valid Caddy v2.8.4 shorthand or
{http.request.*}field (checked againstshorthands.go/replacer.go), which resolves to its value or an empty string — never a literal — so the guard is unnecessary.Also corrects two genuinely-invalid placeholders:
{http.request.tls.public_key}/{http.request.tls.public_key_sha256}don't exist (Caddy only has them under the client-cert namespace). Left with the guard removed they'd forward a literal{...}; repointed to{http.request.tls.client.public_key[_sha256]}, which resolve to""for the captcha clients (no client certs).Result
x-tls-resumednow carries the realtrue/falsesession-resumption bit, and the other TLS/connection headers populate as intended. Applied to bothprovider.Caddyfileandlocal.Caddyfile; both passcaddy fmt.🤖 Generated with Claude Code