Repository navigation
This patch release is a big batch of bug fixes and security hardening, plus a handful of small additions. This release is cut from our 2.11 maintenance branch: fixes are backported from master. We recommend upgrading, especially if you serve PHP or use {host} in file paths.
Huge thank you to our sponsors for keeping the project alive with resources, and for our maintainers who triage and assist tirelessly in this relentless new age of AI.
Highlights
- Idle timeouts, round two. More fixes for the idle read/write timeouts introduced in 2.11.6:
- Per-route
timeoutsnow take precedence over the server-wideread_body_idle/write_idle, and nestedtimeoutsuse the innermost one. (#8121) - Over HTTP/2, a handler that paused reading the request body (like
reverse_proxyuploading to a slow upstream) no longer loses the body and fails with 502. (#8125) - Deadlines set by handlers now act as a ceiling instead of being overridden, so things like Mercure's
dispatch_timeoutand FrankenPHP'srequest_body_timeoutwork again. Thanks @dunglas! (#8145, #8284) - HTTP/1.1 uploads that time out with a little body left no longer hang instead of sending the error response. Thanks @SiyXo511! (#8178)
- Per-route
- Binding privileged ports with fewer capabilities (Linux): If
CAP_NET_BIND_SERVICEis permitted but not effective, Caddy raises it just for binding a privileged port. Packagers can usesetcap cap_net_bind_service=+pinstead of+ep, so Caddy still starts under--cap-drop ALL, OpenShift'srestricted-v2, or systemd units with an emptyCapabilityBoundingSet=. It also follows the kernel'sip_unprivileged_port_startinstead of assuming 1024. (#8102, #8253) - Small additions:
- new
log_leveldirective to set the access log level for matching requests, e.g.log_level @healthchecks DEBUG(#8201) - new
{time.now.rfc3339}placeholder. Thanks @steadytao! (#8140) - new
notmatcher for TLS connection policies. Thanks @simonether! (#8138) - the
tlsmatcher can be used in CELexpressionmatchers:tls(),tls('complete'),tls('early_data'). Thanks @stackedbyaradhya! (#8088)
- new
- Reverse proxy:
- headers you set yourself (like
Strict-Transport-Securityviaheader) are no longer lost when the upstream sends a 1xx response first, like 103 Early Hints (#8169) - request trailer values are forwarded, not just their names. Thanks @krishsinghhura! (#8246)
- request-only
expressions inlb_retry_matchno longer retry successful responses (a regression since 2.11.3) (#8168) - dial errors through a forward proxy and to FastCGI backends are retried like other dial errors. Thanks @adarshxsh and @zemamba! (#8264, #8172)
weighted_round_robinno longer panics with fewer weights than upstreams, and no longer picks an unavailable upstream when there's only one weight;random_choosepicks the least loaded host when all are busy (#8159, #8320, #8160)- with
request_buffers/response_buffers unlimited, a body that fails partway is no longer treated as complete (#8207) - connections and sockets are no longer leaked when a protocol upgrade (e.g. WebSocket) fails during setup, or when a
handle_responsestatus isn't a number. Thanks @Version2beta and @BenMacha! (#8269, #8318) - HTTP/3 client cancellations are logged at debug level instead of as warnings. Thanks @larrasket! (#8135)
- headers you set yourself (like
encode:If-None-Matchlists and weak tags get 304s properly (#8128), HEAD responses no longer get a bogusContent-Length(#8150), andAccept-Encoding/Cache-Controlsplit across several header lines are all taken into account, with faster header handling in the proxy too. Thanks @Indra55, @jayhemnani9910 and @dunglas! (#8025)- PHP / FastCGI: a
Locationresponse without aStatusis now a 302 redirect, as CGI specifies (#8209), and an app that writes more than its declaredContent-Length(like a PHP warning after the page) no longer corrupts HTTP/1.1 responses or kills HTTP/2 connections. Thanks @Sulaiman-Dauda! (#8212) - TLS & ECH:
- when Caddy rejects Encrypted ClientHello, it now sends retry configs, so clients with stale ECH configs can recover. Thanks @videah! (#8142)
- an ECH public name that also has its own site block keeps that site's TLS settings (#8156)
client_authmodesrequestandverify_if_givenno longer reject clients without a certificate when verifiers are configured (#8251)- a site block spanning several listener addresses no longer lets one listener's TLS connection policy (including client authentication) overwrite another's. Thanks @steadytao! (#8192)
- repeated matcher lines in a TLS policy
matchblock are all applied, not just the first. Thanks @simonether! (#8137) - merged automation policies keep a specific domain's policy ahead of a wildcard's (#8287), and an ACME issuer's
network_proxysurvives re-provisioning (#8288)
- Caddyfile:
caddy runprints the error again when startup fails, instead of silently exiting with code 1. Thanks @IndraGunawan! (#8180)- routes in a group that already matched no longer run their matchers, which fixes e.g.
try_files {path} {path}.html?{query} =404returning 404 for everything (#8189) - multiple
handle_errorsblocks andvarsnow follow Caddyfile order (#8195, #8197) - each
?Fielddefault in aheaderblock is checked on its own, and respectsmatch(#8217) importshows the real OS error for files it can't read (#8163), negative{args[...]}indices no longer panic (#8316), and very long heredoc markers no longer make parsing crawl (#8203, #8249)
file_server: a path below a regular file (/index.php/foo) is a 404 again and works withpass_thru(#8131);browsehandles symlinks in directories with escaped names (#8152) and offsets past the end (#8213)templates: template errors no longer keep staleContent-Length/Etagheaders (#8149), bodyless responses no longer getContent-Length: 0(which broke iOS PWAs) (#8153), plus fixes forhumanizetime layouts with colons and front matter closed with...(#8215, #8259)- Logging:
includeof a parent namespace includes its child modules (#8221),dir_mode from_fileno longer creates unusable directories (#8225), and log routing to a site's logger ignores Host case (#8176) - Admin API: with remote admin
access_control, a request is allowed if any permission entry permits it, not just the first (#8317), andCache-Control: must-revalidateis recognized alongside other directives (#8330) - Other fixes:
caddy hash-passwordhandles Windows line endings and input without a final newline (#8227), a data race in thebasic_authcache is gone (#8278), the Windows service no longer gets stuck inSTART_PENDING(#8274), anduristrip_prefix/strip_suffixhandle percent-escapes and literal%correctly (#8005, #8345) - Release builds use Go 1.27 (Go 1.26 is still the minimum to build Caddy). (#8210)
⚠️ Breaking changes
- Requests with an invalid or dots-only host (like
..) are rejected with400on every protocol, before any route runs. Before, HTTP/2 and HTTP/3 also accepted hosts containing/or\. (#8328) request_bodymax_sizenow rejects a request with413as soon as itsContent-Lengthis over the limit, even if nothing would have read the body. (#8337)php_fastcgisites without anindex.php: requests for paths that don't exist are now sent to PHP asindex.php, as the documentedfirst_exist_fallbacktry policy always promised, instead of falling through tofile_serverand returning404. Also,PATH_INFOandDOCUMENT_URIare now derived from the normalized path (repeated slashes merged,.segments removed);REQUEST_URIis unchanged.- A
try_files/filecandidate ending in/only matches a directory, so/foo.txt/no longer matches the file/foo.txt. (#8326) encodenow compresses responses even when the request hasCache-Control: no-transform(a response'sno-transformis still honored), and responses that could have been encoded always getVary: Accept-Encoding. To keep the old behavior:@transformable not header Cache-Control *no-transform*andencode @transformable. Malformed forms likeno-cache; no-transformno longer disable compression. (#8301, #8025)- Stricter config validation. Some configs that were silently accepted (and didn't do what you'd expect) are now errors:
- more than one
expressionline in the same matcher set (combine them with&&) (#8183) - unknown TLS
curvesorprotocol_min/protocol_maxnames, likeTLS1.3instead oftls1.3, which used to be silently ignored (#8266) - a repeated
sni_regexpin a TLS policymatchblock (#8137) ip_masklog filter sizes out of range, like33for IPv4 (#8241)basic_authusernames that end up identical after placeholder expansion (#8315)- ports longer than 11 characters (#8277)
- more than one
- Evaluation order now follows the Caddyfile for
handle_errorsblocks with status codes and forvarswith non-path matchers. If you had overlapping codes or set the same variable twice, a different block or value may now win. (#8195, #8197)
Security fixes
Thank you to everyone who reported responsibly and helped with patches:
- caddyhttp: A
Host/:authorityvalue with dot segments or slashes could escape the intended directory when used in file paths, likeroot /srv/sites/{host}, and withphp_fastcgieven execute files outside it. Such hosts are now rejected (see above). Thanks @dunglas. (GHSA-r766-rf9f-588h, #8328) - fastcgi:
SCRIPT_FILENAMEis now derived from the same normalized path that path matchers see, so a route protected by a path matcher can't be made to run a different script, e.g. with/admin.php/../../other.php. Thephp_fastcgirewrite also always runs now (see above). Thanks @pamnario. (GHSA-f278-mgc7-46j3) - fileserver: The
filematcher no longer resolves paths like/.env/./to the regular file, which bypassed path rules such aspath *.env. Thanks @dunglas. (GHSA-25cw-38mq-q7m6, #8326) - rewrite:
{http.matchers.file.remainder}is now escaped before rewriting, so double-encoded path info (like..%252f) can't turn into../after path rules have run. Thanks @dunglas. (GHSA-69hv-5jfc-mvmv, #8325) - fileserver: When neither
HOMEnorXDG_DATA_HOMEis set, Caddy's data directory falls back to./caddy, and afile_serverserving the working directory could expose certificates and private keys. That directory is now always hidden. Thanks @dunglas. (GHSA-jf74-f69g-hcj9, #8327) - fastcgi: The
php_fastcgitrailing-slash redirect for directories with anindex.phpcan no longer be turned into an open redirect to another site. Thanks @sam-kota. (#8285) - fileserver: Files excluded with
hideno longer show up inbrowselistings, and hiddenprecompressedsidecars are no longer served. Thanks @agammann. (#8155) - reverseproxy: Hop-by-hop headers are stripped from 1xx responses and from
copy_response_headersinhandle_response, completing the fix for CVE-2026-55458. Thanks @SssiiiSssii1 and @tomholford. (GHSA-8qx7-j2v2-x6q5, #8256, #8270) - logging: The
querylog filter now parses request URIs like the server does, so parameters after a#are redacted too. Thanks @jayhemnani9910. (#8255)
🚨 Notice for Caddy plugin maintainers: Dependabot will probably alert you to the security fixes in Caddy and urge you to upgrade it in your go.mod file. Please ONLY upgrade the Caddy dependency if there's a change to an exported API your plugin uses.
Thank you to everyone who was involved in this release, especially our 28 new contributors! 🎉
What's Changed
- core: Raise CAP_NET_BIND_SERVICE when binding privileged ports by @francislavoie in #8102
- timeouts: take precedence over server-wide and enclosing idle timeouts by @francislavoie in #8121
- encode: Fix ETag list matching by @Indra55 in #8128
- fileserver: treat a path below a regular file as not found by @klokv in #8131
- reverseproxy: log HTTP/3 client cancellations at debug level by @larrasket in #8135
- caddytls: keep repeated matchers in nested matcher sets by @simonether in #8137
- core: add RFC 3339 time placeholder by @steadytao in #8140
- caddytls: add not matcher for TLS handshakes by @simonether in #8138
- caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout by @francislavoie in #8125
- caddyhttp: document that negative idle timeouts disable them by @dunglas in #8146
- fileserver: stat symlink targets by decoded path in browse listings by @awss1i in #8152
- caddytls: Send ECH retry configs when rejecting ECH by @videah in #8142
- caddyfile: surface OS error when an import file is inaccessible by @Kshot3000 in #8163
- reverseproxy: Pick the least loaded host in random_choose when all are busy by @jayhemnani9910 in #8160
- reverseproxy: Don't panic in weighted_round_robin with fewer weights than upstreams by @jayhemnani9910 in #8159
- caddyhttp: let handler write deadlines cap the idle write timeout by @dunglas in #8145
- fileserver: apply hide rules to browse entries and precompressed sidecars by @agammann in #8155
- templates: Delete file headers on any template error by @jayhemnani9910 in #8149
- fastcgi: classify dial failures for retry by @zemamba in #8172
- go.mod: update golang.org/x/net to v0.60.0 by @elee1766 in #8179
- reverseproxy: Keep own headers after 1xx responses by @jayhemnani9910 in #8169
- httpcaddyfile: keep a site block's TLS policy for ECH public names by @awss1i in #8156
- encode: Don't write an empty encoded stream for HEAD responses by @jayhemnani9910 in #8150
- caddyhttp: ignore case when matching hosts in logger_names by @tbshfr in #8176
- reverseproxy: Only retry responses for expressions that use response data by @jayhemnani9910 in #8168
- cmd: unbuffer default logger on startup failure by @IndraGunawan in #8180
- caddyhttp: preserve expired request-body read deadlines by @SiyXo511 in #8178
- caddyhttp: skip matchers for satisfied route groups by @jayhemnani9910 in #8189
- caddyhttp: Reject repeated expression matchers in a matcher set by @jayhemnani9910 in #8183
- httpcaddyfile: isolate TLS policies across listeners by @steadytao in #8192
- httpcaddyfile: Keep handle_errors blocks in Caddyfile order by @jayhemnani9910 in #8195
- httpcaddyfile: keep source order for vars with non-comparable matchers by @Icyjerry in #8197
- caddyfile: avoid quadratic heredoc marker scanning by @francislavoie in #8203
- httpcaddyfile: Add log_level directive for access logs by @francislavoie in #8201
- templates: avoid zero content length for bodyless responses by @elee1766 in #8153
- reverseproxy: Don't drop read errors with unlimited buffers by @jayhemnani9910 in #8207
- fastcgi: Use 302 for a Location response without Status by @jayhemnani9910 in #8209
- fileserver: return an empty listing for offsets past the end by @shanejonas in #8213
- templates: preserve colons in humanize time layouts by @SiyXo511 in #8215
- logging: include module logs from its parent namespace by @omaresmael in #8221
- logging: Use the default file mode for dir_mode from_file by @jayhemnani9910 in #8225
- caddyauth: handle CRLF and unterminated hash-password input by @jayhemnani9910 in #8227
- headers: evaluate conditional defaults independently by @jayhemnani9910 in #8217
- ci: fix exhaustive linting by @steadytao in #8238
- caddyfile: check heredoc markers without a regexp by @awss1i in #8249
- logging: Reject out-of-range
ip_maskCIDRs by @jayhemnani9910 in #8241 - caddytls: skip client cert verifiers when no certificate is presented by @awss1i in #8251
- logging: Parse request URIs in the
queryfilter like the server does by @jayhemnani9910 in #8255 - reverseproxy: preserve request trailer values by @krishsinghhura in #8246
- core: Retry privileged binds on EACCES instead of checking port < 1024 by @francislavoie in #8253
- templates: use the earliest front matter closing fence by @jayhemnani9910 in #8259
- reverseproxy: strip hop-by-hop headers from interim responses by @SssiiiSssii1 in #8256
- reverseproxy: retry dial errors wrapped by a forward proxy by @adarshxsh in #8264
- caddyhttp: add CEL expression support to the tls matcher by @stackedbyaradhya in #8088
- windows: serialise service status updates by @krishsinghhura in #8274
- reverseproxy: close backend body and hijacked socket on upgrade setup failures by @Version2beta in #8269
- replacer: cache file lookups and cap them by @mohammed90 in #8248
- reverseproxy: strip hop-by-hop headers when copying response headers by @tomholford in #8270
- fastcgi: Stop response bodies at the declared Content-Length by @Sulaiman-Dauda in #8212
- caddytls: validate curve and protocol names, compare protocol IDs by @HakeemTheEmperor in #8266
- caddytls: keep ACME issuer network proxy after RebuildCertMagic by @k4lleanka in #8288
- fastcgi: keep the php_fastcgi directory redirect on the same origin by @sam-kota in #8285
- build(deps): bump github.com/go-chi/chi/v5 from 5.3.2 to 5.4.0 by @dependabot[bot] in #8298
- caddyhttp: document CEL raw strings for regexp patterns by @francislavoie in #8280
- caddyhttp: optimise header access and fix encoding negotiation by @dunglas in #8025
- caddyhttp: let handler read deadlines cap the idle read timeout by @dunglas in #8284
- httpcaddyfile: preserve automation policy precedence when merging by @bwisn in #8287
- ci: Use macos-latest runner instead of pinning macos-14 by @francislavoie in #8312
- encode: ignore request no-transform, vary identity responses on Accept-Encoding by @dunglas in #8301
- core: reject overlong ports in ParseNetworkAddress by @jasperdingg in #8277
- caddyauth: check basic_auth username uniqueness after placeholder expansion by @10kartik in #8315
- rewrite: escape file matcher remainder before rewriting by @dunglas in #8325
- fileserver: don't match regular file when path ends by slash by @dunglas in #8326
- fileserver: hide the data directory when it is relative by @dunglas in #8327
- admin: better Cache-Control handling by @dunglas in #8330
- encode: use httpguts to look for Accept-Encoding in Vary by @dunglas in #8329
- deps: update x509roots/fallback by @elee1766 in #8333
- ci: test and release with Go 1.27 by @krishsinghhura in #8210
- caddyhttp: reject invalid and dots-only hosts on every protocol by @dunglas in #8328
- caddyhttp: correct path matcher doc on bare trailing wildcards by @datrixlab in #8006
- rewrite: fix strip_path_suffix decoding a literal '%' as an escape by @datrixlab in #8005
- reverseproxy: close upstream body when handle_response status is invalid by @BenMacha in #8318
- fileserver: handle empty files in size try policies by @BenMacha in #8322
- reverseproxy: skip unavailable upstream with a single weighted_round_robin weight by @BenMacha in #8320
- admin: authorize remote requests matching any permission entry by @BenMacha in #8317
- rewrite: treat only valid percent encodings as escapes by @steadytao in #8345
- caddyfile: reject negative import argument indices by @BenMacha in #8316
- requestbody: enforce max_size early using Content-Length by @dunglas in #8337
- caddyauth: avoid writing into the shared hash buffer by @VenkateshPanda0 in #8278
- ci: release changelogs since the previous published release, and workflow hardening by @francislavoie in #8341
New Contributors
- @klokv made their first contribution in #8131
- @simonether made their first contribution in #8137
- @awss1i made their first contribution in #8152
- @videah made their first contribution in #8142
- @Kshot3000 made their first contribution in #8163
- @jayhemnani9910 made their first contribution in #8160
- @agammann made their first contribution in #8155
- @zemamba made their first contribution in #8172
- @tbshfr made their first contribution in #8176
- @SiyXo511 made their first contribution in #8178
- @Icyjerry made their first contribution in #8197
- @shanejonas made their first contribution in #8213
- @omaresmael made their first contribution in #8221
- @krishsinghhura made their first contribution in #8246
- @SssiiiSssii1 made their first contribution in #8256
- @adarshxsh made their first contribution in #8264
- @stackedbyaradhya made their first contribution in #8088
- @Version2beta made their first contribution in #8269
- @Sulaiman-Dauda made their first contribution in #8212
- @HakeemTheEmperor made their first contribution in #8266
- @k4lleanka made their first contribution in #8288
- @sam-kota made their first contribution in #8285
- @bwisn made their first contribution in #8287
- @jasperdingg made their first contribution in #8277
- @10kartik made their first contribution in #8315
- @datrixlab made their first contribution in #8006
- @BenMacha made their first contribution in #8318
- @VenkateshPanda0 made their first contribution in #8278
Full Changelog: v2.11.7...v2.11.8