如何設定 HSTS 而不把自己鎖在門外
一套分階段、可回復的 Strict-Transport-Security 推出計畫,在提升隱私的同時,避免把一次憑證失誤變成服務中斷。
目錄
- HSTS 很簡單,直到它不再簡單
- HSTS 標頭實際上做了什麼
- 要避免的鎖定情境
- 1. 被遺忘的子網域尚未準備好 HTTPS
- 2. 憑證過期
- 3. 測試環境或內部工具放在正式網域底下
- 4. 把 preload 當成例行核取方塊
- 安全的推出計畫
- 步驟 1:稽核你控制的每一個主機名稱
- 步驟 2:先修好 HTTPS,再加入 HSTS
- 步驟 3:從非常短的 max-age 開始
- 步驟 4:逐步增加
- 步驟 5:只有在稽核確實完成後才加入 includeSubDomains
- 步驟 6:把 preload 視為獨立專案
- 設定範例
- Nginx
- Apache
- CDN 或邊緣平台
- 如何安全地撤銷 HSTS
- 發布前的測試檢查清單
- HSTS 的隱私理由
HSTS 很簡單,直到它不再簡單
HTTP Strict Transport Security,通常簡稱為 HSTS,會告訴瀏覽器:「對這個網站,一律使用 HTTPS。」一旦瀏覽器透過有效的 HTTPS 連線收到這個標頭,就會在你指定的期間內記住這條規則。
這很有用。它可以防止通訊協定降級攻擊,減少意外的不安全請求,也避免使用者輸入 example.com 時,在被重新導向前短暫碰到純 HTTP 的尷尬時刻。
它也很黏著。如果你發布了錯誤的 HSTS 政策,即使你已經從伺服器移除該標頭,瀏覽器仍可能在很久之後繼續強制執行。這就是團隊把自己鎖在門外的方式:不一定是被自己的管理後台鎖住,而是被使用者的瀏覽器、子網域、測試環境、舊端點,以及那些尚未準備好強制使用 HTTPS 的被遺忘服務鎖住。
目標不是避免使用 HSTS。目標是像遷移一樣部署它,而不是像切換開關一樣啟用它。
HSTS 標頭實際上做了什麼
典型的 HSTS 標頭長這樣:
Strict-Transport-Security: max-age=31536000; includeSubDomains
它有三個重要部分:
max-age:瀏覽器應對這個主機強制使用 HTTPS 的時間長度,以秒為單位。includeSubDomains:這條規則是否也套用到每一個子網域。preload:表示你希望該網域被納入瀏覽器 preload 清單的訊號。
瀏覽器只會在透過有效 HTTPS 收到這個標頭時才信任它。如果憑證無效、過期或不相符,瀏覽器不應接受該回應中的新 HSTS 政策。
一旦政策被儲存,之後嘗試造訪 http://example.com 時,瀏覽器會在送出請求前先將它升級為 https://example.com。這就是隱私上的收益:不安全的請求永遠不會離開裝置。
要避免的鎖定情境
多數 HSTS 失敗並不是由主網站造成的。它們發生在邊緣地帶。
1. 被遺忘的子網域尚未準備好 HTTPS
includeSubDomains 聽起來很整潔,但它是絕對的。如果你在 example.com 上設定它,它會套用到:
www.example.comapi.example.comold-crm.example.comprinter-setup.example.comstaging.example.com- 該網域底下的任何其他項目
如果其中任何主機無法提供有效 HTTPS,已快取 HSTS 政策的使用者就無法透過 HTTP 連到它們。
2. 憑證過期
沒有 HSTS 時,使用者有時會點過憑證警告。這不是好的安全做法,但確實會發生。
有了 HSTS,現代瀏覽器不會讓使用者輕易略過該主機的憑證錯誤。這正是它的目的。這也代表憑證續期需要變得平淡、受監控,且經過測試。
3. 測試環境或內部工具放在正式網域底下
一旦父網域使用 includeSubDomains,把內部工具放在 *.example.com 底下就可能變得痛苦。如果這些工具使用自簽憑證、私有憑證授權單位、老舊 TLS 設定,或根本沒有 HTTPS,HSTS 會把這個捷徑暴露出來。
這也是許多團隊將內部與實驗性系統放在另一個有自身安全政策的網域底下的原因之一。
4. 把 preload 當成例行核取方塊
HSTS preload 不只是另一個指令。它代表你的網域可以在任何使用者造訪你的網站之前,就被內建到瀏覽器中,成為僅限 HTTPS 的網域。
這可以關閉「第一次造訪」的缺口,但也更難復原。從 preload 清單移除可能需要數週或數月才會送達使用者,取決於瀏覽器的發行週期。preload 適合穩定、成熟的網域。不適合仍在盤點子網域資產的網站。
安全的推出計畫
步驟 1:稽核你控制的每一個主機名稱
在設定 includeSubDomains 之前,列出該網域底下的每一個主機名稱。DNS 記錄是一個起點,但不是全部。檢查 CDN 設定、主機代管儀表板、電子郵件相關主機名稱、舊行銷工具、儲存桶,以及內部文件。
針對每個主機名稱,回答:
- 它提供 HTTP、HTTPS,還是兩者都有?
- HTTPS 憑證是否有效並自動續期?
- HTTP 是否乾淨地重新導向到 HTTPS?
- 它是否應該公開?
- 它是否仍然需要?
如果你的團隊已經有正式環境標頭除錯的習慣,這會自然地放在重新導向與標頭檢查旁邊。我們在一套用於正式環境除錯重新導向與 HTTP 標頭的小工具組中介紹過這個流程。
步驟 2:先修好 HTTPS,再加入 HSTS
HSTS 不會讓壞掉的 HTTPS 設定變安全。它只會讓 HTTPS 變成強制。
啟用前,請確認:
- TLS 憑證涵蓋正確的主機名稱。
- 憑證會自動續期。
- HTTP 會重新導向到 HTTPS,並在可能時只經過一次乾淨的跳轉。
- 標準主機重新導向一致,例如非
www到www,或反過來。 - 應用程式資源不依賴不安全的
http://URL。
混合內容已不像過去那麼常見,但仍會出現在舊 CMS 佈景主題、分析程式碼片段、嵌入式媒體,以及硬編碼的圖片路徑中。
步驟 3:從非常短的 max-age 開始
不要一開始就設定一年。從五分鐘開始:
Strict-Transport-Security: max-age=300
只在你正在測試的主機名稱上部署它,通常是標準的正式網站。先不要加入 includeSubDomains。
接著用真實瀏覽器與命令列請求測試:
curl -I https://example.com
你應該只看到一個 Strict-Transport-Security 標頭。來自應用程式伺服器與 CDN 的重複 HSTS 標頭,是常見的混淆來源。瀏覽器通常會套用有效政策,但正在除錯事件的人不需要模稜兩可。
步驟 4:逐步增加
如果沒有任何東西壞掉,就分階段增加期間:
Strict-Transport-Security: max-age=86400
然後:
Strict-Transport-Security: max-age=604800
接著或許:
Strict-Transport-Security: max-age=2592000
實務上的時程可以是:
- 5 分鐘
- 1 天
- 1 週
- 1 個月
- 6 個月或 1 年
搶快沒有獎品。分階段推出的重點,就是讓你的監控、客服信箱與邊界案例有時間告訴你檢查清單漏了什麼。
步驟 5:只有在稽核確實完成後才加入 includeSubDomains
一旦每個公開子網域都已準備好 HTTPS,你可以考慮:
Strict-Transport-Security: max-age=31536000; includeSubDomains
這是應該保守的時刻。如果某個舊服務仍然需要 HTTP,就不要在父網域加入 includeSubDomains。要嘛遷移該服務,要嘛把它移到另一個網域,或接受你的 HSTS 政策目前必須維持較窄的範圍。
安全標頭應該反映現實。它們不應該被當成你希望未來基礎架構能達成的勵志海報。
步驟 6:把 preload 視為獨立專案
只有在以下條件全部成立時,才考慮 preload:
- 該網域與所有子網域都支援有效 HTTPS。
- HTTP 會重新導向到 HTTPS。
- HSTS 標頭使用至少 31536000 秒的
max-age。 - 標頭包含
includeSubDomains。 - 標頭包含
preload。 - 你確信該網域底下任何地方都不再需要純 HTTP。
符合 preload 條件的標頭長這樣:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
提交到 preload 清單是一項長期承諾。如果網站是活動微型網站、臨時產品網域,或所有權邊界不清楚的網域,就跳過它。
設定範例
Nginx
使用 always,讓錯誤回應也送出該標頭:
add_header Strict-Transport-Security "max-age=300" always;
推出穩定後:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache
啟用 mod_headers 後:
Header always set Strict-Transport-Security "max-age=300"
之後:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
CDN 或邊緣平台
如果你的 CDN 會設定回應標頭,最好在單一地方管理 HSTS。除非你有非常明確的理由,否則不要在 origin 設定一套政策、在 edge 又設定另一套。
也要檢查 CDN 是否會將標頭套用到重新導向、快取錯誤與自訂錯誤頁面。正式網站不只包含它的 200 OK 回應。
如何安全地撤銷 HSTS
如果你需要停用 HSTS,請送出:
Strict-Transport-Security: max-age=0
但有個陷阱:瀏覽器必須能透過有效 HTTPS 成功連到網站,才能收到這個標頭。如果 HTTPS 本身壞了,已快取 HSTS 政策的使用者就無法取得清除政策的指示。
因此通常的復原順序是:
- 恢復有效 HTTPS。
- 提供
Strict-Transport-Security: max-age=0。 - 保留足夠長的時間,讓回訪使用者收到它。
- 事件解決後,移除或替換該標頭。
如果該網域已被 preload,提供 max-age=0 對新的瀏覽器設定檔並不足夠。你還需要請求從 preload 清單移除,並等待該變更透過瀏覽器更新發送出去。
發布前的測試檢查清單
在提高 max-age 或加入 includeSubDomains 之前,使用這份檢查清單:
- 標準 HTTPS URL 回傳有效憑證。
- HTTP 會重新導向到 HTTPS。
- 只有一個 HSTS 標頭。
- 在適當情況下,重新導向與錯誤回應也會出現該標頭。
- 所有公開子網域都有有效 HTTPS。
- 憑證續期受到監控。
- 沒有關鍵內部系統依賴同一個父網域底下的 HTTP。
- preload 已被明確討論,而不是出於習慣加入。
Lighthouse 在某些情境下也可能標示缺少或薄弱的安全標頭,但它不應該是你唯一的驗證方法。如果你把它作為更廣泛檢查的一部分,請把結果視為訊號,而不是判決;當你不慌張地閱讀 Lighthouse 報告時,也適用同樣的心態。
<!-- tool-cta:start -->
💡 試試這個: 在每次 HSTS 變更前後,使用 Get Headers 檢查 Strict-Transport-Security response,以確認 max-age、includeSubDomains 和 preload 符合你的預期。
<!-- tool-cta:end -->
HSTS 的隱私理由
HSTS 常被描述為安全標頭,而它確實是。它也有隱私上的好處:它降低使用者第一次請求在不受信任網路上透過純 HTTP 洩漏的機率。
這在機場 Wi-Fi、飯店網路、企業訪客網路,以及任何使用者流量可能被觀察或修改的地方都很重要。純 HTTP 請求可能暴露主機名稱、路徑、沒有 Secure 旗標的 cookies,以及其他請求細節。HTTPS 不是魔法,但一致地強制使用它,可以移除整整一類可避免的洩漏。
最好的 HSTS 部署是無趣的。它們慢慢推出,有可靠憑證支撐,且無聊到沒有人注意。對於一個失敗模式可能很戲劇化的標頭來說,這正是你想要的。