HSTS 是一种强制浏览器通过 TLS 与服务器通信的机制。启用 HSTS 后,浏览器便只会通过 TLS 向相关域名发送请求。此外,当因为攻击等任何原因导致 TLS 证书不受信时,HSTS 还会禁止大意的用户忽略错误继续建立连接,从而最大程度地保证用户信息的安全。

HSTS 预加载

一般的 HSTS 部署通过在响应中添加 HSTS 头来实现。HSTS 头指示浏览器在接下来的一段时间内只通过 TLS 与域名背后的服务器通信。这样的实现方式虽然大幅度提高了安全性,但仍然存在缺陷。当用户首次访问域名,或先前收到的 HSTS 指令已过期时,浏览器默认仍会通过明文方式建立连接 (除非用户手动指定 HTTPS 等安全的协议名,多数用户是不会这么做的),这就为监听与劫持提供了可能。

要解决这一问题,可以将域名添加到 HSTS 数据库中。HSTS 数据库是一个硬编码在浏览器中的启用了 HSTS 的域名列表。支持 HSTS 预加载的浏览器 (包括 Chrome, Firefox, Microsoft Edge 等主流现代浏览器) 在向服务器发送请求时,会检查这一列表。如果目标域名在此列表中,则浏览器会直接通过 TLS 发送请求,不论用户是否指定了安全协议。这就消除了首次连接时的安全风险。

另外,由于浏览器在任何时间都不会通过明文发送请求,服务器自然也就无法根据 “用户是否尝试建立明文连接” 配合层级重定向来标识与跟踪用户,有利于增进用户信任。注意类似的技巧也可以用在 Favicon 等其它浏览器长期缓存的信息上。

有 .dev 等一部分顶级域名是自动包含在 HSTS 数据库中的 (此类顶级域名多数为 Google 管理)。在使用这类域名时,必须保证服务可以支持 TLS, 否则将无法通过多数浏览器访问。如果自己的域名不属于此类,也可以在 hstspreload.org 提交申请,将其添加到 HSTS 数据库中。HSTS Preload 将检查你的域名与服务器是否满足要求,给出整改建议,并将合规的域名添加到待添加名单中。这些域名将随着各大浏览器的迭代进入实际的 HSTS 数据库中。

截至本文发布,通过 HSTS Preload 提交域名的要求如下:

  1. 只能提交 Apex 域名 (如 example.com 及 dl444.net),不能提交子域名
  2. 必须提供受信的 TLS 证书
  3. 不监听 80 端口,或者在 80 端口立即重定向到支持 TLS 的端口
  4. 对域名 Apex 的请求返回满足以下要求的 HSTS 头
    • 有效期至少为一年
    • 必须包括 includeSubDomainspreload 指令
  5. 任何子域名必须同样支持 TLS

需要注意的是,域名及服务器必须持续地满足上述条件,否则将可能被移除出 HSTS 数据库。

满足提交要求

不难发现,要想通过 HSTS Preload 将域名添加到 HSTS 数据库,则域名 Apex 必须能够响应 HTTP 请求。在 Azure 中,比较 “正常” 的解决方案是创建一个虚拟机或 App Service 实例,并将域名 Apex 通过 A 或 AAAA 记录映射到实例上,通过计算实例运行 HTTP 服务器。

截至本文发布,Azure 中最廉价的计算实例每月定价约为 $4 (Apps Service 有免费级别,但不支持定制域名)。尽管 $4 并不是十分昂贵,但当其它应用均为低流量无服务器应用时,$4 在总成本中的占比便变得相当高。一个低流量无服务器应用每月的成本不足 $1, 如果没有其它在域名 Apex 上运行服务的需求,仅仅是为了 HSTS 而多花几倍的钱使用计算实例,不免令人感到有些浪费。

如果不需要在域名 Apex 上运行动态 Web 应用程序,则我们也可以使用 Azure CDN 的规则引擎来满足 HSTS 预加载的要求。如果只是用于提供 HSTS 头,则这一方案的成本几乎为零。而如果有需要,Azure CDN 也可以用于优化静态内容分发。

