如何设置 HSTS 而不把自己锁在门外
一个分阶段、可回滚的 Strict-Transport-Security 推出计划:提升隐私保护,同时避免一个错误证书演变成服务中断。
目录
- HSTS 很简单,直到它不再简单
- HSTS 响应头实际做了什么
- 需要避免的锁定场景
- 1. 被遗忘的子域名尚未准备好 HTTPS
- 2. 证书过期
- 3. 预发布或内部工具位于生产域名之下
- 4. preload 被当作常规复选框
- 安全的推出计划
- Step 1: 审计你控制的每一个主机名
- Step 2: 先修好 HTTPS,再添加 HSTS
- Step 3: 从非常短的 max-age 开始
- Step 4: 逐步增加
- Step 5: 只有在审计真实完成后才添加 includeSubDomains
- Step 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:表示你希望该域名被加入浏览器预加载列表。
浏览器只有在通过有效 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. 预发布或内部工具位于生产域名之下
把内部工具放在 *.example.com 下,一旦父域名使用 includeSubDomains,就可能变得痛苦。如果这些工具使用自签名证书、私有证书颁发机构、旧 TLS 配置,或者完全不使用 HTTPS,HSTS 会暴露这条捷径。
这也是许多团队将内部和实验性系统放在单独域名下的原因之一,该域名可以拥有自己的安全策略。
4. preload 被当作常规复选框
HSTS preload 不只是另一个指令。它意味着你的域名可以在任何用户访问你的网站之前,就随浏览器一起作为仅 HTTPS 域名发布。
这弥合了“首次访问”缺口,但撤销要困难得多。根据浏览器发布周期,从预加载列表中移除可能需要数周或数月才能触达用户。preload 适合稳定、成熟的域名。它不适合仍在摸清子域名清单的站点。
安全的推出计划
Step 1: 审计你控制的每一个主机名
在设置 includeSubDomains 之前,列出该域名下的每一个主机名。DNS 记录是一个起点,但不是全部。检查 CDN 配置、托管控制台、与电子邮件相关的主机名、旧营销工具、存储桶以及内部文档。
对每个主机名,回答:
- 它提供 HTTP、HTTPS,还是两者都提供?
- HTTPS 证书是否有效并自动续期?
- 它是否能干净地从 HTTP 重定向到 HTTPS?
- 它是否应当公开访问?
- 它是否仍然需要存在?
如果你的团队已经有生产环境响应头调试习惯,这可以自然地纳入重定向和响应头检查流程。我们在一个用于调试生产环境重定向和 HTTP 响应头的小工具箱中介绍过该工作流。
Step 2: 先修好 HTTPS,再添加 HSTS
HSTS 不会让损坏的 HTTPS 配置变得安全。它只会让 HTTPS 成为强制要求。
在启用它之前,请确认:
- TLS 证书覆盖了正确的主机名。
- 证书会自动续期。
- HTTP 尽可能通过一次干净的跳转重定向到 HTTPS。
- 规范主机重定向保持一致,例如非
www到www,或反向。 - 应用资源不依赖不安全的
http://URL。
混合内容已经比过去少见,但它仍会出现在旧 CMS 主题、分析代码片段、嵌入媒体和硬编码图片路径中。
Step 3: 从非常短的 max-age 开始
不要从一年开始。从五分钟开始:
Strict-Transport-Security: max-age=300
只在你正在测试的主机名上部署它,通常是规范的生产网站。暂时不要包含 includeSubDomains。
然后在真实浏览器中以及通过命令行请求进行测试:
curl -I https://example.com
你应该只看到一个 Strict-Transport-Security 响应头。来自应用服务器和 CDN 的重复 HSTS 响应头是常见的混乱来源。浏览器通常会应用有效策略,但在调试事故时,人类不需要额外的歧义。
Step 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 年
抢进度没有奖励。分阶段推出的全部意义,就是给你的监控、支持收件箱和边缘案例足够时间,告诉你检查清单遗漏了什么。
Step 5: 只有在审计真实完成后才添加 includeSubDomains
一旦每个公共子域名都已准备好 HTTPS,你可以考虑:
Strict-Transport-Security: max-age=31536000; includeSubDomains
这是需要保守的时刻。如果仍有一个遗留服务需要 HTTP,就不要在父域名上添加 includeSubDomains。要么迁移该服务,要么把它移到另一个域名,要么接受你的 HSTS 策略目前必须保持更窄的范围。
安全响应头应反映现实。它们不应被用作你希望未来拥有的基础设施的励志海报。
Step 6: 把 preload 当作一个单独项目
只有在以下全部条件都成立时,才考虑 preload:
- 该域名及所有子域名都支持有效 HTTPS。
- HTTP 会重定向到 HTTPS。
- HSTS 响应头使用至少 31536000 秒的
max-age。 - 响应头包含
includeSubDomains。 - 响应头包含
preload。 - 你确信该域名下任何位置都不再需要明文 HTTP。
一个可用于 preload 的响应头如下:
Strict-Transport-Security: max-age=31536000; includeSubDomains; 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。不要在源站设置一种策略、在边缘设置另一种策略,除非你有非常明确的理由。
还要检查 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 对新的浏览器配置文件并不足够。你还需要请求从预加载列表中移除,并等待该变更通过浏览器更新发布出去。
发布前测试清单
在增加 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 部署是平淡无奇的。它们缓慢推出,由可靠证书支撑,并且无聊到没人注意。这正是你希望从一个失败模式可能非常剧烈的响应头那里得到的结果。