Running ECH enabled nginx

ECH is a good way to hide the SNI of the website being connected to.

However, since ECH is not yet an official RFC (March 2024) and not many libs support it (especially on the server side), we need to do some hacky stuff to deploy it.

Specifically, we will use openssl & nginx forks by the great people at defo.ie to deploy ECH support.

Building

Building the OpenSSL fork

This fork adds support for ECH into openssl.

cd ~
mkdir -p code
cd code
git clone https://github.com/sftcd/openssl.git openssl-for-nginx
cd openssl-for-nginx
git checkout ECH-draft-13c
./config -d
make

Building the nginx fork

This fork uses the forked OpenSSL along with Nginx to support incoming ECH TLS handshakes.

cd ~/code
git clone https://github.com/sftcd/nginx.git
cd nginx
git checkout ECH-experimental
./auto/configure --with-debug --prefix=nginx --with-http_ssl_module --with-openssl=$HOME/code/openssl-for-nginx  --with-openssl-opt="--debug"
make

Now nginx should be compiled with the ECH compatible OpenSSL!

Some nginx dirs

We need to create some directories which nginx expects for logs and stuff.

cd ~/code/openssl-for-nginx/esnistuff
mkdir nginx
cd nginx
mkdir logs
mkdir www
mkdir echkeydir

Deploying

To deploy a website with ECH support behind nginx, we need to generate ECH keys, update our DNS, and then configure nginx to use ECH.

The steps are outlined below:

Generate the ECH keys

Put whatever public_name here you want snoopers to think you're connecting to (via SNI)!

cd ~/code/openssl-for-nginx/esnistuff
../apps/openssl ech -public_name example.com -pemout ./nginx/echkeydir/example.pem.ech

Setup DNS records for ECHConfig

Once you generate the ECH keys, the contents will look something like this:

-----BEGIN PRIVATE KEY-----
[REDACTED]
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AD7+DQA6hAAgACAmYD8cK8laATn/iKI1jt79RGkFzoppTl0LVpshC/Q1VQAEAAEAAQALZXhhbXBsZS5jb20AAA==
-----END ECHCONFIG-----

The base64 text is what you need to add to your domains "HTTPS" DNS record.

For example, if your domain name (whatever value you used for public_name in the keygen step doesn't matter) is rfc5746.mywaifu.best , then you need to add a HTTPS record in your DNS, as such:

Cloudflare DNS example for ECH HTTPS

Note: the value should be: ech="THE BASE 64 ECHCONFIG"

Configure nginx

A sample configuration file could look like this: (nginx-ech.conf):

worker_processes  1;
error_log  logs/error.log  info;

events {
    worker_connections  1024;
}

http {
    access_log          logs/access.log combined;
    ssl_echkeydir        echkeydir;
    server {
        listen              443 default_server ssl;
        ssl_certificate     cadir/domain.crt;
        ssl_certificate_key cadir/domain.key;
        ssl_protocols       TLSv1.3;
        server_name         rfc5746.mywaifu.best;
        location / {
            root   www;
            index  index.html index.htm;
        }
    }
}

Replace server_name with whatever your real server (domain) name is, e.g. for me it is rfc5746.mywaifu.best). Now put this config in ~/code/openssl-for-nginx/esnistuff/nginx/nginx-ech.conf .

Run nginx

cd ~/code/openssl-for-nginx/esnistuff
../../nginx/objs/nginx -c nginx-ech.conf

Bonus: Multiple ECHConfigs with different SNIs

If you configure nginx to listen on multiple ports, you can advertise separate ECHConfigs for each port, with their own SNI. Specifically, the ECHConfig advertised in the HTTPS RR follows "Port Prefix Naming" as per Section 2.3 of RFC9460.

Generating a new ECHConfig

Let's say, now instead of example.com, we want to use the SNI cia.gov. Let's first generate an ECHConfig for this domain:

cd ~/code/openssl-for-nginx/esnistuff
../apps/openssl ech -public_name cia.gov -pemout ./nginx/echkeydir/cia.gov.pem.ech

Tell NGINX to listen on this port

If we want port 4443 to advertise this ECHConfig and be connectable, we just add a listen directive for this port in our NGINX config:

    listen 4443 default_server ssl;  

Finally, we need a new DNS HTTPS record for specifically for this port, based on "Port Prefix Naming". So add a new HTTPS record for your domain, with the target as:

_4443._https.rfc5746.mywaifu.best  

Note: The new part is _4443._https.! This will tell browsers (or well, more generally ECH capable clients) the ECHConfig to use on this port.

Reference

  • https://github.com/sftcd/openssl/blob/9e66beb759d274f3069e19cc96c793712e83122c/esnistuff/nginx.md?plain=1#L172
  • https://github.com/sftcd/openssl/issues/26
  • https://guardianproject.info/2023/11/10/quick-set-up-guide-for-encrypted-client-hello-ech/