Setting Up ECH for a Website
If you are reading this, you probably know what Encrypted Client Hello (ECH) is already. It is a much more complex successor of the ESNI, an earlier solution to the same problem of SNI visibility, and, unfortunately, there aren’t that many practical guides on setting up an ECH-enabled website available. Having a working setup under your control helps you understand how ECH works and explore how it can be adopted, and this minimalistic guide will tell you what you need to do to set up an ECH-enabled website.
Table of Contents
1. Architecture
2. Preparing VMs
3. Creating authoritative DNS servers for the domain
Primary DNS server on VM1
Secondary DNS server on VM2
Pointing the domain to our DNS servers
Testing the DNS
4. Setting up an ECH-capable web server on VM1
Getting patched OpenSSL sources
Getting patched Nginx sources
Compiling the Nginx with OpenSSL
Starting the web server
Setting up TLS for the web server
Acquiring a TLS certificate
Enabling HTTPS
Enabling ECH
Generating the ECHConfig and a private key
Creating the required DNS records
5. Testing
6. A note on GREASE ECH
7. Next steps
1. Architecture
Here’s what we will need:
- A web browser with ECH support (e.g., Chrome 117+).
- A registered domain name for testing (I’ll be using ech-labs.com).
- Authoritative DNS server(s) for the domain (many registrars require 2 of them).
- An ECH-capable web server with a TLS certificate.
Here’s how it’s going to work:
- The domain and site owner sets up an ECH-enabled website. They:
- Generate the ECHConfig (by specifying the HPKE cipher suite, config identifier and public name) and a corresponding private key.
- Add the ECHConfig to the HTTPS DNS record for the site.
- Add the private key and ECHConfig to an ECH-capable web server.
- When the client uses an ECH-capable web browser to access the ECH-enabled website:
- The web browser asks the DNS for an HTTPS record of the website.
- The web browser extracts the ECHConfig from the HTTPS record.
- The web browser constructs an ECH TLS extension, which:
- Sets an inner SNI for the actual website name requested (which will be encrypted).
- Sets a config identifier from ECHConfig.
- Encrypts the ECH extension with the public key from the ECHConfig.
- The web browser then sets an outer SNI for the public name from the ECHConfig (which will remain in plaintext).
- The web browser performs an ECH-enabled TLS handshake.
- When the web server receives an ECH-enabled TLS handshake attempt, it:
- Tries to find the ECHConfig matching config identifier in the ECH extension.
- Tries to decrypt the ECH extension using a private key corresponding to the ECHConfig:
- If it succeeds, the web server serves the content for the decrypted inner SNI (the actual requested website name).
- If it fails, the web server serves content for the outer SNI (the public name from the ECHConfig).
2. Preparing VMs
We’ll start by creating two VMs, both running Ubuntu 23.10 and having 2 separate public IP addresses, e.g.:
- VM1: 123.1.1.1
- VM2: 123.1.1.2
3. Creating authoritative DNS servers for the domain
Again, I’ll be using ech-labs.com as the domain for this example.
Start by installing the latest bind9 on both VMs:
$ sudo apt update
$ sudo apt install bind9
$ dpkg -s bind9 | grep Version
Version: 1:9.18.18-0ubuntu2
Also, configure bind9 to globally disable DNS recursion and zone transfers on both VMs by adding the following lines to the options
block in /etc/bind/named.conf.options:
recursion no;
allow-transfer { none; };
Primary DNS server on VM1
Now we’ll configure VM1 to act as the primary DNS server for the domain (ech-labs.com). The first step is to create a zone file /etc/bind/db.ech-labs.com
with the following information:
$TTL 604800
@ IN SOA ns1.ech-labs.com. root.ech-labs.com. (
2023111501 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns1.ech-labs.com.
@ IN NS ns2.ech-labs.com.
ns1 IN A 123.1.1.1
ns2 IN A 123.1.1.2
Here we’ve defined the domain and its two authoritative DNS servers – ns1.ech-labs.com and ns2.ech-labs.com, pointed to public IPs of our VMs.
Now we need to add a defined zone to the bind9 configuration by adding the following lines to /etc/bind/named.conf.local
:
zone "ech-labs.com" {
type master;
notify yes;
allow-transfer { 123.1.1.2; };
file "/etc/bind/db.ech-labs.com";
};
Here we’ve configured bind9 to be the primary (master) DNS server for the ech-labs.com domain, with its zone stored in /etc/bind/db.ech-labs.com
. Also, we’ve allowed zone transfers from 123.1.1.2 (the secondary DNS server), and that IP gets notified on zone changes.
Restart bind9 using systemctl restart bind9
to apply the configuration.
Secondary DNS server on VM2
Now let’s configure VM2 as the secondary DNS server for ech-labs.com. A secondary DNS server performs zone transfers from the primary DNS server, so we don’t need to create a zone file. Instead, we only need to add a zone to /etc/bind/named.conf.local
:
zone "ech-labs.com" {
type slave;
masters { 123.1.1.1; };
file "/var/lib/bind/db.ech-labs.com";
};
Here we’ve configured bind9 to be the secondary (slave) DNS server for the ech-labs.com domain. It performs a zone transfer from the defined primary (master) server 123.1.1.1 and stores it in /var/lib/bind/db.ech-labs.com
.
Restart bind9 using systemctl restart bind9
to apply configuration.
If the zone is transferred successfully, the /var/lib/bind/db.ech-labs.com
file will be created.
Pointing the domain to our DNS servers
Specify DNS servers for the domain ech-labs.com in the domain registrar’s management interface:
123.1.1.1 ns1.ech-labs.com
(primary)123.1.1.2 ns2.ech-labs.com
(secondary)
Then apply the changes. It may take some time for the DNS to propagate these changes.
Testing the DNS
Check that the primary DNS works correctly by querying it directly from your local machine:
$ host ns2.ech-labs.com 123.1.1.1
Using domain server:
Name: 123.1.1.1
Address: 123.1.1.1#53
Aliases:
ns2.ech-labs.com has address 123.1.1.2
Check to make sure that the secondary DNS works and successfully transfers the zone by querying it directly from your local machine:
$ host ns1.ech-labs.com 123.1.1.2
Using domain server:
Name: 123.1.1.2
Address: 123.1.1.2#53
Aliases:
ns1.ech-labs.com has address 123.1.1.1
Check whether the domain is resolvable from the internet by querying the default recursive resolver from your local machine:
$ host ns1.ech-labs.com
ns1.ech-labs.com has address 123.1.1.1
$ host ns2.ech-labs.com
ns2.ech-labs.com has address 123.1.1.2
4. Setting up an ECH-capable web server on VM1
Since no popular web servers support ECH out of the box yet, we will borrow from the awesome work on ECH support by Stephen Farrell. He added experimental ECH support to the OpenSSL library and the Nginx web server.
We will use the ~/src
directory to store Nginx and OpenSSL sources and /opt/ech
as the installation prefix.
First we need to install the compiler toolchain and dependencies:
$ sudo apt install build-essential libpcre2-dev zlib1g-dev
Getting patched OpenSSL sources
Clone the repo and switch to the patched branch:
$ mkdir ~/src && cd ~/src
$ git clone https://github.com/sftcd/openssl.git
...
$ cd openssl
$ git checkout ECH-draft-13c
Getting patched Nginx sources
Clone the repo and switch to the patched branch:
$ cd ~/src
$ git clone https://github.com/sftcd/nginx.git
...
$ cd nginx
$ git checkout ECH-experimental
Compiling the Nginx with OpenSSL
Nginx will compile the OpenSSL as a dependency, we just need to configure it properly:
$ cd ~/src/nginx
$ ./auto/configure --with-debug --prefix=/opt/ech --with-http_ssl_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-openssl=~/src/openssl --with-openssl-opt="--debug '-Wl,--enable-new-dtags,-rpath,$(LIBRPATH)'" --with-http_v2_module
...
$ make && make install
Let’s test if we’ve built OpenSSL with ECH support:
$ /opt/ech/bin/openssl ech -h
Usage: ech [options]
General options options:
-help Display this summary
-verbose Provide additional output (though not much:-)
Key generation options:
-pemout outfile Private key and ECHConfig [echconfig.pem]
-pubout outfile Public key output file
-privout outfile Private key output file
-public_name val public_name value
-mlen int Maximum name length value
-suite val HPKE ciphersuite: e.g. "0x20,1,3"
-ech_version int ECHConfig version [0xff0d (13)]
-extfile val Input file with encoded ECH extensions
ECHConfig print/down-selection options:
-pemin outfile File with optional private key and ECHConfig
-select int Output only n-th ECHConfig from input file
Let’s test if we’ve built Nginx with ECH support:
$ strings /opt/ech/sbin/nginx | grep -w ssl_echkeydir
ssl_echkeydir
Starting the web server
$ sudo /opt/ech/sbin/nginx
Nginx will start in the background without any output and will serve the contents of /opt/ech/html
via plaintext HTTP.
Now point the web browser to http://ns1.ech-labs.com and it should display the default “Welcome to nginx!” page.
Setting up TLS for the web server
Acquiring a TLS certificate
Now we will install and use certbot
to get the free Let’s Encrypt TLS certificate for ech-labs.com and its subdomains:
$ sudo apt install certbot
$ sudo certbot certonly -d 'ech-labs.com' -d '*.ech-labs.com' --expand --manual
Now certbot
will ask you to confirm site ownership by creating the .well-known
file with provided contents. Connect to VM1 in a new terminal and do the following:
$ sudo mkdir -p /opt/ech/html/.well-known/acme-challenge
$ sudo sh -c 'echo "XhfgL4luX8TCzWz2hfs--Vl-Ezsi_Y3oS4pol4LmKL7.C1YB-i-o7BJpFSKmE3S78erJNG5vqFYI9ivmv3J8jYX" > /opt/ech/html/.well-known/acme-challenge/XhfgL4luX8TCzWz2hfs--Vl-Ezsi_Y3oS4pol4LmKL7'
Since we are asking for a wildcard certificate, certbot
also will ask for DNS ownership confirmation by creating a DNS TXT record with the specified value.
Add a line containing TXT record for _acme-challenge.ech-labs.com with the provided value in /etc/bind/db.ech-labs.com
, e.g.:
$TTL 604800
@ IN SOA ns1.ech-labs.com. root.ech-labs.com. (
2023111502 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns1.ech-labs.com.
@ IN NS ns2.ech-labs.com.
ns1 IN A 123.1.1.1
ns2 IN A 123.1.1.2
_acme-challenge IN TXT 7bOkB4AnNGgV8NFF8BTK199JCZbvwwpHhoLBZnxdmm
Also, increase the DNS zone Serial
by 1 and run sudo rndc reload
to apply the zone change.
It is very important to change the zone serial, because it indicates that the zone has been changed and updates must be propagated.
After all the checks are passed, the certificate will be issued and ready for use:
$ sudo ls -l /etc/letsencrypt/live/ech-labs.com
total 4
-rw-r--r-- 1 root root 692 Nov 10 13:08 README
lrwxrwxrwx 1 root root 36 Nov 10 13:08 cert.pem -> ../../archive/ech-labs.com/cert1.pem
lrwxrwxrwx 1 root root 37 Nov 10 13:08 chain.pem -> ../../archive/ech-labs.com/chain1.pem
lrwxrwxrwx 1 root root 41 Nov 10 13:08 fullchain.pem -> ../../archive/ech-labs.com/fullchain1.pem
lrwxrwxrwx 1 root root 39 Nov 10 13:08 privkey.pem -> ../../archive/ech-labs.com/privkey1.pem
Enabling HTTPS
Replace the Nginx configuration file /opt/ech/conf/nginx.conf
with the following one:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
ssl_echkeydir ech_keys;
sendfile on;
keepalive_timeout 65;
# HTTP
server {
listen 80 default_server;
server_name localhost;
location / {
root html;
ssi on;
index index.html index.htm;
keepalive_timeout 0;
# Force non-cache
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache';
if_modified_since off;
expires off;
etag off;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
# HTTPS
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/letsencrypt/live/ech-labs.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ech-labs.com/privkey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
ssi on;
index index.html index.htm;
keepalive_timeout 0;
# Force non-cache
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache';
if_modified_since off;
expires off;
etag off;
}
}
}
Note that the ssl_echkeydir ech_keys
directive in the http
block of the Nginx config comes with an Nginx ECH support patch and defines the directory where the ECH configurations are stored. For now, we’ll just create that directory and populate it later:
$ sudo mkdir /opt/ech/conf/ech_keys
Now restart Nginx:
$ sudo /opt/ech/sbin/nginx -s quit; sudo /opt/ech/sbin/nginx
Now, point the web browser to https://n1.ech-labs.com, and it should display the default “Welcome to nginx!” page loaded securely using HTTPS.
Enabling ECH
Now we’ll enable ECH for test1.ech-labs.com and test2.ech-labs.com. We will need to:
- Generate the ECHConfig and a private key.
- Add DNS A records for test1.ech-labs.com and test2.ech-labs.com.
- Add a DNS A record for the server’s public name, e.g. cover.ech-labs.com (this name will be visible in the SNI of the TLS with ECH enabled).
- Add DNS HTTPS records with ECHConfig for test1.ech-labs.com and test2.ech-labs.com.
Generating the ECHConfig and a private key
Let’s enter the directory we’ve created for ECH configurations and generate our first ECHConfig and a private key using patched OpenSSL:
$ cd /opt/ech/conf/ech_keys
$ sudo /opt/ech/bin/openssl ech -public_name cover.ech-labs.com -pemout config1.ech
Wrote ECH key pair to config1.ech
Let’s inspect the contents of config1.ech
:
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEIGD3RHrWw1V1s1ocwvUPHmXnAiLau0zrhCf/kWGhluht
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AEX+DQBBSQAgACBuPcsDfK+zfZY0gE1U80ppEIny7ZVjHw+y2AiJFqsZBAAEAAEAAQASY292ZXIuZWNoLWxhYnMuY29tAAA=
-----END ECHCONFIG-----
Just out of curiosity, let’s parse it:
$ /opt/ech/bin/openssl ech -pemin ./config1.ech
Loaded Key+ECHConfigList from: ./config1.ech
ECH details (1 configs total)
index: 0: SNI (inner:NULL;outer:NULL), ALPN (inner:NULL;outer:NULL)
[fe0d,49,cover.ech-labs.com,0020,[0001,0001],6e3dcb037cafb37d9634804d54f34a691089f2ed95631f0fb2d8088916ab1904,00,00]
What we see here:
fe0d
– the ECH version (draft 13) in hex49
– the config identifier (73) in hexcover.ech-labs.com
– the public name we’ll put in the outer (unencrypted) SNI0020
– the maximum name length (optional, used for calculation padding) in hex[0001,0001]
– the HPKE Cipher Suite: HKDF-SHA256/AES-128-GCM6e3dcb03...
– the public key
Creating the required DNS records
To add the required DNS records, we need to append the following lines to the zone file /etc/bind/db.ech-labs.com
:
cover IN A 123.1.1.1
test1 IN A 123.1.1.1
test2 IN A 123.1.1.1
test1 IN HTTPS 1 . alpn="h2" ech="AEX+DQBBSQAgACBuPcsDfK+zfZY0gE1U80ppEIny7ZVjHw+y2AiJFqsZBAAEAAEAAQASY292ZXIuZWNoLWxhYnMuY29tAAA="
test2 IN HTTPS 1 . alpn="h2" ech="AEX+DQBBSQAgACBuPcsDfK+zfZY0gE1U80ppEIny7ZVjHw+y2AiJFqsZBAAEAAEAAQASY292ZXIuZWNoLWxhYnMuY29tAAA="
The values for ech=
in the HTTPS records are Base64-encoded ECHConfig, found between -----BEGIN ECHCONFIG-----
and -----END ECHCONFIG-----
in the file /opt/ech/conf/ech_keys/config1.ech
which we’ve inspected previously.
Also, DON’T FORGET TO INCREASE THE SERIAL IN THE ZONE FILE.
Now, we’ll reload the DNS server with sudo rndc reload
to start propagating the DNS records.
Wait 15 minutes and test whether the records are correctly propagated using dns.google:
- https://dns.google/query?name=cover.ech-labs.com&rr_type=a
Result must containAnswer
with"name": "cover.ech-labs.com.", "type": 1, "data": "123.1.1.1"
- https://dns.google/query?name=test1.ech-labs.com&rr_type=a
Result must containAnswer
with"name": "test1.ech-labs.com.", "type": 1, "data": "123.1.1.1"
- https://dns.google/query?name=test2.ech-labs.com&rr_type=a
Result must containAnswer
with"name": "test2.ech-labs.com.", "type": 1, "data": "123.1.1.1"
- https://dns.google/query?name=test1.ech-labs.com&rr_type=https
Result must containAnswer
with"name": "test1.ech-labs.com.", "type": 65, "data": "1 . alpn=h2 ech=AEX+DQBB(...)Y29tAAA="
- https://dns.google/query?name=test2.ech-labs.com&rr_type=https
Result must containAnswer
with"name": "test2.ech-labs.com.", "type": 65, "data": "1 . alpn=h2 ech=AEX+DQBB(...)Y29tAAA="
If the records are correct, we are ready for testing.
5. Testing
To see what is happening with TLS and ECH, let’s replace the web server’s default index.html with the following:
<pre>
HTTP host: <!--# echo var="http_host" -->
ALPN protocol: <!--# echo var="ssl_alpn_protocol" -->
SSL cipher: <!--# echo var="ssl_cipher" -->
SSL protocol: <!--# echo var="ssl_protocol" -->
SNI: <!--# echo var="ssl_server_name" -->
ECH status: <!--# echo var="ssl_ech_status" -->
Outer SNI (public name): <!--# echo var="ssl_ech_outer_sni" -->
Inner SNI: <!--# echo var="ssl_ech_inner_sni" -->
</pre>
This will display various Nginx variables using SSI (we have SSI on; directives in the Nginx config).
We’ll visit http://ns1.cujo-labs.com using Chrome (v117+).
Since we use plain HTTP, the page should contain:
HTTP host: ns1.cujo-labs.eu
ALPN protocol: (none)
SSL cipher: (none)
SSL protocol: (none)
SNI: (none)
ECH status: (none)
Outer SNI (public name): (none)
Inner SNI: (none)
Now, let’s visit https://ns1.cujo-labs.com or https://cover.cujo-labs.com using Chrome (v117+).
Since we don’t have HTTPS records with ECHConfig for these sites, ECH will not be attempted (ECH status: not attempted
). The page should contain:
HTTP host: ns1.cujo-labs.eu
ALPN protocol: h2
SSL cipher: TLS_AES_256_GCM_SHA384
SSL protocol: TLSv1.3
SNI: ns1.cujo-labs.eu
ECH status: not attempted
Outer SNI (public name): NONE
Inner SNI: NONE
Now, we’ll visit visit https://test1.cujo-labs.com and https://test2.cujo-labs.com using Chrome (v117+).
We have HTTPS records with ECHConfig for these sites, so ECH will be used (ECH status: success
). The pages should contain:
HTTP host: test1.cujo-labs.com
ALPN protocol: h2
SSL cipher: TLS_AES_256_GCM_SHA384
SSL protocol: TLSv1.3
SNI: test1.cujo-labs.com
ECH status: success
Outer SNI (public name): cover.cujo-labs.com
Inner SNI: test1.cujo-labs.com
HTTP host: test2.cujo-labs.com
ALPN protocol: h2
SSL cipher: TLS_AES_256_GCM_SHA384
SSL protocol: TLSv1.3
SNI: test2.cujo-labs.com
ECH status: success
Outer SNI (public name): cover.cujo-labs.com
Inner SNI: test2.cujo-labs.com
We can also check whether ECH was enabled for a certain request by using Chrome Developer Tools’ Security
tab and choosing the URL in the Main origin
(on the left):
If we dump the handshake traffic of this test with Wireshark, we will see the following:
No. Time Source Address Destination Address Source Destination Info
40 17:36:44.920833 192.168.9.100 123.1.1.1 Apple_12:fb:0c 12:aa:fa:fe:9f:cd Client Hello
Frame 40: 583 bytes on wire (4664 bits), 583 bytes captured (4664 bits) on interface en0, id 0
Ethernet II, Src: Apple_12:fb:0c (f8:ff:c2:12:fb:0c), Dst: 12:aa:fa:fe:9f:cd (12:aa:fa:fe:9f:cd)
Internet Protocol Version 4, Src: 192.168.9.100, Dst: 123.1.1.1
Transmission Control Protocol, Src Port: 60604, Dst Port: 443, Seq: 1, Ack: 1, Len: 517
Transport Layer Security
TLSv1.3 Record Layer: Handshake Protocol: Client Hello
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 512
Handshake Protocol: Client Hello
Handshake Type: Client Hello (1)
Length: 508
Version: TLS 1.2 (0x0303)
Random: 14c7fd5b15a0452723d2ab119d914886c0391d4e4bfe2fbe53a2cc399c7837c8
Session ID Length: 32
Session ID: ffb8e7eb3b81684ee37a38d0b630728290f7c9b94267b2d466aa5cedf88b34fb
Cipher Suites Length: 32
Cipher Suites (16 suites)
Compression Methods Length: 1
Compression Methods (1 method)
Extensions Length: 403
Extension: Reserved (GREASE) (len=0)
Extension: session_ticket (len=0)
Extension: application_settings (len=5)
Extension: supported_versions (len=7)
Extension: ec_point_formats (len=2)
Extension: signature_algorithms (len=18)
Extension: application_layer_protocol_negotiation (len=14)
Extension: supported_groups (len=10)
Extension: encrypted_client_hello (len=186)
Type: encrypted_client_hello (65037)
Length: 186
Client Hello type: Outer Client Hello (0)
Cipher Suite: HKDF-SHA256/AES-128-GCM
KDF Id: HKDF-SHA256 (1)
AEAD Id: AES-128-GCM (1)
Config Id: 73
Enc length: 32
Enc: b90eab14c57a87642d3017635c43bbcddd73656e48346ed4f67b441470ea817f
Payload length: 144
Payload: 722069d62433b0db8efb26984e47c784c6ab0baaad871ed9a8fefc1088fc3af2fb0ecea3…
Extension: signed_certificate_timestamp (len=0)
Extension: status_request (len=5)
Extension: renegotiation_info (len=1)
Extension: server_name (len=27)
Type: server_name (0)
Length: 23
Server Name Indication extension
Server Name list length: 21
Server Name Type: host_name (0)
Server Name length: 18
Server Name: cover.ech-labs.com
Extension: compress_certificate (len=3)
Extension: extended_master_secret (len=0)
Extension: key_share (len=43)
Extension: psk_key_exchange_modes (len=2)
Extension: Reserved (GREASE) (len=1)
Extension: padding (len=3)
We can see that there’s Extension: encrypted_client_hello
with the configuration identifier we generated (Config Id: 73
) and cipher suites. The payload contains the encrypted SNI test1.ech-labs.com
that we were actually visiting, while the Extension: server_name
contains the public name cover.ech-labs.com
that we had specified in the ECHConfig. That’s how ECH works – we mask the actual SNI behind a public name.
6. A note on GREASE ECH
Both Firefox and Chrome have ECH support with GREASE. In the ECH context, this means that when a website is not ECH-enabled (there’s no ECHConfig in the HTTPS DNS records for the site), a dummy ECH extension is generated and used in the TLS handshake, with the real site name visible in outer SNI.
In this case, the server tries to decrypt the ECH extension, fails, and falls back to the outer SNI (which is the actual real site name in the GREASE ECH).
7. Next steps
This post covers only the bare minimum setup for ECH. What was left behind:
- Setting up ECH in Split Mode
- Multiple ECHConfigs per website
- Synchronizing ECHConfig generation, DNS records and Web server configuration
- Automatic rotation of ECHConfigs and their private keys