鏈與Nginx排障全鏈路)
第一次在抓包工具里看到自己登錄請(qǐng)求里的賬號(hào)密碼以純文本形式躺在那里說(shuō)實(shí)話有點(diǎn)后背發(fā)涼——那條請(qǐng)求穿過(guò)了家里的路由器、運(yùn)營(yíng)商鏈路、若干個(gè)中間設(shè)備任何一環(huán)想看一眼都能看得一清二楚。這就是HTTP最原始的樣子它假設(shè)鏈路是可信的而現(xiàn)實(shí)里鏈路從來(lái)不可信。HTTPS存在的意義就是把假設(shè)換成驗(yàn)證。這篇內(nèi)容我會(huì)從一次明文抓包的現(xiàn)場(chǎng)講起把TLS握手里每一步在干什么、證書(shū)信任鏈?zhǔn)窃趺匆粚訉哟钇饋?lái)的、HTTPS和HTTP在報(bào)文語(yǔ)義與工程行為上到底差在哪以及證書(shū)部署、Nginx配置、線上排障的完整鏈路都過(guò)一遍。適合已經(jīng)會(huì)寫(xiě)接口但沒(méi)系統(tǒng)啃過(guò)TLS的開(kāi)發(fā)者也適合正在給站點(diǎn)上證書(shū)、被證書(shū)鏈不完整混合內(nèi)容這類(lèi)報(bào)錯(cuò)卡住的同學(xué)。文中所有域名、證書(shū)、場(chǎng)景均為示例參數(shù)以主流實(shí)踐為準(zhǔn)。1. 從一次抓包現(xiàn)場(chǎng)看HTTP到底缺了什么1.1 明文報(bào)文在鏈路上到底有多透明HTTP的本質(zhì)是一段可讀文本請(qǐng)求行、請(qǐng)求頭、請(qǐng)求體依次排開(kāi)中間用空行分隔。這種設(shè)計(jì)在早期是優(yōu)點(diǎn)調(diào)試方便隨手一個(gè)telnet就能發(fā)請(qǐng)求日志里一眼能看出問(wèn)題。但它同時(shí)意味著報(bào)文在網(wǎng)絡(luò)中每一跳都是裸奔狀態(tài)。家用路由器、公司的上網(wǎng)行為管理設(shè)備、機(jī)房里的鏡像交換機(jī)只要能看到流量就能把報(bào)文內(nèi)容原樣打印出來(lái)。很多人以為我訪問(wèn)的是內(nèi)網(wǎng)系統(tǒng)沒(méi)人看。這個(gè)假設(shè)成立的前提是鏈路完全可控而現(xiàn)實(shí)是只要有一次請(qǐng)求經(jīng)過(guò)公共網(wǎng)絡(luò)鏈路就不再由你掌控。更隱蔽的是被動(dòng)記錄流量被留存下來(lái)當(dāng)時(shí)沒(méi)人看幾個(gè)月后被人翻出來(lái)分析這類(lèi)風(fēng)險(xiǎn)在明文協(xié)議下無(wú)法消除。所以討論HTTP的安全性第一句話應(yīng)該是它壓根沒(méi)打算提供安全性它只負(fù)責(zé)把字節(jié)從A搬到B。1.2 三個(gè)缺口機(jī)密性、完整性、身份真實(shí)性把HTTP的問(wèn)題拆開(kāi)看其實(shí)是三個(gè)獨(dú)立的缺口而不是一個(gè)籠統(tǒng)的不安全。機(jī)密性缺失報(bào)文可讀賬號(hào)、令牌、業(yè)務(wù)數(shù)據(jù)全部暴露。這是最容易理解的一個(gè)。完整性缺失中間設(shè)備不僅能看還能改。你請(qǐng)求的是A頁(yè)面返回的可能被插入了一段腳本你提交的金額是100中途可能被改成1000。HTTP本身沒(méi)有任何校驗(yàn)機(jī)制來(lái)發(fā)現(xiàn)這種篡改接收方拿到什么就信什么。身份真實(shí)性缺失你連上的服務(wù)器到底是不是你以為的那臺(tái)HTTP層面無(wú)從判斷。DNS被污染、鏈路被劫持到一臺(tái)仿冒服務(wù)器客戶(hù)端完全感知不到因?yàn)樗鼪](méi)有對(duì)方是誰(shuí)這個(gè)概念。這三個(gè)缺口是并列關(guān)系不是遞進(jìn)關(guān)系。這一點(diǎn)很關(guān)鍵——很多人在做方案時(shí)會(huì)誤以為我加了加密就安全了但實(shí)際上只做加密不做身份驗(yàn)證等于把保險(xiǎn)箱交給一個(gè)沒(méi)核對(duì)身份證的快遞員他拿著鑰匙一樣能開(kāi)。1.3 只加密不驗(yàn)身份等于把保險(xiǎn)箱交給陌生人舉個(gè)具體例子說(shuō)明這個(gè)誤區(qū)。假設(shè)客戶(hù)端和服務(wù)器之間用了一個(gè)雙方共享的對(duì)稱(chēng)密鑰做加密鏈路中間人不知道密鑰看起來(lái)安全了。但問(wèn)題是這個(gè)密鑰怎么送到對(duì)方手上如果明文送中間人截獲即可如果提前內(nèi)置在客戶(hù)端里那所有客戶(hù)端共用一把鑰匙泄露一次全盤(pán)皆輸如果每次協(xié)商那協(xié)商過(guò)程本身又需要保護(hù)——這是一個(gè)循環(huán)依賴(lài)。非對(duì)稱(chēng)加密打破了這個(gè)循環(huán)但它帶來(lái)新問(wèn)題客戶(hù)端拿到的公鑰怎么確認(rèn)是服務(wù)器的而不是中間人偽造的答案就是證書(shū)體系。證書(shū)解決的不是怎么加密而是我手上這把公鑰屬于誰(shuí)。所以完整的HTTPS 加密解決機(jī)密性 消息認(rèn)證碼解決完整性 證書(shū)驗(yàn)證解決身份真實(shí)性三者缺一不可。只做了前兩個(gè)而跳過(guò)第三個(gè)的方案在真實(shí)攻擊面前基本等于沒(méi)做。2. 一次HTTPS連接里TLS握手究竟在做什么2.1 協(xié)商階段ClientHello與ServerHello談了什么TLS握手的第一步是雙方對(duì)暗號(hào)確認(rèn)彼此支持哪些能力??蛻?hù)端發(fā)出ClientHello里面裝了幾樣?xùn)|西支持的TLS版本列表、密碼套件列表Cipher Suites、一個(gè)客戶(hù)端隨機(jī)數(shù)Client Random、SNI擴(kuò)展告訴服務(wù)器我要訪問(wèn)哪個(gè)域名、支持的橢圓曲線和簽名算法TLS 1.3里還會(huì)直接帶上密鑰交換的公開(kāi)參數(shù)key_share。服務(wù)器回ServerHello從客戶(hù)端給的列表里挑一套雙方都支持的組合回一個(gè)服務(wù)器隨機(jī)數(shù)Server Random并確認(rèn)最終使用的TLS版本和密碼套件。這里有個(gè)容易被忽略的點(diǎn)密碼套件是服務(wù)器選的但候選范圍是客戶(hù)端給的。如果客戶(hù)端只提供了老舊套件服務(wù)器再想用強(qiáng)套件也沒(méi)得選。這也是為什么老客戶(hù)端會(huì)拖累整體安全基線。SNI這個(gè)擴(kuò)展值得單獨(dú)說(shuō)。一臺(tái)服務(wù)器上跑幾十個(gè)站點(diǎn)共用同一個(gè)IP時(shí)服務(wù)器需要在握手階段就知道客戶(hù)端要訪問(wèn)哪個(gè)域名才能選對(duì)應(yīng)的證書(shū)。SNI就是在ClientHello里明文傳的域名——注意握手階段它還沒(méi)加密所以在一些對(duì)隱私要求高的場(chǎng)景里會(huì)引入加密的ClientHello方案但那是后話主流部署里SNI依然是明文。2.2 驗(yàn)證階段瀏覽器拿到證書(shū)后做的五件事服務(wù)器把證書(shū)鏈發(fā)過(guò)來(lái)之后客戶(hù)端要做一系列檢查任何一項(xiàng)不通過(guò)都會(huì)亮紅燈。這套檢查邏輯值得逐條記住因?yàn)榫€上排障時(shí)90%的證書(shū)問(wèn)題都出在這五步里。第一鏈?zhǔn)津?yàn)證。從服務(wù)器發(fā)來(lái)的站點(diǎn)證書(shū)開(kāi)始一級(jí)一級(jí)往上找簽發(fā)者直到找到一個(gè)客戶(hù)端本地信任庫(kù)里存在的根證書(shū)。鏈條斷在任何一環(huán)驗(yàn)證就失敗。第二簽名驗(yàn)證。每一級(jí)證書(shū)的簽名都要用上一級(jí)的公鑰去驗(yàn)。這一步保證了證書(shū)內(nèi)容沒(méi)有被篡改過(guò)。第三有效期檢查。當(dāng)前時(shí)間必須落在證書(shū)的notBefore和notAfter之間。這里有個(gè)隱藏坑客戶(hù)端系統(tǒng)時(shí)間不準(zhǔn)會(huì)導(dǎo)致本來(lái)有效的證書(shū)被判過(guò)期報(bào)錯(cuò)信息還非常具有誤導(dǎo)性。第四域名匹配。檢查證書(shū)的SANSubject Alternative Name擴(kuò)展里是否包含客戶(hù)端請(qǐng)求的域名。注意現(xiàn)代瀏覽器已經(jīng)完全忽略CN字段只看SAN。通配符證書(shū)*.example.com只匹配一級(jí)子域a.b.example.com是匹配不上的。第五用途與吊銷(xiāo)狀態(tài)。證書(shū)的擴(kuò)展密鑰用法EKU里必須包含serverAuth吊銷(xiāo)狀態(tài)通過(guò)CRL或OCSP查詢(xún)OCSP Stapling就是把查詢(xún)結(jié)果由服務(wù)器緩存后隨握手一起下發(fā)省掉客戶(hù)端的一次額外請(qǐng)求。2.3 密鑰交換為什么最后一定要落到對(duì)稱(chēng)加密非對(duì)稱(chēng)加密RSA、ECDSA的運(yùn)算成本比對(duì)稱(chēng)加密高出幾個(gè)數(shù)量級(jí)拿它來(lái)加密業(yè)務(wù)數(shù)據(jù)是不現(xiàn)實(shí)的。所以TLS的做法是用非對(duì)稱(chēng)手段安全地協(xié)商出一份對(duì)稱(chēng)密鑰然后整個(gè)會(huì)話的數(shù)據(jù)都用這份對(duì)稱(chēng)密鑰加密。以ECDHE為例雙方各自生成一對(duì)臨時(shí)密鑰交換公開(kāi)部分各自用自己的私鑰和對(duì)方的公鑰算出同一個(gè)共享秘密再經(jīng)過(guò)HKDF之類(lèi)的密鑰派生函數(shù)從這個(gè)共享秘密里推導(dǎo)出會(huì)話用的對(duì)稱(chēng)密鑰、初始向量等參數(shù)。這個(gè)過(guò)程的妙處在于臨時(shí)私鑰用完即棄即使服務(wù)器的長(zhǎng)期私鑰日后泄露攻擊者也無(wú)法解密過(guò)去記錄的流量——這就是前向保密Forward Secrecy。TLS 1.3把這一點(diǎn)做到了極致它直接砍掉了不支持前向保密的RSA密鑰交換只保留(EC)DHE和PSK兩類(lèi)模式。所以如果你還在用TLS 1.2并配置了RSA密鑰交換套件實(shí)際上是在給自己留一個(gè)歷史流量被解密的隱患。2.4 TLS 1.2與1.3的握手差距在哪兩者的差距不只是版本號(hào)加一握手輪次直接少了一半。TLS 1.2的完整握手需要2個(gè)RTT客戶(hù)端發(fā)ClientHello第1次往返開(kāi)始服務(wù)器回ServerHelloCertificateServerKeyExchangeServerHelloDone第1次往返結(jié)束客戶(hù)端發(fā)ClientKeyExchangeChangeCipherSpecFinished第2次往返開(kāi)始服務(wù)器回ChangeCipherSpecFinished第2次往返結(jié)束。這之后才能開(kāi)始傳應(yīng)用數(shù)據(jù)。TLS 1.3把密鑰交換的參數(shù)提前塞進(jìn)ClientHello的key_share擴(kuò)展服務(wù)器收到后立刻就能算出共享秘密于是只需要1個(gè)RTTClientHello發(fā)出服務(wù)器回ServerHello此時(shí)已經(jīng)加密Finished握手完成應(yīng)用數(shù)據(jù)緊跟著就能發(fā)。更激進(jìn)的是0-RTT模式客戶(hù)端用之前會(huì)話留下的PSK在第一個(gè)包里就帶上應(yīng)用數(shù)據(jù)——但0-RTT有重放風(fēng)險(xiǎn)只適合冪等請(qǐng)求絕對(duì)不能拿它傳轉(zhuǎn)賬下單這類(lèi)操作。對(duì)比項(xiàng)TLS 1.2TLS 1.3完整握手RTT21會(huì)話恢復(fù)Session ID / Session TicketPSK支持0-RTT密鑰交換RSA 或 (EC)DHE僅(EC)DHE / PSK對(duì)稱(chēng)算法含CBC模式僅AEADGCM/ChaCha20-Poly1305/CCM握手明文部分證書(shū)明文傳輸除ClientHello外基本加密這張表里證書(shū)明文傳輸這一行值得注意TLS 1.2下服務(wù)器證書(shū)在握手中是明文下發(fā)的旁觀者能看到你訪問(wèn)的是哪個(gè)站點(diǎn)雖然看不到內(nèi)容TLS 1.3把ServerHello之后的內(nèi)容全部加密隱私性更好。3. 證書(shū)信任鏈那把小鎖背后的層級(jí)關(guān)系3.1 根證書(shū)、中間證書(shū)與信任錨證書(shū)體系是一棵樹(shù)。最頂端是根證書(shū)它自簽名預(yù)置在操作系統(tǒng)和瀏覽器的信任庫(kù)里。根證書(shū)的私鑰被極度嚴(yán)密地保管幾乎不直接簽發(fā)站點(diǎn)證書(shū)。中間層是中間證書(shū)由根證書(shū)簽發(fā)站點(diǎn)證書(shū)則由中間證書(shū)簽發(fā)。這樣一個(gè)三級(jí)結(jié)構(gòu)根 → 中間 → 站點(diǎn)是當(dāng)前的主流。為什么要多一層中間證書(shū)因?yàn)楦C書(shū)一旦泄露吊銷(xiāo)成本極高——它內(nèi)置在幾十億臺(tái)設(shè)備里沒(méi)法快速更新。引入中間層之后根私鑰可以離線保存日常簽發(fā)全部由中間證書(shū)完成出問(wèn)題只需要吊銷(xiāo)中間證書(shū)影響面可控。這個(gè)設(shè)計(jì)思路和很多系統(tǒng)的權(quán)限分層是一個(gè)道理高頻操作使用低權(quán)限憑證高權(quán)限憑證盡量少用??蛻?hù)端驗(yàn)證時(shí)需要拿到站點(diǎn)證書(shū) 中間證書(shū)一路追到本地信任庫(kù)里的根證書(shū)。注意服務(wù)器一般不需要下發(fā)根證書(shū)因?yàn)榭蛻?hù)端本來(lái)就有下發(fā)根證書(shū)反而會(huì)增加握手包體積。3.2 DV、OV、EV與通配符證書(shū)怎么選按驗(yàn)證強(qiáng)度分證書(shū)大致三類(lèi)。DV域名驗(yàn)證只驗(yàn)證你對(duì)域名的控制權(quán)通常通過(guò)DNS記錄或文件校驗(yàn)完成簽發(fā)快、成本低個(gè)人站和小型項(xiàng)目基本都選這個(gè)。OV組織驗(yàn)證額外核驗(yàn)申請(qǐng)主體的真實(shí)身份信息簽發(fā)需要幾個(gè)工作日適合對(duì)外提供服務(wù)的商業(yè)站點(diǎn)。EV擴(kuò)展驗(yàn)證曾經(jīng)會(huì)在瀏覽器地址欄顯示特殊標(biāo)識(shí)現(xiàn)在主流瀏覽器已經(jīng)弱化這個(gè)展示實(shí)際收益不如從前。按覆蓋范圍分單域名證書(shū)只管一個(gè)域名通配符證書(shū)*.example.com覆蓋同一級(jí)的所有子域但不覆蓋裸域example.com本身也不覆蓋多級(jí)子域多域名證書(shū)SAN證書(shū)可以在一張證書(shū)里塞入多個(gè)不同域名適合域名比較分散的場(chǎng)景。選型時(shí)我的判斷順序是先看域名結(jié)構(gòu)再看驗(yàn)證等級(jí)。如果子域很多且都是同一級(jí)api、cdn、img、static……通配符最省事續(xù)期也只需要管一張如果域名橫跨多個(gè)主域多域名證書(shū)更合適如果只是單個(gè)小站DV單域名就夠了沒(méi)必要為OV的核驗(yàn)流程多等幾天。有一個(gè)細(xì)節(jié)容易被忽略通配符證書(shū)一旦私鑰泄露所有子域同時(shí)受影響所以密鑰管理要更嚴(yán)格。3.3 證書(shū)鏈配錯(cuò)是最容易踩的坑我見(jiàn)過(guò)最多的HTTPS故障不是證書(shū)本身有問(wèn)題而是服務(wù)器只下發(fā)了站點(diǎn)證書(shū)沒(méi)下發(fā)中間證書(shū)?,F(xiàn)象很典型用桌面瀏覽器訪問(wèn)一切正常但用某些移動(dòng)端App、某些命令行工具、某些嵌入式設(shè)備訪問(wèn)就報(bào)證書(shū)錯(cuò)誤。原因在于桌面瀏覽器有證書(shū)緩存和AI A中間證書(shū)自動(dòng)補(bǔ)全機(jī)制它會(huì)自己去補(bǔ)缺失的中間證書(shū)而那些沒(méi)有這個(gè)能力的客戶(hù)端就只能看到一條斷掉的鏈。排查方法很直接用openssl看服務(wù)器實(shí)際下發(fā)的證書(shū)數(shù)量openssl s_client -connect example.com:443 -servername example.com -showcerts如果輸出里只有一段-----BEGIN CERTIFICATE-----那基本可以確認(rèn)缺中間證書(shū)。正確做法是把站點(diǎn)證書(shū)和中間證書(shū)按順序拼成一個(gè)fullchain文件順序是站點(diǎn)證書(shū)在前中間證書(shū)在后順序顛倒同樣會(huì)失敗。cat server.crt intermediate.crt fullchain.crt注意拼接時(shí)不要包含根證書(shū)也不要用中間證書(shū)在前的順序。這個(gè)細(xì)節(jié)在很多教程里被寫(xiě)錯(cuò)實(shí)測(cè)會(huì)導(dǎo)致部分客戶(hù)端驗(yàn)證失敗。4. HTTPS與HTTP的差異不止多了一把鎖4.1 連接語(yǔ)義與報(bào)文封裝的差異最直觀的差異是端口和封裝。HTTP默認(rèn)80端口報(bào)文直接跑在TCP之上HTTPS默認(rèn)443端口HTTP報(bào)文被整體塞進(jìn)TLS記錄層再由TLS跑在TCP上。所以在抓包工具里看HTTPS流量只能看到TLS握手和應(yīng)用數(shù)據(jù)的密文看不到HTTP請(qǐng)求行。這帶來(lái)一系列工程上的連鎖反應(yīng)。比如中間設(shè)備不再能解析內(nèi)容企業(yè)的上網(wǎng)行為管理、運(yùn)營(yíng)商的流量整形、某些對(duì)HTTP頭做改寫(xiě)的代理在HTTPS面前統(tǒng)統(tǒng)失效。這既是好事隱私得到保護(hù)也是麻煩運(yùn)維可觀測(cè)性下降。所以生產(chǎn)環(huán)境通常會(huì)引入TLS終止層——在負(fù)載均衡或反向代理上解密內(nèi)部再用明文或重新加密轉(zhuǎn)發(fā)把解密點(diǎn)收斂到可控的少數(shù)幾個(gè)位置。另一個(gè)差異在連接復(fù)用上。HTTP/1.1的keep-alive讓一個(gè)TCP連接承載多個(gè)請(qǐng)求但TLS連接建立本身有額外開(kāi)銷(xiāo)所以連接池的參數(shù)最大連接數(shù)、空閑超時(shí)在HTTPS場(chǎng)景下需要重新調(diào)。一個(gè)常見(jiàn)的誤配是連接池開(kāi)得太大導(dǎo)致握手次數(shù)暴漲反而比復(fù)用少量長(zhǎng)連接更慢。4.2 緩存、代理與重定向的行為差異HTTP的中間緩存可以放心地緩存響應(yīng)因?yàn)閮?nèi)容對(duì)所有人一樣。HTTPS下共享緩存CDN、反向代理要緩存內(nèi)容就必須在某一層解密緩存的是解密后的內(nèi)容再對(duì)每個(gè)客戶(hù)端單獨(dú)重新加密。這決定了緩存層的位置和架構(gòu)。更細(xì)的差異體現(xiàn)在降級(jí)場(chǎng)景。當(dāng)頁(yè)面是HTTPS而其中某個(gè)資源是HTTP時(shí)瀏覽器會(huì)判定為混合內(nèi)容。被動(dòng)混合內(nèi)容圖片、音視頻在部分瀏覽器里會(huì)被自動(dòng)升級(jí)為HTTPS嘗試失敗則阻止主動(dòng)混合內(nèi)容腳本、樣式表、iframe、XHR則直接被阻止加載。這是HTTPS站點(diǎn)最常見(jiàn)的頁(yè)面莫名缺東西的原因。Referer的行為也不同。從HTTPS頁(yè)面跳到HTTP頁(yè)面時(shí)默認(rèn)策略下瀏覽器不會(huì)發(fā)送Referer頭這是為了防止把HTTPS頁(yè)面的路徑信息泄露到明文鏈路。如果你的業(yè)務(wù)依賴(lài)Referer做來(lái)源統(tǒng)計(jì)或防盜鏈跨協(xié)議跳轉(zhuǎn)時(shí)數(shù)據(jù)會(huì)丟失這個(gè)坑在遷移過(guò)程中經(jīng)常被忽略。Cookie的Secure屬性只在HTTPS下才會(huì)被發(fā)送SameSite策略的默認(rèn)值在新版瀏覽器里已經(jīng)收緊為L(zhǎng)ax。所以把站點(diǎn)從HTTP遷到HTTPS時(shí)Cookie相關(guān)的行為變化需要單獨(dú)回歸測(cè)試一遍。4.3 現(xiàn)代協(xié)議能力只在HTTPS上開(kāi)放這是很多人沒(méi)意識(shí)到的差異一批現(xiàn)代Web能力直接把HTTPS設(shè)為準(zhǔn)入門(mén)檻。HTTP/2在瀏覽器里只對(duì)TLS連接啟用通過(guò)ALPN擴(kuò)展在握手階段協(xié)商出h2。雖然協(xié)議規(guī)范里存在明文版本的h2c但主流瀏覽器根本不支持。這意味著不上HTTPS你就用不上多路復(fù)用、頭部壓縮這些HTTP/2帶來(lái)的性能紅利。HTTP/3更徹底它跑在QUIC之上UDP 443端口而QUIC內(nèi)置了TLS 1.3加密不是可選項(xiàng)而是協(xié)議的一部分。再往前看Service Worker、地理位置、攝像頭麥克風(fēng)訪問(wèn)、剪貼板寫(xiě)入、Web Push、部分PWA能力全部要求在安全上下文Secure Context中運(yùn)行。所謂安全上下文判定標(biāo)準(zhǔn)就是頁(yè)面是否通過(guò)HTTPS加載localhost除外。所以HTTPS在今天已經(jīng)不是一個(gè)安全加固項(xiàng)而是功能準(zhǔn)入項(xiàng)。維度HTTPHTTPS默認(rèn)端口80443報(bào)文可見(jiàn)性明文可讀加密完整性校驗(yàn)無(wú)AEAD/MAC服務(wù)端身份驗(yàn)證無(wú)證書(shū)鏈驗(yàn)證HTTP/2支持瀏覽器不支持ALPN協(xié)商啟用HTTP/3支持不支持QUIC內(nèi)置TLS 1.3安全上下文否是跨協(xié)議Referer正常發(fā)送降級(jí)時(shí)不發(fā)送中間緩存可直接緩存需在解密層緩存5. TLS的成本賬與優(yōu)化手段5.1 握手延遲與CPU開(kāi)銷(xiāo)的真實(shí)量級(jí)經(jīng)常聽(tīng)到HTTPS會(huì)變慢這個(gè)說(shuō)法需要拆開(kāi)看。延遲方面TLS 1.3完整握手增加1個(gè)RTT如果客戶(hù)端和服務(wù)器之間RTT是30ms那就是多30ms。相比之下DNS解析、TCP三次握手本身也是1個(gè)RTT所以首次連接的額外開(kāi)銷(xiāo)大約是整體連接建立的30%到50%。但連接復(fù)用之后這個(gè)開(kāi)銷(xiāo)就攤薄了。CPU方面現(xiàn)代CPU普遍帶有AES-NI指令集對(duì)稱(chēng)加密的吞吐可以做到幾個(gè)GB每秒業(yè)務(wù)代碼自己產(chǎn)生的開(kāi)銷(xiāo)往往比加解密大得多。真正貴的是非對(duì)稱(chēng)運(yùn)算一次ECDSA簽名驗(yàn)證在毫秒量級(jí)如果用RSA 2048簽名也差不多。所以一個(gè)每秒處理上萬(wàn)次新建連接的網(wǎng)關(guān)才需要認(rèn)真考慮握手CPU成本普通業(yè)務(wù)站點(diǎn)的瓶頸基本不在TLS上。我的經(jīng)驗(yàn)是首屏慢不一定是TLS的鍋但握手次數(shù)多一定是架構(gòu)問(wèn)題。如果你發(fā)現(xiàn)QPS不高但CPU吃緊先去看是不是每次請(qǐng)求都新建了TLS連接而不是先去調(diào)密碼套件。5.2 會(huì)話復(fù)用、OCSP Stapling與TLS 1.3優(yōu)化手段有幾個(gè)層次成本從低到高。會(huì)話復(fù)用是第一優(yōu)先。TLS 1.2下靠Session ID或Session Ticket讓客戶(hù)端在后續(xù)連接中跳過(guò)完整的密鑰交換TLS 1.3下統(tǒng)一用PSK。配置上主要是開(kāi)啟會(huì)話緩存并設(shè)置合理的超時(shí)ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;這里10m大約能存4萬(wàn)個(gè)會(huì)話1MB約存4000個(gè)中小站點(diǎn)足夠用了。ssl_session_tickets off是個(gè)安全權(quán)衡會(huì)話票據(jù)用服務(wù)器密鑰加密后交給客戶(hù)端保存如果密鑰長(zhǎng)期不輪換歷史流量有被解密的風(fēng)險(xiǎn)。關(guān)掉票據(jù)、只用服務(wù)端緩存安全性更好代價(jià)是集群環(huán)境下需要共享緩存。OCSP Stapling解決的是證書(shū)吊銷(xiāo)狀態(tài)查詢(xún)的延遲問(wèn)題。不開(kāi)的話客戶(hù)端可能要多發(fā)一次請(qǐng)求去CA查詢(xún)鏈路差的時(shí)候這一下能拖幾百毫秒。開(kāi)啟后由服務(wù)器定期去查詢(xún)并緩存結(jié)果握手時(shí)直接下發(fā)給客戶(hù)端ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/chain.pem; resolver 8.8.8.8 valid300s;注意resolver必須配置否則nginx無(wú)法解析OCSP服務(wù)器地址stapling會(huì)靜默失敗。TLS 1.3是收益最大的一步直接把握手砍到一個(gè)RTT同時(shí)砍掉了老舊的算法。代價(jià)是要確認(rèn)客戶(hù)端的兼容性——目前主流瀏覽器和現(xiàn)代操作系統(tǒng)都支持但如果你的用戶(hù)里有大量老舊Android設(shè)備或嵌入式終端可能需要同時(shí)保留TLS 1.2。5.3 證書(shū)與協(xié)議參數(shù)的取舍參數(shù)配置上有個(gè)常見(jiàn)的過(guò)度優(yōu)化傾向把密碼套件列表寫(xiě)得極長(zhǎng)把各種強(qiáng)算法全列上。實(shí)際上列表長(zhǎng)度對(duì)手握延遲的影響很小真正影響兼容性的是最低版本和是否禁用某些降級(jí)套件。我的建議基線是最低TLS 1.2優(yōu)先TLS 1.3禁用所有CBC模式套件有padding oracle類(lèi)攻擊的歷史禁用RSA密鑰交換無(wú)前向保密禁用3DES、RC4、MD5、SHA-1。這樣一套下來(lái)兼容性覆蓋絕大多數(shù)現(xiàn)代客戶(hù)端安全性也站得住。證書(shū)算法選擇上ECDSA P-256的證書(shū)體積更小、握手更快在有條件的情況下優(yōu)先選它但要注意極老的客戶(hù)端可能不支持需要準(zhǔn)備RSA證書(shū)做回退。雙證書(shū)部署ECDSA RSA在nginx里可以通過(guò)兩個(gè)ssl_certificate指令實(shí)現(xiàn)nginx會(huì)自動(dòng)根據(jù)客戶(hù)端能力選擇。6. 從申請(qǐng)證書(shū)到Nginx上線的一套可復(fù)現(xiàn)流程6.1 生成密鑰與CSR第一步是在服務(wù)器上生成私鑰和證書(shū)簽名請(qǐng)求CSR。私鑰必須留在自己手里絕不能在網(wǎng)絡(luò)上傳輸。推薦用ECDSAopenssl ecparam -genkey -name prime256v1 -out privkey.pem openssl req -new -key privkey.pem -out server.csr \ -subj /CCN/STState/LCity/OExample/CNexample.com \ -addext subjectAltNameDNS:example.com,DNS:www.example.com如果目標(biāo)環(huán)境需要RSA換成openssl req -new -newkey rsa:2048 -nodes \ -keyout privkey.pem -out server.csr \ -subj /CNexample.com \ -addext subjectAltNameDNS:example.com,DNS:www.example.com這里有兩個(gè)細(xì)節(jié)。-nodes表示私鑰不加密no DES因?yàn)閚ginx啟動(dòng)時(shí)無(wú)法交互輸入密碼加密的私鑰需要每次手動(dòng)解密生產(chǎn)環(huán)境一般不用。subjectAltName必須顯式寫(xiě)上所有要用到的域名包括裸域和www因?yàn)楝F(xiàn)代客戶(hù)端只看SAN。少寫(xiě)一個(gè)域名上線后就是一次證書(shū)名稱(chēng)不匹配的報(bào)障。把CSR提交給證書(shū)頒發(fā)機(jī)構(gòu)后DV證書(shū)通常幾分鐘內(nèi)就能完成域名驗(yàn)證并下發(fā)。下載下來(lái)一般是一個(gè)壓縮包里面包含站點(diǎn)證書(shū)、中間證書(shū)有的還有根證書(shū)。這時(shí)候按前面說(shuō)的規(guī)則拼fullchaincat server.crt intermediate.crt fullchain.pem6.2 Nginx關(guān)鍵配置逐行解釋下面這份配置是我目前用得比較順手的一套基線每一項(xiàng)都標(biāo)注了理由server { listen 443 ssl; http2 on; # nginx 1.25.1 寫(xiě)法舊版本用 listen 443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; # TLS 1.3時(shí)代交給客戶(hù)端選更合理 ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/chain.pem; resolver 8.8.8.8 1.1.1.1 valid300s; resolver_timeout 5s; add_header Strict-Transport-Security max-age31536000; includeSubDomains always; add_header X-Content-Type-Options nosniff always; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }幾個(gè)必須解釋的點(diǎn)。ssl_prefer_server_ciphers off看著反直覺(jué)但在TLS 1.3下密碼套件已經(jīng)由客戶(hù)端和服務(wù)端的交集決定強(qiáng)行讓服務(wù)端優(yōu)先沒(méi)有實(shí)際收益反而可能選到客戶(hù)端不太擅長(zhǎng)的算法。X-Forwarded-Proto $scheme這一行非常關(guān)鍵——后端應(yīng)用需要知道原始請(qǐng)求是HTTP還是HTTPS否則生成的重定向地址、靜態(tài)資源鏈接可能還是http開(kāi)頭直接引發(fā)混合內(nèi)容。HTTP到HTTPS的跳轉(zhuǎn)單獨(dú)寫(xiě)一個(gè)server塊不要用ifserver { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }return 301比rewrite效率高語(yǔ)義也更明確。注意這里用$host而不是寫(xiě)死域名避免多域名場(chǎng)景下跳錯(cuò)。HSTS頭要謹(jǐn)慎開(kāi)啟。max-age31536000意味著一整年內(nèi)瀏覽器都會(huì)強(qiáng)制用HTTPS訪問(wèn)該域名一旦證書(shū)出問(wèn)題用戶(hù)連繼續(xù)訪問(wèn)的按鈕都點(diǎn)不了。我的做法是先用一個(gè)很短的max-age比如300秒跑一周確認(rèn)沒(méi)有問(wèn)題再逐步加長(zhǎng)。6.3 上線后的驗(yàn)證清單配完不是重啟就完事至少過(guò)一遍這幾項(xiàng)。檢查協(xié)議版本openssl s_client -connect example.com:443 -servername example.com -tls1_3 /dev/null 21 | grep Protocol確認(rèn)TLS 1.3可用同時(shí)試著用-tls1連接應(yīng)該失敗。檢查證書(shū)鏈完整性openssl s_client -connect example.com:443 -servername example.com -showcerts 2/dev/null | grep -c BEGIN CERTIFICATE正常應(yīng)該輸出2站點(diǎn)證書(shū) 中間證書(shū)。檢查證書(shū)到期時(shí)間echo | openssl s_client -connect example.com:443 -servername example.com 2/dev/null | openssl x509 -noout -dates檢查HTTP/2是否生效curl -I --http2 https://example.com響應(yīng)行里應(yīng)該出現(xiàn)HTTP/2。檢查混合內(nèi)容打開(kāi)瀏覽器開(kāi)發(fā)者工具切到Console和Network面板把所有頁(yè)面走一遍看有沒(méi)有被阻止的資源。這一步最容易被跳過(guò)但問(wèn)題往往在這里。7. 線上排障幾個(gè)高頻故障的定位鏈路7.1 部分客戶(hù)端報(bào)證書(shū)錯(cuò)誤部分正常這個(gè)現(xiàn)象最典型的根因就是證書(shū)鏈不完整。診斷路徑是先用openssl s_client -showcerts看服務(wù)器下發(fā)了幾張證書(shū)如果只有一張基本可以確定。修復(fù)方法是補(bǔ)上中間證書(shū)并重載nginx。還有一種變體鏈完整但中間證書(shū)順序錯(cuò)了。有些CA下發(fā)的壓縮包里同時(shí)包含交叉簽名證書(shū)和普通中間證書(shū)如果拼錯(cuò)順序部分客戶(hù)端會(huì)驗(yàn)證失敗。判斷方法是把服務(wù)器下發(fā)的證書(shū)逐個(gè)拆分出來(lái)用openssl verify逐個(gè)驗(yàn)證路徑。第三個(gè)可能是SNI相關(guān)。極老的客戶(hù)端某些老舊Android版本、部分Java 6/7環(huán)境握手時(shí)不發(fā)SNI服務(wù)器只能返回默認(rèn)證書(shū)如果默認(rèn)證書(shū)不覆蓋該域名就會(huì)報(bào)名稱(chēng)不匹配。這種情況下要么升級(jí)客戶(hù)端要么讓默認(rèn)證書(shū)覆蓋主域名。第四個(gè)可能是客戶(hù)端時(shí)間不對(duì)。嵌入式設(shè)備的系統(tǒng)時(shí)間停在幾年前驗(yàn)證有效期時(shí)直接失敗。這個(gè)從服務(wù)端日志里能看出端倪請(qǐng)求集中在某幾類(lèi)User-Agent上且這些設(shè)備的TLS握手版本往往也比較老。7.2 混合內(nèi)容與HSTS導(dǎo)致的打不開(kāi)混合內(nèi)容的排查思路是先看Console里報(bào)的阻塞類(lèi)型。主動(dòng)混合內(nèi)容腳本、樣式、iframe是被硬阻止的Console會(huì)明確報(bào)Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure ...。被動(dòng)混合內(nèi)容圖片通常是警告部分瀏覽器會(huì)自動(dòng)升級(jí)。修復(fù)方式有三種按優(yōu)先級(jí)排把資源地址改成相對(duì)路徑或協(xié)議相對(duì)路徑現(xiàn)在更推薦直接用https絕對(duì)路徑在頁(yè)面里加meta http-equivContent-Security-Policy contentupgrade-insecure-requests讓瀏覽器自動(dòng)升級(jí)實(shí)在改不動(dòng)的第三方資源就在服務(wù)端做一層代理轉(zhuǎn)發(fā)。HSTS導(dǎo)致的問(wèn)題更隱蔽。一旦瀏覽器記錄了這個(gè)域名的HSTS策略之后所有HTTP請(qǐng)求在瀏覽器內(nèi)部就被改寫(xiě)成HTTPS用戶(hù)完全無(wú)感但如果證書(shū)失效或服務(wù)掛掉用戶(hù)會(huì)看到無(wú)法跳過(guò)的警告頁(yè)。排查時(shí)注意HSTS記錄在瀏覽器本地清緩存、換瀏覽器、用無(wú)痕模式都可以用來(lái)確認(rèn)是不是HSTS導(dǎo)致的。運(yùn)維層面如果確實(shí)需要緊急降級(jí)只能等max-age過(guò)期或者讓用戶(hù)手動(dòng)清理瀏覽器HSTS設(shè)置——這就是為什么前面強(qiáng)調(diào)max-age要循序漸進(jìn)。7.3 證書(shū)過(guò)期與時(shí)間不同步證書(shū)過(guò)期是最不該發(fā)生但又最常發(fā)生的一類(lèi)故障。根本原因是缺乏自動(dòng)化的到期監(jiān)控。人工記日歷遲早會(huì)漏尤其當(dāng)證書(shū)數(shù)量上來(lái)之后。可行的做法是在監(jiān)控系統(tǒng)里加一個(gè)探針每天檢查證書(shū)剩余天數(shù)低于30天告警低于7天升級(jí)告警級(jí)別。檢查邏輯很簡(jiǎn)單就是前面那條openssl命令加日期解析。時(shí)間不同步的問(wèn)題在容器環(huán)境里更常見(jiàn)。容器啟動(dòng)時(shí)如果宿主機(jī)時(shí)間不準(zhǔn)或者NTP服務(wù)沒(méi)配好證書(shū)驗(yàn)證會(huì)莫名其妙失敗。這類(lèi)問(wèn)題的排查要養(yǎng)成一個(gè)習(xí)慣看到證書(shū)報(bào)錯(cuò)第一反應(yīng)先確認(rèn)時(shí)間和時(shí)區(qū)。一條date -u就能排除掉一大類(lèi)問(wèn)題。還有一個(gè)容易踩的坑是自動(dòng)續(xù)期任務(wù)靜默失敗。續(xù)期腳本跑在cron里失敗了沒(méi)人知道等到證書(shū)真的過(guò)期才炸。所以續(xù)期任務(wù)必須配置失敗告警并且在續(xù)期成功后自動(dòng)reload服務(wù)。reload這一步經(jīng)常被漏掉——證書(shū)文件更新了但nginx還在用內(nèi)存里的舊證書(shū)表面上文件是新的實(shí)際服務(wù)下發(fā)的還是舊的。提示續(xù)期成功后務(wù)必執(zhí)行nginx -t nginx -s reload??梢韵扔胣ginx -t檢查配置通過(guò)了再reload避免因?yàn)榕渲脝?wèn)題導(dǎo)致服務(wù)中斷。我個(gè)人在這塊踩過(guò)的坑是早年覺(jué)得證書(shū)配好就完事結(jié)果第一次到期是在一個(gè)周五晚上站點(diǎn)直接不可訪問(wèn)用戶(hù)投訴才發(fā)現(xiàn)。從那之后我的做法是任何證書(shū)上線時(shí)同步做三件事——加監(jiān)控、配續(xù)期、續(xù)期后自動(dòng)reload三件缺一件都算沒(méi)做完。另外一個(gè)小技巧是把證書(shū)的到期時(shí)間寫(xiě)進(jìn)巡檢腳本的輸出里每周看一眼比任何告警都讓人踏實(shí)。TLS這套東西真正難的不是理解握手流程而是把證書(shū)的生命周期管理做成一個(gè)不需要人盯著的閉環(huán)這一點(diǎn)做過(guò)幾年運(yùn)維的人應(yīng)該都有體會(huì)。