curl 8.13.0 Ships –ech true: Hardfail vs Grease for Encrypted Client Hello

Last updated: May 03, 2026

Event date: April 2, 2026 — curl/curl 8.13.0


curl 8.13.0, released on April 2, 2025, hardened its Encrypted Client Hello story by adding rustls-backed support for ECHConfigList retrieval over DoH plus full GREASE generation. The --ech flag accepts four values — false, grease, true, and hard — and they encode three distinct threat models, not one feature. Pick --ech hard when ECH must succeed or the request must fail loudly. Pick true only if you accept silent downgrade to cleartext SNI. Use grease as cover traffic for the rest of the internet, not for yourself.

In this post

  • Release: curl 8.13.0 shipped on April 2, 2025; ECH itself remains listed in docs/EXPERIMENTAL.md.
  • Modes: --ech false (off, default), grease (decoy), true (best-effort), hard (abort on failure).
  • Failure code: --ech hard exits with CURLE_ECH_REQUIRED (101) if the handshake cannot be encrypted.
  • Backends with ECH in 8.13.0: rustls-ffi, wolfSSL, BoringSSL, AWS-LC, custom-patched OpenSSL. Stock OpenSSL releases still no-op.
  • Required infrastructure: a DNS HTTPS RR (RFC 9460, type 65) carrying an ech= SvcParam, ideally fetched via --doh-url to avoid leaking the lookup.

The 70-second answer: which –ech mode to pick

Each mode answers a different question. --ech hard answers “is the SNI for this request actually encrypted, yes or no?” and aborts on no. --ech true answers “would you like ECH if it happens to be cheap?” and ships your SNI in cleartext when it isn’t. --ech grease answers “are you willing to add a decoy extension to help blend the wider internet?” and provides zero privacy on the current TLS handshake. The trap is that “true” sounds stronger than “grease” and weaker than “hard” — so operators reach for it expecting a middle ground. There is no middle ground. There is only “abort on failure” or “silently downgrade”.

Terminal output for curl 8.13.0 Ships --ech true: Hardfail vs Grease for Encrypted Client Hello
Real output from a sandboxed container.

The terminal capture above shows the divergence between modes back-to-back against the same hostname: with --ech hard against a name that lacks an HTTPS RR, curl prints ECH required but not used and exits 101. With --ech true against the identical hostname, the request completes with HTTP 200 and the ClientHello on the wire carries plaintext SNI. Same flag family, opposite security posture.

What actually shipped in curl 8.13.0

The curl 8.13.0 changelog lists six ECH-related entries — three feature additions on the rustls backend and three build-system fixes:

  • rustls: support ECH w/ DoH lookup for config
  • rustls: add ECH support w/ string ECH config
  • rustls: support ECH GREASE
  • cmake: fix ECH detection in custom-patched OpenSSL
  • cmake: fix typo in ECH config error msg
  • configure: fix ECH detection with MultiSSL

Daniel Stenberg’s 8.13.0 release post calls this out as one of the headline items, but does not announce promotion out of experimental status. The EXPERIMENTAL.md file still lists “Use of the HTTPS resource record and Encrypted Client Hello (ECH) when using DoH” as not stabilized. The graduation criteria explicitly require ECH support in a TLS library other than BoringSSL and wolfSSL — which is exactly the gap rustls-ffi started filling in this release. Treat 8.13.0 as the moment ECH became practically usable on a redistributable curl build, not as the moment ECH became stable.

I wrote about HTTPS internals primer if you want to dig deeper.

Official documentation for curl 8.13 ech hardfail grease
Canonical reference.

The screenshot of the official ECH documentation captures what the rest of the option surface looks like: --ech ecl:<b64value> for pasting an ECHConfigList from somewhere other than DNS, and --ech pn:<name> for overriding the public_name. Those two suffixes are independent of the mode word and let you pin a config when DoH is unavailable.

Hardfail vs true: the silent-fallback trap

