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

> HSTS 如何强制浏览器只走 HTTPS，max-age、includeSubDomains、preload 各是什么意思，怎么分阶段上线，为什么预加载很难撤回。

Source: https://ssl.sh/zh/learn/hsts/
Last updated: 2026-11-10

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 剥离）攻击。

```text
没有 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 连接上被浏览器接受。

```text
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 以后，漏续一次证书不是“用户可以点过去的警告”，而是事故。参见[自动化证书续期](https://ssl.sh/zh/learn/automate-certificate-renewal/)，以及[47 天证书](https://ssl.sh/zh/learn/47-day-certificates/)里证书有效期不断缩短的时间表。

## 服务器配置示例

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

```nginx
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`）：

```apache
<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 规定浏览器只处理第一个，所以要么两边取值一致，要么只在一处设置。

命令行验证：

```bash
curl -sI https://example.com/ | grep -i strict-transport-security
curl -sI https://example.com/does-not-exist | grep -i strict-transport-security   # 404 也要有
```

```text
strict-transport-security: max-age=31536000; includeSubDomains
```

[HSTS 检测工具](https://ssl.sh/zh/tools/hsts-checker/)会做同样的检查，并告诉你域名是否已在预加载列表中。

## 常见坑

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 服务器或反向代理层统一设置。
- **证书过期等于宕机。** 这是“用户无路可退”的代价。要监控到期时间和整条证书链，参见[证书链](https://ssl.sh/zh/learn/certificate-chains/)。

## 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 证书查询](https://ssl.sh/zh/tools/ct-search/)搜一下自己的域名，能列出曾经签发过证书的主机名，是做资产盘点的好起点。
- 证书续期已经自动化并有监控，到期前很早就会告警。
- 页面没有[混合内容](https://ssl.sh/zh/glossary/mixed-content/)；HSTS 只升级访问你主机的请求，不会修好写死在第三方主机上的 `http://` 地址。
- 200、301、404、500 响应都带了这个头。
- 你清楚预加载在几个月内基本是单向的，而且 hstspreload.org 已经不推荐大多数网站预加载。

底层协议见[TLS 的工作原理](https://ssl.sh/zh/learn/how-tls-works/)；术语见 [HSTS](https://ssl.sh/zh/glossary/hsts/) 和 [HSTS 预加载](https://ssl.sh/zh/glossary/hsts-preload/)。
