Setting Up ECH for a Website

header image
All posts

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.


the architecture needed to set up an ECH-enabled website.
The image shows a Client running a browser sending two request, the first one is TLS with ECH to a web server on VM1, the second request is ECHConfig retrieval to a recursive DNS server, which is then sent to authoritative DNS servers for the domain. The two DNS servers: primary and secondary, are set up on VM1 and VM2, respectively

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 hex
  • 49 – the config identifier (73) in hex
  • cover.ech-labs.com – the public name we’ll put in the outer (unencrypted) SNI
  • 0020 – the maximum name length (optional, used for calculation padding) in hex
  • [0001,0001] – the HPKE Cipher Suite: HKDF-SHA256/AES-128-GCM
  • 6e3dcb03... – 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:

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):

Chrome developer tools security tab Main Origin section showing Encrypted ClientHello enabled

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