由于 CDN 作为分布系统,是没有唯一固定的 IP 的,这也就意味着我们无法使用常规的 A 或 AAAA 记录将域名映射到 CDN 端点,而只能使用 CNAME 记录创建一个别称映射。同时,DNS 规则在事实上禁止了在域名的 Apex 上添加 CNAME 记录。这看起来矛盾,但目前包括 Azure DNS 与 Cloudflare 在内的不少 DNS 服务现在都提供了 CNAME 扁平化的功能。通过这一功能,我们可以照常在 Apex 上添加 CNAME 记录,而当用户进行 DNS 查询的时候,DNS 服务会递归地查询 CNAME 指向的域名 DNS, 直到查询到一个 A 或 AAAA 记录,并返回这一记录。这就允许我们通过 CNAME 在 Apex 上创建别称,而在查询方看来,又不违反 RFC 1034 的规则。Azure CDN 也支持通过 CNAME 扁平化的方式将域名 Apex 映射到 CDN 端点。

创建 CDN 端点

我们将使用 Azure CDN 的规则引擎功能来进行明文请求的重定向,并为响应添加 HSTS 头。Azure CDN 有 Microsoft 标准版,Akamai 标准版,Verizon 标准版,Verizon 高级版四种类型,不同的类型有不同的功能与性能指标,定价方案也有所不同。其中 Microsoft 标准版及 Verizon 高级版支持我们这里需要的规则引擎功能。

首先创建一个 CDN 配置文件。CDN 配置文件是 CDN 端点的容器,上面提到的四种类型,指的便是配置文件的类型。配置文件里面的所有端点共享同样的类型与定价方案,在账单上合并计费。本文将选取 Microsoft 标准版进行介绍,因此本人在这里选择 Standard Microsoft 作为计价级别。当然,如果有已经有合适的 CDN 配置文件,也可以直接利用。

接下来在配置文件中创建一个 CDN 端点。如果前面一步使用 Azure 管理门户创建新的 CDN 配置文件,也可以在创建配置文件的同时创建一个端点。如果需要使用 Azure CDN 优化静态内容分发,则需要相应地配置源。如果只是为了使用规则引擎来提供 HSTS 头,则可以配置源为任何一个你实际控制的可公开资源,例如一个空的存储帐户。

创建一个计价级别为 Standard Microsoft 的 CDN 配置文件,同时创建一个名为 dl-hsts-test 的端点,其源指向一个空存储帐户

添加域名

接下来将域名 Apex 映射到 CDN 端点。继续之前,请确保域名使用的 DNS 服务支持 CNAME 扁平化。Microsoft 推荐使用 Azure DNS, Azure DNS 与 Azure CDN 有集成,可以省去很多配置步骤。如果使用 Azure DNS, 请参考文档进行域名的配置。本文将使用 Cloudflare 的 DNS 服务,虽然需要手动配置,但同样可以正常使用。

首先在 DNS 服务中添加两条记录:

类型 名称 目标
CNAME @ <endpoint>.azureedge.net
CNAME cdnverify cdnverify.<endpoint>.azureedge.net

其中 <endpoint> 为端点的名称。对于上图的例子,该值应为 dl-hsts-test. 第二条在域名添加完成后可以删除,因此其 TTL 可以设置得稍小一点。另外,如果使用 Cloudflare 的 DNS 服务,应该关闭代理功能。

为域名 Apex 创建指向 dl-hsts-test.azureedge.net 的 CNAME 记录

记录添加完成后,将域名添加到 CDN 端点中。

将 Apex 域名添加到 CDN 端点中

配置 TLS 证书

接下来为新添加的域名配置 TLS 证书。如果使用管理门户进行配置,则点击上一步添加的域名,选择启用 HTTPS.

Azure CDN 可以通过自动证书管理为我们提供免费的证书,或者我们也可以自行提供证书。对于域名为 Apex 的情况,如果使用自动证书管理,则需要通过域名的管理邮箱批准颁发证书,详见文档。如果没有文档中列举的任何一个域名管理邮箱,则必须自行提供证书。

如果自行提供证书,则需要将证书导入到 Key Vault 中。如果使用 Let’s Encrypt 等使用 ACME 协议的证书颁发机构,则可以使用 keyvault-acmebot 来自动完成证书的获取,更新与导入。如果证书是其它方式获得的,则需要手动将证书导入到 Key Vault. 关于自行提供证书的配置方法,请参见文档

为添加的域名配置 TLS 证书

