协议特性

HSTS 详解:严格传输安全、预加载与常见坑

作者: · 最后核实

HSTS(HTTP Strict Transport Security,HTTP 严格传输安全)是一个响应头,告诉浏览器在指定秒数内只能用 HTTPS 访问这个主机。浏览器一旦收到 Strict-Transport-Security: max-age=31536000,之后所有 http:// 请求都会在发出之前被它自己改写成 https://,而且遇到证书错误不再允许用户点“继续访问”。它补上了 301 跳转留下的明文缺口,代价是证书出错会直接变成网站打不开。

HSTS 解决什么问题

HSTS 去掉的是“第一跳明文请求”。没有 HSTS 时,用户在地址栏输入 example.com,或者点了一个旧的 http:// 链接,第一个请求是明文发出去的。同一路径上的攻击者(公共 Wi-Fi、被控制的路由器)可以抢先应答,让用户一直停在 HTTP 上,自己在后面代理真正的 HTTPS 站点。这就是促成 RFC 6797 的 SSL stripping(SSL 剥离)攻击。

没有 HSTS                                 有 HSTS(首次访问之后)
--------                                  ----------------------
输入 example.com                          输入 example.com
  |                                         |
  v                                         v
GET http://example.com/(明文)           浏览器内部直接改写:
  |  <-- 攻击者可在这里截获                 https://example.com/(内部 307)
  v                                         |
301 -> https://example.com/                 v
  |                                       TLS 握手,校验证书
  v                                       (证书错误不可绕过)
TLS 握手

301 跳转仍然需要,首次访问的用户靠它进入 HTTPS;HSTS 负责让之后的每次访问都跳过 HTTP。

响应头和三个指令

这个头只有一个必填指令、两个可选指令,并且只在证书有效的 HTTPS 连接上被浏览器接受。

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
指令 出处 含义
max-age=<秒> RFC 6797 必填。浏览器记住策略多久,每次收到新响应都会重新计时。max-age=0 表示删除策略。
includeSubDomains RFC 6797 策略覆盖所有层级的子域名:www、api、intranet.corp 等等。
preload 不在 RFC 6797 里 只是给 hstspreload.org 看的“同意收录”标记,浏览器本身不把它当指令处理。

RFC 6797 里几条容易踩坑的规定:

  • HTTP 响应里的头被忽略。 防止攻击者在明文里设置或清除你的策略。
  • 证书必须有效。 如果这次 HTTPS 响应本身证书有问题,头不会被记录。
  • 策略按主机生效。 www.example.com 发的头管不到 example.com。想用 includeSubDomains 覆盖整个域名,根域名自己必须发这个头,所以预加载检查的是根域名。
  • 错误变成硬失败。 RFC 6797 第 12.1 节称之为“no user recourse”(用户无路可退)。HSTS 主机证书过期、域名不匹配、颁发者未知,浏览器都只显示错误页,没有“继续前往”的链接。

首次访问问题与预加载列表

HSTS 保护不了从没见过你这个头的浏览器,预加载列表就是为了补这个洞。Chrome 在源代码里内置了一份 HSTS 主机列表,Firefox、Safari、Edge 的列表也以它为基础。在列表里的域名,浏览器从第一次请求起就只走 HTTPS。

截至 2026 年 11 月,通过 hstspreload.org 提交需要满足:

  1. 证书有效。
  2. 如果监听 80 端口,必须在同一主机上把 HTTP 跳转到 HTTPS。
  3. 所有子域名都支持 HTTPS;只要 www 有 DNS 记录,www 也必须支持。不对外开放的内网子域名同样算在内。
  4. 根域名的 HTTPS 响应带 HSTS 头:max-age 至少 31536000 秒,包含 includeSubDomains 和 preload。
  5. HTTPS 站点上如果还有额外的跳转,跳转响应本身也要带 HSTS 头。

官网的说法很直白:移除“通常又慢又痛苦”,新条目进入 Chrome 稳定版“可能要几个月”,其他浏览器跟不跟进也没法保证。现在的页面还明确写着:推荐 HSTS,但不推荐 HSTS 预加载。原因是 Chrome 和 Safari 已经会自动把 HTTP 导航升级为 HTTPS,预加载只在攻击者主动阻断这种升级时才有额外价值。

