DEV Community

Cover image for Safe HTTP/3 Experimentation With Caddy
Michael Driscoll
Michael Driscoll

Posted on

Safe HTTP/3 Experimentation With Caddy

The Bleeding Edge Problem

I'm writing QUIC documentation and thought it would be great to have the site available as HTTP/3 (which runs on top of QUIC). This is a relatively new protocol, so I ran into some familiar problems:

  • Tooling: The webserver that I'm using (apache) doesn't offer HTTP/3 support, and has no current plans to
  • Uptime: The page in question is relatively popular, and I don't want to risk it going down while I set this up

The QUIC Possibility

Luckily the details of the QUIC transport protocol give us some flexibility here:

  • QUIC uses a different underlying protocol (UDP vs TCP), so I can leave apache on TCP port 443 and put up a reverse proxy on the UDP port
  • All HTTP/3-capable browsers will try QUIC and, if they get a connection error, will fall back to TCP (and the unmodified apache setup)

The Solution

The fix is a simple Caddy reverse proxy. It's running in a docker container to let me "split the bindings": Caddy wants to bind both TCP and UDP ports but I can only give it the UDP port. Containerizing gives me the flexibility of letting it bind both in its container, but only exposing the UDP port to the world.

Details

The Caddy installation looks like this. You'll want to tweak a few lines, indicated with "youruser" or "yoursite" placeholders:

setup:

### change as appropriate for your OS
sudo snap install docker

mkdir ~/caddy/
mkdir ~/caddy/caddy_data
mkdir ~/caddy/caddy_config
Enter fullscreen mode Exit fullscreen mode

~/caddy/docker-compose.yaml:

version: "3.7"

services:
  caddy:
    container_name: caddy
    hostname: caddy
    image: caddy:2.4.6
    restart: unless-stopped
    ports:
      - "443:443/udp"
    volumes:
      - /home/youruser/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /path/to/yoursite/fullchain.pem:/caddy.crt
      - /path/to/yoursite/privkey.pem:/caddy.key
      - /home/youruser/caddy/caddy_data:/data
      - /home/youruser/caddy/caddy_config:/config
    extra_hosts:
      - "host-gateway:172.17.0.1"

volumes:
  caddy_data:
    external: true
  caddy_config:
Enter fullscreen mode Exit fullscreen mode

~/caddy/Caddyfile:

{
    auto_https off
    servers {
        protocol {
            experimental_http3
        }
    }
}

yoursite.com {
    tls /caddy.crt /caddy.key
    reverse_proxy * https://host-gateway {
        transport http {
            tls_insecure_skip_verify
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

startup:

cd ~/caddy
sudo docker-compose up -d
### tail the logs with `sudo docker logs -f caddy`
Enter fullscreen mode Exit fullscreen mode

Advertising HTTP/3

The above sets up a reverse proxy that serves HTTP/3 on UDP port 443, but nothing will try it until you advertise it on your "real" HTTP server. Fortunately this minor config was the only change needed on the production server:

In my VirtualHost apache config for the site:

Header set alt-svc "h3=\":443\"; ma=3600, h3-29=\":443\"; ma=3600
Enter fullscreen mode Exit fullscreen mode

This advertises an HTTP/3 service (both "standard" and "draft 29" versions of the protocol) with the "Alt-Svc:" header. You'll need to bounce apache for this to take effect.

Cert Rotation

One last thing is needed to handle cert rotation. The above solution copies your site certificate and key into the Caddy container, but if you're using LetsEncrypt that cert is only good for three months and is likely being rotated monthly. I run the following in cron to ensure the container is always capturing a relatively fresh certificate:

### bounce the reverse proxy every month
39 0 25 * * cd /home/youruser/caddy && /path/to/docker-compose down && /path/to/docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

cover image by Stephen Cleary CC 2.0

Discussion (0)