The single most expensive mistake an operator can make with this feature is reading --ech true as “ECH on” and shipping it. The official CURLOPT_ECH documentation defines the value precisely: “Instructs client to attempt ECH, if possible, but to not fail if attempting ECH is not possible.” Translation: if the HTTPS RR is missing, malformed, expired, or the TLS server rejects the inner ClientHello, curl quietly retries with the original outer SNI. A passive observer on the path sees the requested hostname in cleartext and you receive HTTP 200. There is no warning in the verbose output beyond a single line noting that ECH was not used.

By contrast, --ech hard — the value behind the colloquial “hardfail” label in the title above — is documented as: “Instructs client to attempt ECH and fail if attempting ECH is not possible.” The verbose log shows the failure reason, the exit code is non-zero, and no fallback handshake is sent. That is the only mode that gives you the property the IETF ECH draft was designed to deliver: the SNI is encrypted or the request is dropped.

If you need more context, spotting encryption in captures covers the same ground.

For a CI script that posts to a private endpoint, --ech true is actively dangerous: it gives you a feeling of confidentiality without the property. For a casual curl -O against a CDN that may or may not have ECH live, true is fine and matches user intent. Choose by intent, not by the word that sounds least scary.

What grease actually does — and doesn’t do

GREASE — Generate Random Extensions And Sustain Extensibility — is an IETF practice from RFC 8701 for shipping random-looking unknown extensions so middleboxes never get to assume that “no ECH extension” means anything. --ech grease emits a fake ECH extension with random ciphertext bytes. A passive observer cannot tell whether you actually performed ECH or not, because everyone is sending the extension. That is the population-level benefit.

It does not encrypt your current ClientHello. The outer SNI in a GREASE-only handshake still contains the real hostname. If the question is “does an observer see example.com?” the answer is yes. If the question is “does the existence of an ECH extension in my packet leak that I’m trying to hide something?” — that is the question grease is designed to answer, and the answer is no.

If you need more context, inspecting ClientHello on the wire covers the same ground.

Topic diagram for curl 8.13.0 Ships --ech true: Hardfail vs Grease for Encrypted Client Hello
Purpose-built diagram for this article — curl 8.13.0 Ships –ech true: Hardfail vs Grease for Encrypted Client Hello.

The diagram above maps the three modes to what a passive observer captures on the wire: with hard, the SNI is encrypted or the connection fails to reach the server’s Certificate message; with true on a misconfigured target, the observer sees plaintext SNI and a successful handshake; with grease, the observer sees plaintext SNI and a decoy encrypted_client_hello extension whose payload decrypts to nothing. Three modes, three different shapes on the wire.

The DNS HTTPS RR dependency nobody mentions

Every ECH mode except an explicit ecl: override depends on a DNS HTTPS resource record (RFC 9460, type 65) carrying an ech= SvcParam. A quick check:

$ dig +short HTTPS crypto.cloudflare.com
1 . alpn="http/1.1,h2" ipv4hint=162.159.137.85,162.159.138.85 ech=AED+DQA8...
$ dig +short HTTPS example.com
$

The first hostname returns an ech= blob — the base64 ECHConfigList curl will use to encrypt the inner ClientHello. The second returns nothing, which is why --ech hard https://example.com/ fails immediately and --ech true https://example.com/ silently downgrades.

For more on this, see encrypted DNS transport basics.

The lookup itself can leak. If your stub resolver is a corporate DNS server, an ISP recursor, or a captive-portal forwarder, that party sees you ask for the HTTPS record of the very hostname you intend to hide. The fix is to pair --ech with --doh-url:

curl --doh-url https://cloudflare-dns.com/dns-query \
     --ech hard https://crypto.cloudflare.com/cdn-cgi/trace

Now the HTTPS RR lookup itself rides over DoH to a separate provider and the only on-path observer with full visibility is the DoH provider you chose. Without --doh-url, ECH protects the TLS layer and your local resolver still gets a free copy of your browsing history.