整个顶级域名也可以预加载。Google 就把 .dev、.app 等自己运营的后缀整体加进了列表,这些后缀下的任何域名在浏览器里天生只能走 HTTPS。买了 .app 域名,第一天就得准备好证书,没有 HTTP 可退。

国内基于 Chromium 内核的浏览器(360、QQ 浏览器等)一般也带有预加载列表,但内核版本可能落后于 Chrome,新加入或刚移除的条目生效时间会更晚。

分阶段安全上线

max-age 要分阶段逐步拉长,每个阶段等满时长再进入下一阶段。hstspreload.org 推荐的就是这个做法,并且从一开始就带上 includeSubDomains,让子域名的问题尽早暴露。

阶段 头的取值 进入下一阶段前等待 检查什么
1 max-age=300; includeSubDomains 5 分钟到 1 天 每个子域名都能 HTTPS 打开,没有混合内容
2 max-age=604800; includeSubDomains 1 周 内部工具、测试环境、网页邮箱、老的活动页
3 max-age=2592000; includeSubDomains 1 个月 流量和错误率没有异常
4 max-age=31536000; includeSubDomains 长期 证书监控和自动续期已就位
5(可选) max-age=63072000; includeSubDomains; preload 然后再去提交 确认今后没有任何子域名需要 HTTP

一定要等满,因为浏览器一旦记住了较长的 max-age,你之后改短也没用,它要等下次访问才会拿到新值。

开始之前先把证书续期自动化。开了 HSTS 以后,漏续一次证书不是“用户可以点过去的警告”,而是事故。参见自动化证书续期,以及47 天证书里证书有效期不断缩短的时间表。

服务器配置示例

HSTS 头要加在所有 HTTPS 响应上,包括错误页和跳转。nginx 里要带 always 参数,否则 4xx、5xx 响应不会加这个头。

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;   # 这里不加 HSTS,加了也会被忽略
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        root /var/www/example.com;
    }
}

nginx 有个细节导致很多“HSTS 头莫名消失”的问题:只有当前层级没有任何 add_header 时,才会继承上一层的 add_header。如果某个 location 里用 add_header 加了 Cache-Control,这个 location 的 HSTS 头就悄悄没了。解决办法是在该 location 里重复写一遍 HSTS,或者把公共响应头放进 include 文件。

Apache(需启用 mod_headers):

<VirtualHost *:443>
    ServerName example.com
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>

宝塔面板:SSL 页面的“强制 HTTPS”开关只是加了 301 跳转,并不等于 HSTS。需要在“网站 → 设置 → 配置文件”里,于 443 的 server 块中手动加上面那行 add_header,保存后宝塔会自动重载 nginx。

阿里云、腾讯云等 CDN:TLS 在 CDN 节点终止时,HSTS 应该在 CDN 控制台的 HTTPS 配置里开启,这类控制台通常都有 HSTS 开关。如果 CDN 和源站都加了这个头,响应里会出现两个;RFC 6797 规定浏览器只处理第一个,所以要么两边取值一致,要么只在一处设置。

命令行验证:

curl -sI https://example.com/ | grep -i strict-transport-security
curl -sI https://example.com/does-not-exist | grep -i strict-transport-security   # 404 也要有
strict-transport-security: max-age=31536000; includeSubDomains

HSTS 检测工具会做同样的检查,并告诉你域名是否已在预加载列表中。

常见坑

HSTS 事故大多出在没人记得的子域名上,而不是主站。

  • 被遗忘的纯 HTTP 子域名。 includeSubDomains 会覆盖 printer.office.example.com、放在虚拟主机上的旧博客、第三方托管的 status 页。只要其中一个没有有效证书,访问过根域名的浏览器就再也打不开它。
  • 公网域名下的内网主机名。 像 git.corp.example.com 这种用自签名或私有 CA 证书的内网服务,一旦根域名发了 includeSubDomains,员工浏览器就会报错,除非每台设备都装了私有 CA 根证书。
  • 在正式域名上做开发。 开发人员用 HTTP 访问 dev.example.com:8080,策略生效后会被强制改成 HTTPS。本地开发请用单独的域名。
  • 清浏览器缓存不是解决办法。 Chrome 可以在 chrome://net-internals/#hsts 删除动态记录,但删不掉预加载条目,而且你的用户也不会去删。
  • 只有首页带头。 在应用代码里设置响应头,往往漏掉静态文件和错误页。应该在 Web 服务器或反向代理层统一设置。
  • 证书过期等于宕机。 这是“用户无路可退”的代价。要监控到期时间和整条证书链,参见证书链。