证书的完全部署需要几分钟到几小时。在等待期间,可以进行下一步配置。证书部署完成后,我们就满足了 HSTS 预加载的第 2 项条件。

添加规则

接下来为 CDN 端点添加规则,以满足 HSTS 预加载的条件 3 与条件 4. 截至本文发布,对于 Microsoft 标准版 CDN,每个端点的前 5 条规则免费,后续规则每条每月计费 $1. 若全局规则非空,则也按一条规则计算。

对于 Microsoft 标准版的规则引擎,规则从下方到上方匹配的。当一条规则匹配成功后,其它规则将不被执行。

首先添加明文重定向规则,将所有通过 HTTP 协议发送的请求重定向到 HTTPS 协议。其次添加 HSTS 头规则,对于所有通过 HTTPS 协议发送的请求,在响应中添加名为 Strict-Transport-Security 的头,其值为 max-age=<timeBySeconds>; includeSubDomains; preload. 其中 <timeBySeconds> 为以秒为单位的指令有效期。这里选取 63072000, 即两年。

注意不要将 HSTS 头规则添加到全局规则中,这样会使通过 HTTP 协议传输的明文响应也附带 HSTS 头。出于安全考虑,浏览器会丢弃通过明文传输的 HSTS 头,这也会导致 HSTS Preload 生成一条警告。

如果不打算在域名 Apex 处进行静态内容分发,则可以将请求重定向到他处。这可以通过全局规则来实现。

在 CDN 端点中创建重定向规则与 HSTS 头规则

在启用 HSTS 之前,请务必做好评估与测试,确保域名及子域名中所有的服务都可以长期持续地仅通过 TLS 访问,以免日后造成服务无法访问。

测试并提交申请

在证书与规则均部署完毕后,可以在浏览器中进行测试。因为 HSTS 头的有效期可能很长,因此测试时建议使用一个隐私标签,以免当操作出现失误或测试只有部分成功时,无法再次复现完整路径。

打开浏览器的开发人员工具,切换到 网络 选项卡并开始请求记录。使用 HTTP 协议访问域名的 Apex. 观察开发人员工具中是否记录了 HTTP 到 HTTPS 的重定向,观察 HTTPS 请求是否成功完成,观察 HTTPS 请求的响应中是否包含先前配置的 Strict-Transport-Security 头。

如果没有执行重定向,证书存在问题或响应中没有头,则可能是证书或规则尚未部署完毕,可以稍后重新测试。如果一切正常,则可以向 hstspreload.org 提交申请,将域名添加到 HSTS 数据库中。

检查预加载状态

从提交申请,到 HSTS 数据库中包含新域名的浏览器版本发行,可能需要数周到数月时间。在此期间,有几种方法可以检查 HSTS 预加载的状态。

首先,可以在 HSTS Preload 中输入自己的域名,HSTS Preload 将给出域名正在等待列表或已完成预加载的结论。

HSTS Preload 指示域名已添加到 HSTS 数据库中

然而,HSTS Preload 的完成结论只意味着域名已经进入 Chromium 代码库中的 HSTS 数据库,并不代表域名已经进入了用户使用的浏览器版本的 HSTS 数据库。对于使用 Chromium 内核的浏览器,可以通过访问 net-internals 内部页面进行查询。对于 Chrome 浏览器,该页面地址为 chrome://net-internals/#hsts, 对于新版本的 Edge 则为 edge://net-internals/#hsts. 在页面中 Query HSTS/PKP domain 标题下方的框中输入你的域名,并单击 Query. 观察是否返回了结果,且结果中 static_sts_domain 字段显示了你的域名或其顶级域名。如果是,则说明该浏览器版本的 HSTS 数据库中已经包含了你的域名。

在 net-internals 页面的 HSTS 查询结果表明该版本浏览器的 HSTS 数据库中包含查询的域名

如果希望检查多种浏览器对域名的 HSTS 加载情况,则可以通过 SSL Labs 对域名进行 TLS 测试。HSTS 预加载情况将显示在详细测试结果较靠后处的 HSTS Preloading 字段中。注意这一结果也不意味浏览器的所有版本都在 HSTS 数据库中包含你的域名。

在 SSL Labs 的测试结果表示域名已经在所有被测浏览器的 HSTS 数据库中

完成以上步骤,我们便成功地以极为廉价的方式,为域名启用 HSTS 预加载。