TLS 1.3 把幾乎整個交握都加密了,唯獨 ClientHello 裡的 SNI(Server Name Indication)欄位還是明文——只要 ISP、企業防火牆、公共 Wi-Fi 網關看得到封包,就能一眼看出使用者連的是哪個網域。這件事讓「HTTPS = 隱私」的宣稱始終缺一角。Encrypted ClientHello(ECH)用 DNS 攜帶伺服器的臨時公鑰、把 SNI 包在第二層加密裡,把這個缺口補起來。2026 年 3 月 RFC 9848 正式敲定,Caddy 2.10 隨後把 ECH 端到端跑通,2.11 把金鑰輪替也丟回排程器手上——自架反向代理要落地 ECH,現在只差一個 DNS 外掛。
SNI 為什麼還是明文,補救為什麼拖到現在
早期 TLS 用不同 IP 分辨每張憑證,加了 SNI 之後才能在同一個 IP 上掛多域名,代價是 ClientHello 得先把要連的網域喊出來,伺服器才知道該遞哪一張憑證。這個時序矛盾就寫在協定裡:沒握完手就不能加密,但要挑憑證又非得先知道網域。
ESNI(Encrypted SNI)試過用 DNS 發布公鑰、加密 SNI,2018 年 draft-01 開始跑了幾年,最後在跨中介盒相容性上撞牆。ECH 是重來一次的產物,把整個 ClientHello 而不只是 SNI 用「外層公開網域」的憑證再包一層。middlebox 看到的只是 outer SNI(例如 public.example.com),真正的 inner SNI(admin.example.com、gitlab.internal.example.com)被 HPKE 加密封在 ClientHelloOuter 的 extension 裡,只有握有私鑰的伺服器解得開。
規格拉這麼久是因為要跟現存的 middlebox、中央 CDN、企業內網解密設備角力。draft 從 2020 年一路演到 draft-24,Cloudflare、Firefox、Chrome 早期都在小流量上輪番試。RFC 9848 是把「怎麼用 DNS SVCB / HTTPS record 攜帶 ECHConfig」這一段收乾,網頁伺服器要做的事情才有一份可以引用的固定合約。
ECH 的兩層信封怎麼疊起來
ECH 的核心是把 ClientHello 拆成兩份:ClientHelloOuter 帶的是外層公開網域的 SNI,收件人是任何看得到 outer 網域對應憑證的伺服器;ClientHelloInner 帶著真正的目標網域,被 HPKE(Hybrid Public Key Encryption)用伺服器發布在 DNS 上的公鑰加密後,塞進 outer 的 encrypted_client_hello extension。
流程大致是這樣:
- 客戶端要連
admin.example.com,先發 DNS 查詢拿到該網域的 HTTPS 記錄,裡面有ech=參數,值是 base64 的 ECHConfig(含公鑰、KDF、AEAD 參數、外層公開網域名稱) - 客戶端用該公鑰把「真正的 ClientHello」加密,包進 ClientHelloOuter 的 extension,outer 的 SNI 填成 ECHConfig 指定的
public_name - 中間網路只看得到 outer SNI,看不到內層網域
- 伺服器用自己的 ECH 私鑰解出 inner ClientHello,接著才用真正的網域憑證完成 TLS 交握
實作上要注意兩點:ECHConfig 的公鑰要輪替,過期沒換掉客戶端會 fallback 到明文 SNI 或直接失敗;outer 網域必須是伺服器實際能出示合法憑證的網域,不能亂填一個匿名字串,否則客戶端會拒絕。
Caddy 2.10 打底,2.11 把輪替也接管
Caddy 2.10 在 2026 年 4 月釋出時第一次帶完整 ECH:伺服器在啟動時生成 HPKE 金鑰、產出 ECHConfig、透過已設定的 DNS 供應商外掛把 HTTPS record 寫進 DNS,接下來的每個新連線就吃這組 ECH 設定。當時金鑰輪替是手動的,管理員得記得定期呼叫 API 或直接重啟。
2.11 版把這一段搬進內建排程器:預設 ECH 金鑰壽命是幾天到幾週不等(依環境設定),Caddy 會提前產出新一組 ECHConfig、把 DNS record 更新成「新舊金鑰並存」的清單,等舊金鑰過保留期之後再從 DNS 撤下。這個轉換窗口讓已經 pin 到舊 ECHConfig 的客戶端不會突然斷線,也把 forward secrecy 的粒度從「一台伺服器一輩子一把 key」壓到「一把 key 撐幾週」。
除了 ECH,2.11 還把 reverse proxy 對上游是 HTTPS 時的 Host header 改寫預設打開了,log_append 可以吃 request/response body 方便除錯,也支援 zstd log 壓縮。這些不是 ECH 直接相關,但升級時可以順手校準。
xcaddy 打包 DNS 外掛的實務
Caddy 官方發行版沒帶任何 DNS 供應商外掛,因為那是龐大清單、每個雲要獨立套件。ECH 需要 Caddy 有權限寫 DNS,所以第一步是用 xcaddy 打一份帶對應 DNS 外掛的二進位。用 Cloudflare DNS 舉例:
1 | xcaddy build v2.11.4 \ |
打出來的二進位丟到 /usr/local/bin/caddy 覆蓋掉發行版,systemd unit 不用改。Caddyfile 這樣寫:
1 | { |
public_name 就是外層 SNI 對外看得見的名字,需要一張合法憑證掛在同一台 Caddy 上——通常是隨手做一個 landing page。CF_API_TOKEN 給 Cloudflare API 權限的 scope 只要 Zone:DNS:Edit 加對應的 zone,不需要 Global API Key。
啟動後檢查 journalctl -u caddy -f,應該會看到 generated new ECH config 跟 published ECH config to DNS 兩行 log。用 dig admin.example.com HTTPS 也應該回傳含 ech= 參數的 record。
驗證端到端有沒有真的在跑 ECH
伺服器發布了 ECHConfig,不代表客戶端一定會用。Firefox 119 之後、Chromium 117 之後都預設打開 ECH,但前提是客戶端能真的拿到 HTTPS record——沒開 DoH(DNS-over-HTTPS)的話,DNS 查詢還是走明文,中間人一樣看得到,ech= 記錄可能被中介 DNS 剝掉。
Chrome 端的檢查:chrome://net-internals/#dns 清乾淨、開 chrome://flags 確認 Encrypted Client Hello 跟 Use DNS HTTPS SVCB 兩個旗標是 default(新版已經預設打開),再到 chrome://settings/security 啟用 Secure DNS,設 Cloudflare 或 Google 當 DoH resolver。連上網站後 chrome://net-internals/#events 搜 SSL_CONNECT,正常會看到 ssl_ech_status: success。
Firefox 端:about:config 把 network.dns.echconfig.enabled、network.dns.http3_echconfig.enabled、network.trr.mode 設 3(強制 DoH)、network.trr.uri 指向 DoH 端點。連上後 about:networking#dns 應該能看到有 ECHConfig 的網域。
想抓封包驗證的話,Wireshark 打開後過濾 tls.handshake.type == 1,正常情況下應該看不到你要連的 inner 網域名稱,只看得到 outer public name。
ECH 沒能守住的邊界
ECH 常被誤讀成「HTTPS 隱私最後一塊拼圖」,實務上它守住的區塊比宣稱的小。DNS 查詢本身如果走明文,攻擊者一樣先看到你查了 admin.example.com——ECH 保護的是 TLS 交握這一段,DNS 那一段要靠 DoH、DoQ、DoT 才蓋得住。臺灣網路環境的中央 DNS 通常不會主動剝 SVCB record,但學術網路、企業 VPN、部分公共 Wi-Fi 會,這些場景 ECH 就會 fallback 到明文 SNI,失敗方式沒告警。
TLS 指紋(JA3/JA4)不受 ECH 影響。作業系統跟客戶端的 cipher suite、extension 順序組合起來還是個高熵值指紋,主動分類流量的中間盒能靠這個猜到大致的軟體,只是不再能對到具體網域。
大流量的伺服器把不同租戶塞在同一個 outer public name 底下,中間人看到的 SNI 都會是那個公開名字——這正是 ECH 想要的匿名集效應,也代表如果一個 outer name 底下只掛一個 inner 網域,觀察者從流量大小、TLS 特徵、目的 IP 還是有機會反推。要真的匿名,就要把「outer public name 對應多個 inner 網域」這件事做出來,或直接跟 CDN 這種天然大流量匿名集共用 outer。
跟企業 TLS 中介盒的關係也要事先想清楚。ECH 是設計來抵抗這種中介盒的,如果部署環境本來就靠 SSL Inspection 做 DLP,ECH 會直接把那條路徑打斷——不是 bug,是規格。合規需求上有這條線的環境,得評估是不是給特定內網放 exception。
什麼時候該打開 ECH,什麼時候先擱著
需要對外提供服務、又不希望中介觀察者能列出所有租戶網域的場景——像 SaaS 多租戶主控台、私人 Git 平台、內部工具的公網入口——ECH 值得馬上上。設定成本就是換個 Caddy 二進位、加 DNS 外掛、多維護一個 outer public name 的憑證。
反過來,內網 only、只給 VPN 內用戶連的服務,ECH 幫的忙有限,DNS 也不見得走 DoH,先把 WireGuard、Tailscale 這類 overlay 做好比較實在。CDN 前面已經接 Cloudflare 或類似服務、且該 CDN 已經打開 ECH 的網域,源站再開一次 ECH 沒有加分,因為使用者實際交握對象是 CDN。
伺服器版本上仍然卡在 Nginx 的環境,ECH 目前得靠 patch 或前面掛 Caddy 當 TLS terminator——這也是把 Caddy 從「Nginx alternative」推向「Nginx 前面那一層」的自然演進。Rust 生態的 Rustls 已經合進 ECH 支援,未來像 River、Pingora 這類 Rust 反向代理跟進的門檻不高,但一年內能在生產跑穩的成熟度,Caddy 目前是唯一選擇。
自架 VPS 上跑反向代理、又在意 TLS 隱私這條線的團隊,Caddy 2.11 加 ECH 是目前投入產出比最高的做法。NCSE Network 提供的臺灣 Intel Gold + NVMe VPS 節點適合這種需要低延遲 DNS 更新、又要穩定跑 TLS 反向代理的場景,如果需要搭配自主 IP Transit 做 outer public name 的多站點匿名集,也可以直接詢問方案細節。