HTTP3 with NGINX

Chrome, Firefox, Edge and Safari all currently support HTTP/3 which is faster than HTTP/2. HTTP/3 has shorter loading times and also provides a more stable connection. This leads to a better user-experience as faster page load speeds correlate with lower bounce-rates and it also improves search engine ranking.

A disclaimer about browser support is that Safari as of 2022-05 supports HTTP/3 but does not yet have it enabled by default. The other browsers have HTTP/3 enabled by default.

Nginx is a popular webserver which has implemented HTTP/3 but has not yet merged their HTTP/3 feature branch into mainline. The HTTP/3 branch tracks mainline and I think it currently is in a good state. This means those who are using Nginx and want to reap the benefits of HTTP/3 can do so already. I do not recommend it yet for production critical environments until it has been merged into mainline.

In this post I go through the steps of getting Nginx running with HTTP/3 as well as some recommended configurations to achieve a modern secure SSL-solution.

risaksson_ssllabs.png

The SSL Labs rating for this site as of 2022-05.

You can run their tests yourself here.

What we will achieve

Following these steps we'll achieve a HTTP/3 webserver with contemporary SLL-settings that fallbacks to HTTP/2 in the case HTTP/3 is not supported and supports multiple HTTP/3 server-blocks.

Nginx Image

My recommended solution is using Docker. If you've been using the official Nginx docker-image you can easily build your own HTTP/3-supported image which can be a drop-in alternative to the official image. This means you can more easily revert your webserver to run using the official image if needed.

The following Dockerfile can build an image that can be replaced with the official image.

FROM nginx:1.21.1 AS build

WORKDIR /src
RUN apt-get update && \
    apt-get install -y git gcc make g++ cmake perl libunwind-dev golang && \
    git clone https://boringssl.googlesource.com/boringssl && \
    mkdir boringssl/build && \
    cd boringssl/build && \
    cmake .. && \
    make

RUN apt-get install -y mercurial libperl-dev libpcre3-dev zlib1g-dev libxslt1-dev libgd-ocaml-dev libgeoip-dev && \
    hg clone https://hg.nginx.org/nginx-quic   && \
    hg clone http://hg.nginx.org/njs -r "0.6.2" && \
    cd nginx-quic && \
    hg update quic && \
    auto/configure `nginx -V 2>&1 | sed "s/ \-\-/ \\\ \n\t--/g" | grep "\-\-" | grep -ve opt= -e param= -e build=` \
                   --build=nginx-quic --with-debug  \
                   --with-http_v3_module --with-stream_quic_module \
                   --with-cc-opt="-I/src/boringssl/include" --with-ld-opt="-L/src/boringssl/build/ssl -L/src/boringssl/build/crypto" && \
    make

FROM nginx:1.21.1
COPY --from=build /src/nginx-quic/objs/nginx /usr/sbin
RUN /usr/sbin/nginx -V > /dev/stderr

To use this image is the same as the official Nginx docker image. You mount your own server block configuration files to Nginx's conf.d directory. One difference HTTP/3 has is that it uses UDP. This means you have to make sure UDP-packets can reach your Nginx-instance which might mean configuring your docker image to listen for UDP as well as eventual port-forwarding rules for UDP.

The provided docker-compose file below illustrates how to use the image more clearly.

version: "3.9"
services:
  nginx-http3:
    build: <path to the http3 nginx-Dockerfile>
    restart: always
    volumes:
      - ./my-conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
      - "443:443/tcp"
      - "443:443/udp"

HTTP/3 uses TLS1.3 which means that you also need to make your TLS-certificates available to the Nginx instance.

If not using Docker I suggest reading the following article for more information about building Nginx..

Nginx Configuration

I suggest using Nginx include directives to avoid repetition, as such we'll use a ssl-configuration file that can be reused across the server-blocks.

#http3.conf

ssl_protocols TLSv1.2 TLSv1.3; # TLSv1.3 necessary for http3
quic_retry on;
ssl_early_data on;

#ssl_prefer_server_ciphers on; # Let the client choose, as of 2022-05 these ciphers are all still secure.
ssl_dhparam /etc/nginx/dhparam2048.pem; # generate with 'openssl dhparam -out dhparam.pem 2048'
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve X25519:prime256v1:secp384r1;

ssl_session_cache shared:SSL:5m;
ssl_session_timeout 1h;
ssl_session_tickets off;
ssl_buffer_size 4k; # This is for performance rather than security, the optimal value depends on each site.
                                # 16k default, 4k is a good first guess and likely more performant.

ssl_stapling on;      # As of 2022-05 this version of nginx dosen't support ssl-stapling, but it might be in the future.
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 valid=300s; # Use whichever resolvers you'd like, these are Cloudflare's and is one of the fastest DNS resolvers.
resolver_timeout 5s;

ssl_certificate         /my-path/fullchain.pem;
ssl_certificate_key     /my-path/privkey.pem;
ssl_trusted_certificate /my-path/chain.pem; # For SSL-stapling

proxy_request_buffering off;

add_header alt-svc 'h3=":443"; ma=86400'; # Absolutely necessary header. This informs the client that HTTP/3 is available.
add_header Strict-Transport-Security max-age=15768000; # Optional but good, client should always try to use HTTPS, even for initial requests.

gzip off; #https://en.wikipedia.org/wiki/BREACH

The above configuration is a sane default for reasonably modern clients. If you prefer even tighter security or prefer higher compatability in favor of security then you can refer to Mozilla as a good reference for SSL configurations. I recommend their configuration generator.

Nginx server-blocks use listen-directives to configure the port and protocol a given server-block should use. A small idiosyncrasy about Nginx is that some listen directives are only allowed to appear once in the configuration. One such example is "default_server". HTTP/3 benefits from another unique listen directive which is "reuseport" which brings a performance benefit .

An Nginx convention is to use a default configuration which defines these unique directives and as such configures each server-block using that port and protocol. I present the following as an option.

http {

    ...

    # Log http3 requests
    log_format quic '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" "$http3"';
    access_log  /var/log/nginx/access.log  quic;

    server {
        listen 80 default_server;
        server_name _;
        root usr/share/nginx/html;
    }
  
    server {
        listen 443 http3 reuseport default_server;
        listen 443 ssl http2;
        include http3.conf;
        server_name _;
        root usr/share/nginx/html;
    }
}

Now what's left is using HTTP/3 to do something useful. You can use it in a server-block like this:

server {
    listen 443 http3;
    listen 443 ssl http2;
    include http3.conf;

    server_name my-site.com;

    location / {
        proxy_pass http://my-service;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name my-site.com;

   # Also force https with a redirect.
    location / {
        add_header alt-svc 'h3=":443"; ma=86400';
        return 301 https://my-site.com$request_uri;
    }
}
Test the connection

Before testing you can sanity-check that your browser supports HTTP/3 by visiting Nginx's Quic-site. The way HTTP/3 (Quic) works is by the client reading the alt-svc header for HTTP/3 information on an initial request. This means that the first request to a new site does not default to HTTP/3, but the following requests can make use of it. I've noticed that with Firefox it can be necessary to refresh the site once for it to start preferring HTTP/3.

That said I recommend testing HTTP/3 first with a simple command-line client. For example curl3. You can find clients recommended for testing by Nginx here.

Example using curl3

curl3 --http3 https://quic.nginx.org

returns a successful 0 exit-code and the content data as HTTP/3 is supported on this website.

If HTTP/3 is not supported curl3 exits with code 56 and prints

curl: (56) quiche: recvfrom() unexpectedly returned -1 (errno: 111, socket 5)