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