Skip to content

Add HTTP/3 support#1531

Merged
swalkinshaw merged 42 commits intoroots:masterfrom
strarsis:add-http3-support
Mar 8, 2026
Merged

Add HTTP/3 support#1531
swalkinshaw merged 42 commits intoroots:masterfrom
strarsis:add-http3-support

Conversation

@strarsis
Copy link
Copy Markdown
Contributor

@strarsis strarsis commented Jul 29, 2024

This PR adds the necessary configuration for proper HTTP/3 support (by nginx) (with HTTP/1/HTTPS/2 co-existence).

  • Some additional SSL/QUIC/HTTP/3 tweaks, which are used in nginx documentation, forum threads (1; 2) are added to the configuration when HTTP/3 support is turned on.
  • The OS ansible-managed ferm firewall is also configured to allow inbound UDP/433 for QUIC (HTTP/3).
    The approach is also used in the previous PR for HTTPS (Conditionally add HTTPS inbound allow firewall rule #1530).
  • Repeating configuration is included as external files for readability and maintainability.
  • nginx requires one (and only one listen quic directive to have the reuseport option (only one listen quic directive can have the reuseport option). As the listen quic directive requires a certificate, the first WordPress site ("vhost") that has HTTPS enabled, has the reuseport option added to its listen quic directive.

Additional notes (some may be useful in the documentation):

  • 0-RTT is a TLS1.3 feature, not exclusive to HTTP/3. Protocols on top of TLS1.3 (as HTTP/1/2/3) benefit from it. A very new nginx and underlying OpenSSL library (March 2026) is required, though. For that reason it is disabled by default for now. 0-RTT can improve performance and is still a very new spec/feature, HTTP/3 works without it.
  • HTTP/3 sits on top of QUIC which sits on top of UDP (combining TCP characteristics and TLS encryption).
  • QUIC exists as a kind of UDP-TCP-hybrid on its own, HTTP/3 requires QUIC to work.
  • HTTP/3 is already well supported by all current browsers (baseline 2024).
  • Besides the OS firewall (that is configured by Trellis) there is often a hardware/cloud firewall in front of the server,
    of course it also needs to be configured for allowing inbound UDP/443 traffic for QUIC (HTTP/3) (example for Hetzner Cloud Firewall).
  • HTTP/3 requires an additional HTTP header to be sent for advertising the availability of QUIC/HTTP/3,
    without this HTTP header, HTTP/3 will not work, despite everything else being perfectly configured and running.
  • Port number 443 is actually not mandatory for HTTP/3 (to some degree, also depending on browser), but it is recommended to use the same port number 443 as for HTTPS (TCP), nginx then listens on 443/UDP in parallel to 443/TCP for HTTP/1/HTTPS/2.
  • In my experience, testing HTTP/3 requests...
    • worked best with the Chrome Developer Tools (Network tab, toggle Protocol column on).
    • curl/wget HTTP/3 support is not a given in current stable Ubuntu. I used a curl Docker image with HTTP/3 support.
    • Some online HTTP/3 testing tools do not always work and incorrectly report HTTP/3 as not being supported, despite all other clients and tools being able to use HTTP/3.

Useful resources

@strarsis
Copy link
Copy Markdown
Contributor Author

@swalkinshaw: Using nginx includes makes the configuration much readable now.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Aug 4, 2024

The SSL early data option allows for RTT-0 requests (zero round-trip time), however, it comes with security implications (possibility of replay attacks), the application layer (so the PHP WordPress app here) gets a HTTP Header Early-Data passed from the reverse proxy (nginx), and for risky operations (as authentication/login) the app has to terminate requests with HTTP 425 Too Early.
I was not able to find any occurrence of this logic in WordPress code or plugins or discussions about this, hence I commented out this optimization until it is deemed safe for WordPress applications.

@swalkinshaw
Copy link
Copy Markdown
Member

Mind rebasing @strarsis ? Looks good otherwise and all the notes/documentation is appreciated.

@strarsis
Copy link
Copy Markdown
Contributor Author

@swalkinshaw: Sure! I also have to test the HTTP/3 specific configuration a bit further.

@strarsis strarsis changed the title Add HTTP/3 support Add HTTP/3 support Aug 24, 2024
@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Aug 27, 2024

@swalkinshaw: Well, I "rebased" it somehow. If necessary, I create a new branch/PR.

So it turned out that a global listen for QUIC with reuseport is necessary for working QUIC responses.
For a QUIC listen is also a SSL certificate required, it does not matter which one.
So for providing such a SSL certificate, one of the sites have to be specified as the "default site".

@swalkinshaw
Copy link
Copy Markdown
Member

😓 wow they really don't make this easy. I'll try and think of another solution for the default site/SSL cert 🤔

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Aug 28, 2024

another solution for the default site/SSL cert

That listen quic is only needed for reuseport (apparently required by the nginx worker processes for correctly responding to QUIC requests). With listen quic nginx requires a SSL cert and key, but that listen and SSL would not be used otherwise.

Working, confirmed alternatives:

  • Add reuseport to one (and only one) existing site listen quic directive. This appears to be the simplest way to do it. The site index could be used to render reuseport only for the first site.
  • Use a snakeoil cert (as the already installed one /etc/ssl/certs/ssl-cert-snakeoil.pem; /etc/ssl/private/ssl-cert-snakeoil.key) for the global quic listen directive with reuseport to satisfy the cert condition for a quic listen.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Aug 30, 2024

Edit: jinja namespaces probably do not scope beyond the iteration of ansible template loops, so a different approach is used.

Now simply the first site that uses HTTPS will have the reuseports option added to its quic listen directive.
For this a helper variable is added that contains all sites that use SSL (not the boolean one),
and then just the item key is compared to determine whether the current config in jinja is the one for the first HTTPS using site.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Aug 31, 2024

@swalkinshaw: Edit: After some real-world testing I noticed that some WordPress sites had redirect issues (on frontend) (ERR_TOO_MANY_REDIRECTS). I am not sure whether this has been caused by http/3. Still testing and observing the behavior with http/3 enabled. Possible related issue. This appears to be caused by the redirects, when HTTP/3 is advertised/used there.

@swalkinshaw
Copy link
Copy Markdown
Member

Apologies, just getting around to testing this myself now.

  1. Were you ever able to test this using self-signed certs?
  2. Any follow up on the redirect issues?
  3. re: 0RTT and replay attacks, I think just returning 425 for non-GET requests would mitigate most of it. That's what Cloudflare does by default. I did find https://core.trac.wordpress.org/ticket/58769

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Oct 20, 2025

  1. I have not tried this with self signed certs yet.
  2. Redirect issue do not appear to occur on stock WordPress sites, so this is probably caused by plugin or specific customizations.
  3. Would it make sense to add a roots Plugin that then returns this HTTP status for global mitigation?

@swalkinshaw
Copy link
Copy Markdown
Member

Plug might make sense, but at least for now we can just implement the 425 for non-GET requests in Nginx right? To keep it simple.

I think once we do that the PR should be ready to merge

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Jan 17, 2026

@swalkinshaw: Added here: d33bbae. Needs some testing by me.

I need to resolve the conflicts with main branch.

Comment thread roles/wordpress-setup/tasks/nginx.yml Outdated
Comment thread roles/wordpress-setup/templates/includes/directive-only/http3-rtt0.conf Outdated
Comment thread roles/wordpress-setup/templates/includes/directive-only/http3-rtt0.conf Outdated
Comment thread roles/wordpress-setup/templates/includes/directive-only/http3-tune.conf Outdated
Comment thread roles/wordpress-setup/templates/includes/directive-only/http3-tune.conf Outdated
@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 2, 2026

@swalkinshaw: Thanks for the review, I overhauled some of the PR components, testing it locally before pushing.
Incredibly busy right now, next week I should have more time getting this into an merge-acceptable state.

@retlehs
Copy link
Copy Markdown
Member

retlehs commented Mar 6, 2026

@strarsis I had to make these changes to provision successfully with this PR:

diff --git a/trellis/roles/wordpress-setup/defaults/main.yml b/trellis/roles/wordpress-setup/defaults/main.yml
index 4eff909..408e627 100644
--- a/trellis/roles/wordpress-setup/defaults/main.yml
+++ b/trellis/roles/wordpress-setup/defaults/main.yml
@@ -1,11 +1,12 @@
 site_uses_local_db: "{{ site_env.db_host == 'localhost' }}"
+wordpress_default_site: "{{ wordpress_sites.keys() | first }}"
 nginx_wordpress_site_conf: wordpress-site.conf.j2
 nginx_ssl_path: "{{ nginx_path }}/ssl"
 
 nginx_sites_confs:
   - src: no-default.conf.j2
   - src: http3-reuseport.conf.j2
-    enabled: "{{ nginx_http3_enabled and (sites_use_ssl | bool) and wordpress_default_site }}"
+    enabled: "{{ nginx_http3_enabled and (sites_use_ssl | bool) and (wordpress_default_site | length > 0) }}"
   - src: ssl.no-default.conf.j2
     enabled: false
 
diff --git a/trellis/roles/wordpress-setup/templates/wordpress-site.conf.j2 b/trellis/roles/wordpress-setup/templates/wordpress-site.conf.j2
index 5e0d2ea..b3b7772 100644
--- a/trellis/roles/wordpress-setup/templates/wordpress-site.conf.j2
+++ b/trellis/roles/wordpress-setup/templates/wordpress-site.conf.j2
@@ -9,10 +9,8 @@ server {
 
   {% if nginx_http3_enabled and ssl_enabled -%}
   # Listen on UDP for QUIC+HTTP/3
-  listen [::]:443 quic{% if is_first_site_use_ssl %} reuseport{% endif -%};
-  listen 443 quic{% if is_first_site_use_ssl %} reuseport{% endif -%};
-  {% if is_first_site_use_ssl -%}# there has to be one listen quic directive with `reuseport` for working QUIC responses in current nginx version, using the first site.
-  {% endif %}
+  listen [::]:443 quic;
+  listen 443 quic;
   {% endif %}
 
   http2 {{ nginx_http2_enabled | default(false) | ternary('on', 'off') }};

Also should be worth noting at the top of this PR that it must be enabled by updating trellis/roles/wordpress-setup/defaults/main.yml:

- nginx_http3_enabled: false
+ nginx_http3_enabled: true

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

@retlehs: I fixed these issues. There were two mechanisms for satisfying nginx requirement of one single site having reuseport for QUIC listen, and they were used both at the same time, with one being incomplete, hence the ansible error during site deploy (but not provision).

And I had to remember allowing UDP/443 through the hoster/hardware firewall...

@retlehs
Copy link
Copy Markdown
Member

retlehs commented Mar 7, 2026

🙏

I’ve got this branch deployed at https://http3.roots-example-project.com/ for testing – will pull in your changes soon and test a reprovision

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

I notice that many of these HTTP/3 online checker tools have issues even with established, HTTP/3 supporting sites.

I test with curl with HTTP/3 build from Docker image to determine HTTP/3 support:

docker run --rm ymuski/curl-http3 curl --http3 -Iv https://www.example.com

Though Chrome appears to sometimes not using HTTP/3, for whatever heuristics.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

@retlehs: Also test with two different sites on same Trellis server. Due to how nginx demands one reuseport QUIC listen, the template uses some extra logic to handle multiple sites.

I also try to test 0-RTT, curl should be able to resume a session:

docker run --rm -v $(pwd):/data ymuski/curl-http3   curl --http3 -Iv --sessionid /data/sess.txt https://example.com

Though there is some discussion how to make a WordPress site more safe against potential 0-RTT replay attacks.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

Addendum: RTT0 will not work currently as the nginx (and OpenSSL it was built with) does not support RTT0 yet.

@swalkinshaw, @retlehs: 🤔 Is it worth having the RTT0 feature additionally in this PR then? Or re-add it when nginx package became new enough to support RTT0? RTT0 is independent from HTTP/3 and can speed up connections over TLS1.3. Thinking about it - though RTT0 is often used in conjunction with HTTP/3, it is not required for HTTP/3.
RTT0 is a TLS thing and technically a bit outside of this PR for HTTP/3.
For making RTT0 possible, nginx must be built with quictls or newer OpenSSL. The ondrej PPA does not have a nginx with that yet.

@retlehs
Copy link
Copy Markdown
Member

retlehs commented Mar 7, 2026

Seems out of scope for this HTTP/3 PR. Since it's disabled by default and doesn't affect HTTP/3 functionality, I'd say keep it as-is for now and we can revisit it separately.

PS. Pulled in your latest changes and reprovisioned https://http3.roots-example-project.com/ with no issues 🙏

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

👍 Alright, PR looks fine to me now.

@retlehs retlehs requested a review from swalkinshaw March 7, 2026 04:12
retlehs added a commit to roots/docs that referenced this pull request Mar 7, 2026
Docs updates for roots/trellis#1531

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@retlehs
Copy link
Copy Markdown
Member

retlehs commented Mar 7, 2026

Got docs updates ready to go once this is merged and tagged: roots/docs#567

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 7, 2026

Got docs updates ready to go once this is merged and tagged: roots/docs#567

It may be helpful to note that port UDP/443 is required for HTTP/3/QUIC and that there is often a cloud or hardware firewall in front of and independent from the Trellis server that also needs to be configured to allow the HTTP/3 traffic in.

@retlehs
Copy link
Copy Markdown
Member

retlehs commented Mar 7, 2026

@strarsis Good call out, thanks! Updated the docs

@swalkinshaw
Copy link
Copy Markdown
Member

@strarsis are you still removing RTT0 support? I agree we shouldn't even support it if it won't work with our Nginx which is all Trellis should care about.

@strarsis
Copy link
Copy Markdown
Contributor Author

strarsis commented Mar 8, 2026

@swalkinshaw, @retlehs: Yes, I agree and removed the 0-RTT config from this PR now.

0-RTT support is highly experimental in nginx and also not directly related to HTTP, it is a TLS1.3 optimization feature with HTTP header support on top (Early data). Client support is also lacking: Only Firefox currently supports it as in March 2026.
So when 0-RTT finally becomes widespread in client and server support, we can revisit this in a new PR.
Honestly I do not even know why I have added 0-RTT to this PR in the first place, probably because HTTP/3 had been more experimental when I started the PR and 0-RTT started as equally new then, and often discussed as a bleeding-edge optimization technique.

@swalkinshaw swalkinshaw merged commit e18e9d7 into roots:master Mar 8, 2026
2 checks passed
@swalkinshaw
Copy link
Copy Markdown
Member

Thanks @strarsis and apologies reviewing this took so long. Thanks to @retlehs for testing too

retlehs added a commit to roots/docs that referenced this pull request Mar 11, 2026
* Update Trellis docs for HTTP/3 support

Docs updates for roots/trellis#1531

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add HTTP/3 UDP port 443 firewall note

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants