HSTS preload 进了出不来:如何回滚 HTTPS 强制

你提交了 preload,后来又需要 HTTP。浏览器还在强制 HTTPS。这里给出最快的真实修法、确切的移除步骤,以及诚实的时间线。

你按安全清单把域名提交到了 hstspreload.org。几周后它进了 Chrome 和 Firefox 的内置列表。然后你要回滚:可能要卖域名、可能证书断了想临时回到 HTTP、可能要下线。你把 Strict-Transport-Security header 删了。浏览器照样对你的域名和所有子域强制 HTTPS(因为 preload 必须勾 includeSubDomains)。这是设计如此:preload 把域名编进了浏览器二进制,所以光改服务端没用。

最快的修法(90% 的情况):你几乎肯定根本不用从 preload 移除。 大多数”HSTS preload 卡死”工单其实是”证书过期我们慌了”。在 HTTPS 上重新签一张有效证书,症状几分钟就没了。只有当你真的需要让 apex 或某个子域永远跑明文 HTTP 时,才去走移除流程,而且即便如此也要做好长达数月的尾巴的准备。

先判断你属于哪一类

动手前先对号入座。正确的做法很少是”从 preload 列表移除”。

症状真正原因最快修法
NET::ERR_CERT_DATE_INVALID,没有”继续”按钮preload 域名证书过期重新签证书(第 1 步),不用移除。
新子域还没敲完就报 ERR_SSL_PROTOCOL_ERRORincludeSubDomains 把它也强制成 HTTPS给子域装真证书,或把它从 preload 父域挪走(第 2 步)。
内部 staging NET::ERR_CERT_AUTHORITY_INVALID,无法跳过preload 父域下用了自签名证书staging 改用单独的、非 preload 域名(第 2 步)。
只在酒店 / 机场 Wi-Fi 上打不开强制门户拦不到 HTTPS让用户先打开 http://neverssl.com;不是域名的问题。
你在卖 / 下线这个域名新主人没法跑 HTTP提交移除并等推送完成(第 3、4、6 步)。

常见原因

按事故复盘里出现的频率排序。

1. 某个子域忘了,现在却需要 HTTP

你 preload 时勾了 includeSubDomains。几个月后有人开了 legacy.yourdomain.com,跑在只支持 HTTP 的服务上(旧的 IoT 控制台、打印机后台、CI artifact 服务器)。浏览器永远拒绝用 HTTP 打开它,因为父域的 preload 条目强制下面所有东西走 HTTPS。

怎么判断:新子域返回 ERR_SSL_PROTOCOL_ERROR 或 “This site can’t be reached”,用户连 http:// 都还没敲,Chrome 就先把协议改成 HTTPS 了。

2. 证书过期了,新证书一时签不出来

证书过期,源站 HTTPS 挂了。用户看到 NET::ERR_CERT_DATE_INVALID,而且没有”继续访问”链接——preload 设了严格位,不给点穿。preload 域名没有点穿。

怎么判断:证书已过期,并且在 chrome://net-internals/#hsts 查域名显示为 static。没有 bypass。

3. 域名所有权转出了,新主人需要 HTTP

你卖掉或释放了域名。新主人继承了 preload 条目——在域名从列表移除并且浏览器缓存老化之前,他不能给 apex 或任何子域跑明文 HTTP。

怎么判断:新主人在别的域名能部署,但只要把 yourdomain.com 指过去且没 HTTPS 就崩。

4. 自签名 / 内部 CA 证书不能再被接受

preload 之前,用户能对内部 staging 服务器点穿证书警告。preload 之后没法点穿。内部 QA 团队被锁在 staging 外面。

怎么判断:内部 staging staging.yourdomain.comNET::ERR_CERT_AUTHORITY_INVALID,没有跳过按钮。

5. 公共 Wi-Fi 强制门户拦不到

很多强制门户靠拦截明文 HTTP 请求、重定向到登录页工作。在你的 preload 域名上浏览器根本不发 HTTP 请求,门户触发不了,用户也登不上。基础 Wi-Fi 能通,但用着像断网。

怎么判断:用户反馈站点在酒店 / 机场 Wi-Fi 上不行、别的地方行,其他非 preload 站点正常。临时办法:让用户先打开一个已知走 HTTP 的页面,比如 http://neverssl.com,把门户弹出来。

6. 通配符证书配错代价被放大

没 preload,通配符配错至少给个证书警告、能点穿排错。有 preload,任何证书不匹配都是硬挂、没 bypass——小错变事故。

怎么判断:preload 域名上任何证书不匹配 = 直接 NET::ERR_CERT_COMMON_NAME_INVALID,没法跳过。

开始前

  • 确认域名真在 preload 上:访问 https://hstspreload.org/?domain=yourdomain.com。pending 和已发布的条目报告方式不同,注意你看到的是哪种。
  • 看清楚用户主要用什么浏览器。Chrome / Firefox / Safari 各自有自己的 preload 列表、更新节奏不一样,而 Chromium 系(Edge / Brave / Vivaldi / Opera)都继承 Chrome 的列表。
  • 把当前证书状态记下来:过期日、issuer、证书链。
  • 想清楚你真正的目标:是”永远回滚”,还是”接受 HTTPS-only,把证书修对就行”。后者几乎总是答案。

需要收集的信息

  • curl -sI https://yourdomain.com | grep -i strict-transport——HSTS header 还在发吗、里面有没有 preload
  • 域名在 chrome://net-internals/#hsts(顶部查询框)里的状态。static = preload 条目;dynamic = 你服务器 header 设的。
  • nginx / app 里原来配的 max-ageincludeSubDomains 值。
  • 当前在用的所有子域、每个能不能正常出 HTTPS。
  • 当前有多少用户被卡 / 不便。

一步步修

诚实顺序:短期路径 = 把 HTTPS 恢复;长期路径 = 从 preload 列表移除。先试短期路径。

第 1 步:先看是不是只要把 HTTPS 恢复就行

大多数”HSTS preload 卡死”工单其实是证书过期。重新签。

sudo certbot renew --force-renewal
sudo systemctl reload nginx
# 确认新的有效期窗口:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
  | openssl x509 -noout -dates

有效证书拉回来,问题就没了,根本不用碰 preload 列表。如果在 CDN 后面,“重签”可能简单到只是开启服务商的免费边缘证书——把域名挂到 Cloudflare 代理后面,几分钟就发一张。

第 2 步:内部 staging 需要自签名,就别用 preload 父域的子域

最干净的方案是别在 preload 父域下用内部 / 自签名的工作。staging 挪到单独注册的域名,比如 staging.yourdomain-internal.com——不在 preload 列表上。

实在要保留这个子域名,就装一张公网受信的真证书。Let’s Encrypt 用 DNS-01 challenge 给内部专用主机名也能签,因为 DNS-01 不需要入站 HTTP——只要一条 TXT 记录。

第 3 步:别再带 preload(必要时再发 max-age=0)

这一步是大多数教程搞错的地方。要变成可移除,你需要 max-age=0。按 hstspreload.org/removal/ 的官方要求,你只需发一个不再包含 preload 指令的有效 HSTS header。HSTS 本身继续生效没关系。

所以最小改动就是把 header 里的 preload 去掉:

# 满足移除条件:HSTS 有效,但没有 preload 指令。
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

如果你还想让浏览器彻底忘掉 HSTS(比如你确实需要 apex 上跑 HTTP),就发 max-age=0。按 RFC 6797 §6.1.1,这会在浏览器下次发起 HTTPS 请求时清掉动态缓存:

# 客户端重新走 HTTPS 后,对非 preload 客户端关闭 HSTS。
add_header Strict-Transport-Security "max-age=0" always;

不管哪种,都要重载 nginx(sudo nginx -t && sudo systemctl reload nginx)。这两种改动都碰不到编进浏览器二进制的静态 preload 条目——但发一个不带 preload 的 header 是第 4 步的前置条件。

第 4 步:到 hstspreload.org 提交移除申请

访问 https://hstspreload.org/removal/,输入域名。要被接受,域名必须满足(截至 2026 年 6 月,照表单原文):

  1. 当前已 preload待 preload(pending),且经由 hstspreload.org
  2. 有效证书提供 HTTPS。
  3. 发一个不包含 preload 指令有效 HSTS header

都满足,域名就进移除队列。之后按浏览器分别生效:

  • Chrome:移除先合入 Chromium,再随下一个 stable 大版本发布。截至 2026 年 6 月 Chrome 是 4 周一个 stable(2026 年 9 月起随 Chrome 153 改为 2 周)。Google 自己的估计是,移除要 6-12 周才能到大多数 Chrome 用户
  • Firefox:节奏类似;Mozilla 从 Chromium 源重新生成自己的列表。
  • Safari / WebKit:独立流程,通常更慢。

所以大多数及时更新的用户会在大约 6-12 周内不再强制 preload。真正的长尾——很少更新浏览器的用户——可能还会带着旧的内置列表好几个月。两头都要计划:多数人快,掉队的人慢。

第 5 步:当下受影响的用户怎么救

浏览器里已经有 preload 条目的用户没法轻易绕。真有用的办法:

  • 换浏览器:Firefox 和 Chrome 的列表略有差异,可能其中一个还没收录你的域名。(Edge / Brave / Vivaldi / Opera 都继承 Chrome 列表,在它们之间换没用。)
  • 内部用户的话立刻部一张公网受信的真证书。把域名挂到 Cloudflare 代理后面,几分钟就拿到可用的边缘证书。
  • 开发调试的话 chrome://net-internals/#hsts → “Delete domain security policies” 删动态条目,但删不了静态 preload。用处有限,但当你怀疑是过期的动态条目(而非 preload 列表)时值得一试。

第 6 步:必要时才永久回滚

要永久回滚(卖域名 / 下线):

  1. 发一个不带 preload 的 HSTS header(或 max-age=0)——第 3 步。
  2. hstspreload.org/removal/ 提交移除——第 4 步。
  3. 等下一个去掉你条目的 stable Chrome / Firefox(多数用户约 6-12 周)。
  4. 接受很少更新浏览器的用户还会继续强制 HTTPS 数月、甚至数年。
  5. 告诉接手域名的人这个约束:在很长尾的浏览器装机量上,HTTP 都跑不了。

怎么确认修好了

  • https://hstspreload.org/?domain=yourdomain.com 显示域名 not preloaded(移除发布后)。
  • curl -sI https://yourdomain.com | grep -i strict-transport 不返回 header、返回 max-age=0,或至少返回一个不带 preload 的 header。
  • 在更新过的 Chrome 上,于 chrome://net-internals/#hsts 查域名,静态条目返回 “not found”。
  • apex 下的新子域能跑明文 HTTP、浏览器不改协议——只有在移除发布测试用的浏览器已更新后才成立。用一个你从没访问过该站点的浏览器测试,避免过期的动态条目把结果掩盖掉。

长期预防

  • hstspreload.org 提交当作单向门。除非你能保证所有现有和未来子域永远只走 HTTPS、并且有一套验证过的证书续签流水线,否则不要提交。
  • 生产环境用 max-age=31536000; includeSubDomains(一年)但不带 preload 指令。你拿到几乎全部安全收益,同时保持可逆。等有了一整年干净 HTTPS uptime 再考虑提交 preload——或者干脆别提交。
  • 通配符域、内部服务域、多租户共享域永远不要提交 preload。preload 只留给公开的市场 apex。
  • 证书过期前 25+ 天就告警,不是 7 天。preload 让证书断档变成没有用户 bypass 的硬故障。
  • 在 DNS-as-code 仓库里把 preload 这个约束写清楚,免得未来工程师把不支持 HTTPS 的服务挂到子域。

常见坑

  • 把 nginx 里 HSTS header 删了就以为完事。preload 是浏览器二进制里的、不是你服务器配的。
  • 以为移除表单需要 max-age=0。它只要一个不带 preload 指令的有效 HSTS header;max-age=0 是可选的、只对清动态缓存有意义。
  • 为了”安全评分”提交 preload,不理解它不可逆。大多数安全团队会接受一年的 max-age 拿到同等合规收益。
  • 想在生产浏览器里绕过 preload。没有面向用户的 bypass——thisisunsafe 只能关掉证书警告 interstitial,对 preload 的强制无效。
  • 把子产品放在 preload 父域的子域下,然后惊讶自签名证书不能用。
  • 以为移除明天就生效。它跟下一个浏览器大版本一起发,不按需——多数 Chrome 用户约 6-12 周。

FAQ

Q:移除表单到底要什么——需要 max-age=0 吗?

不需要。截至 2026 年 6 月,hstspreload.org/removal/ 表单只要求你的域名仍用有效证书提供有效 HSTS header,且把 preload 指令去掉。你可以保留正常的 max-ageincludeSubDomains。只有当你另外还想对非 preload 客户端关闭 HSTS 时,才发 max-age=0

Q:从提交移除到真实用户生效要多久?

Google 自己的说法是,移除合入 Chromium 并随 stable 大版本发布后(截至 2026 年 6 月 Chrome 每 4 周发一版),约 6-12 周到大多数 Chrome 用户。Firefox 节奏类似,Safari 是更慢的独立流程。极少更新浏览器的长尾用户可能还会带着旧列表好几个月。

Q:在 Chrome 里删个缓存条目能绕过 preload 吗?

你能通过 chrome://net-internals/#hsts → “Delete domain security policies” 清动态条目(你服务器 header 设的)。但你清不了静态 preload 条目——它编进了浏览器。用一个还没收录该条目的浏览器,是唯一真的 workaround。

Q:有没有靠 max-age 实现的”软 preload”?

有。Strict-Transport-Security: max-age=31536000; includeSubDomains(一年,不带 preload)能拿到几乎全部安全收益、没有不可逆的代价,大多数合规框架都接受。

Q:安全扫描说我不 preload 就扣分,要不要提交?

扫描器太死板的话,推回去:preload 不可逆,相比长 max-age 的边际安全收益,运维风险更大。NIST 和 CIS 的指南都把长 max-age 视为合规的 HSTS 配置。

参见 绑了自定义域名但 SSL 没发HTTPS 没强制SSL 混合内容警告换域名后重定向断了

标签: #排查 #SSL #https #hsts #安全