TLS backend matters: rustls vs wolfSSL vs OpenSSL in 8.13.0

Whether your --ech flag does anything at all depends on which TLS library curl was linked against. The ECH.md file in the 8.13.0 tree lists the qualifying backends as OpenSSL 4.0.0+, BoringSSL, AWS-LC, wolfSSL, and rustls-ffi. The packaged OpenSSL 3.x in every major Linux distribution as of May 2026 does not qualify. Verify with:

curl -V
curl 8.13.0 (x86_64-pc-linux-gnu) libcurl/8.13.0 rustls/0.23.x ...
Features: alt-svc AsynchDNS ECH HSTS HTTP2 HTTPS-RR IPv6 ...

If ECH and HTTPS-RR both appear in the Features line, you are good. If they do not, every --ech value is a no-op except for surfacing an “ECH not supported” message. The cmake fix in 8.13.0 (“fix ECH detection in custom-patched OpenSSL”) is a tell: stock OpenSSL still requires Stephen Farrell’s out-of-tree patch set to compile ECH support in. For a clean redistributable build, the curl-rustls combination introduced here is now the path of least resistance.

how TLS handshakes really work goes into the specifics of this.

Threat-model decision rubric

Comparison: curl 8.13 ECH Modes

Options compared side-by-side — curl 8.13 ECH Modes.

The bar chart above plots the three live modes against the four properties that actually matter — SNI confidentiality on the current connection, observer plausible deniability, failure visibility, and dependency footprint — to make explicit that no single mode wins on every axis. The decision rubric below is the operational version of that chart: pick by adversary, not by feature checklist.

More detail in layered transport security.

curl 8.13.0 --ech mode selection by threat model
Mode Threat model SNI on the wire Failure behaviour When it stops being right
hard Active or passive on-path observer; SNI confidentiality is a requirement Encrypted or no handshake at all Exit 101, CURLE_ECH_REQUIRED When the target reliably has no HTTPS RR and you still need the data
true Best-effort privacy against logging middleboxes; correctness matters more than confidentiality Encrypted if RR present, cleartext otherwise — silently Falls back to plaintext SNI, returns success The instant you would care if the request leaked the hostname
grease Civic-hygiene contributor; you are protecting the population, not yourself Cleartext, plus a decoy extension Connection succeeds normally When you actually need confidentiality on this request
false You are debugging the TLS stack and want a clean baseline Cleartext, no extension Connection succeeds normally Anywhere the population-level GREASE benefit matters more than the diagnostic benefit
Radar chart: curl 8.13 ECH

Different lenses on curl 8.13 ECH.

The radar chart pairs the table by overlaying each mode’s coverage of the same four axes — useful for arguing in a design review which mode the default deployment template should use, since the visual makes it obvious that true wins zero axes outright.

What to expect next: ECH draft status, Cloudflare/Firefox rollout, and when –ech true stops being a trap

Encrypted Client Hello is still an IETF draft — see draft-ietf-tls-esni, which has churned through more than twenty revisions on its way toward RFC status. Cloudflare has had ECH live for orange-cloud zones since 2023 and re-enabled it after a brief 2024 pause; their deployment writeup remains the canonical operational reference. Firefox ships ECH behind network.dns.echconfig.enabled, which is on by default in current releases when DoH is enabled.

The gating factor for --ech true ever being safe-by-default is HTTPS RR coverage. As long as most of the internet has no ech= SvcParam, “true” will silently fall back on most of the internet. Until that flips, treat the flag as a three-way switch keyed to a real adversary: use hard when the SNI must be hidden, grease when you want to contribute to the population baseline, and reach for true only with eyes open about what it costs when the dependencies are not in place.

More detail in complementary privacy tooling.

Sources

More From Author

Caddy 2.9.1 Adds an on_demand_tls interval Directive After a Thundering-Herd Bug

Leave a Reply

Your email address will not be published. Required fields are marked *