HSTS、HTTPS DNS 记录与浏览器默认 HTTPS

现在推动浏览器走 HTTPS 的机制不止 HSTS,但只有 HSTS 同时禁止绕过证书错误。RFC 9460(2023 年 11 月)定义了 HTTPS 类型的 DNS 记录,第 9.5 节规定:客户端发现某主机有 HTTPS 记录时,应把 http:// 请求当成收到 307 跳转到 https://,“类似 HSTS”。它还允许客户端对这类源站取消“绕过证书错误”的选项,并要求发布该记录的源站不能依赖这种绕过。

主流浏览器自己也会优先尝试 HTTPS:hstspreload.org 指出 Chrome 和 Safari 无论有没有 HSTS 都会自动把 HTTP 导航升级为 HTTPS。但这种升级在 HTTPS 失败时会退回 HTTP,攻击者正好利用这一点;HSTS 没有回退。

机制 信号在哪里 保护首次访问 会退回 HTTP 禁止绕过证书错误
301 跳转 HTTP 响应 否 是(可被剥离) 否
浏览器自动升级 HTTPS 浏览器功能 是,尽力而为 是 否
HTTPS DNS 记录(RFC 9460) DNS 是,前提是 DNS 可信 取决于客户端 可选(MAY)
HSTS 头(RFC 6797) HTTPS 响应 否 否 是
HSTS 预加载 浏览器源代码 是 否 是

上线前检查清单

下面每一项都确认之前,不要把 max-age 调到一个月以上:

  • 域名下每个 DNS 名称要么能提供有效证书,要么根本不解析。用 CT 证书查询搜一下自己的域名,能列出曾经签发过证书的主机名,是做资产盘点的好起点。
  • 证书续期已经自动化并有监控,到期前很早就会告警。
  • 页面没有混合内容;HSTS 只升级访问你主机的请求,不会修好写死在第三方主机上的 http:// 地址。
  • 200、301、404、500 响应都带了这个头。
  • 你清楚预加载在几个月内基本是单向的,而且 hstspreload.org 已经不推荐大多数网站预加载。

底层协议见TLS 的工作原理;术语见 HSTS 和 HSTS 预加载。

常见问题

HSTS 的 max-age 设多少合适?

先用 max-age=300 试水,再按一周、一个月逐级拉长,期间盯住子域名有没有打不开。长期值一般是一年或两年:max-age=31536000(365 天)或 max-age=63072000(730 天)。一年也是 hstspreload.org 收录的最低要求。

在 HTTP 响应里加 HSTS 头有用吗?

没用。RFC 6797 规定浏览器必须忽略明文 HTTP 响应里的 Strict-Transport-Security 头,否则中间人可以随意注入或删掉它。正确做法是 HTTP 先 301 跳到 HTTPS,在 HTTPS 响应里再发这个头。

HSTS 开了以后怎么关掉?

在 HTTPS 响应里发 Strict-Transport-Security: max-age=0,浏览器下次访问时就会删除策略。没再来访问的浏览器会一直保留旧策略,直到原来的 max-age 到期。如果域名已经进了预加载列表,还要去 hstspreload.org 申请移除,并等浏览器发版,通常要几个月。

HSTS 网站证书报错时,用户能点“继续访问”吗?

不能。对已知的 HSTS 主机,浏览器必须把证书或 TLS 错误当作致命错误,不提供绕过入口。所以开了 HSTS 一定要做证书到期监控,证书过期对老访客来说就是整站不可用。

要不要把域名提交到 HSTS 预加载列表?

只有在确定所有子域名(包括内网子域名)都能长期提供有效 HTTPS 时才考虑。hstspreload.org 现在明确写着推荐 HSTS、不推荐预加载,因为 Chrome 和 Safari 已经会自动把大多数访问升级到 HTTPS,而从列表移除非常慢。

资料来源

  1. RFC 6797: HTTP Strict Transport Security (HSTS)
  2. HSTS Preload List Submission (hstspreload.org)
  3. Chromium: HTTP Strict Transport Security
  4. RFC 9460: SVCB 与 HTTPS DNS 记录(第 9.5 节 HSTS)
  5. nginx: ngx_http_headers_module(add_header)
  6. Mozilla wiki: Security/Server Side TLS