【翻译】Stack Overflow 的 HTTPS 化:漫漫长
原文地址:HTTPS on Stack Overflow: The End of a Long Road
作者:Nick Craver
译者: 罗晟 & 狄敬超
前文地址:
【翻译】Stack Overflow 的 HTTPS 化:漫漫长路的终点(一)
【翻译】Stack Overflow 的 HTTPS 化:漫漫长路的终点(二)
混合内容:来自于你们
混合内容是个筐,什么都能往里装。我们这些年下来积累了哪些混合内容呢?不幸的是,有很多。这个列表里我们必须处理的用户提交内容:
-
http://
图片,出现在问题、答案、标签、wiki 等内容中使用 -
http://
头像 -
http://
头像,出现在聊天中(站点侧边栏) -
http://
图片,出现于个人资料页的「关于我」部分 -
http://
图片,出现于帮助中心的文章中 -
http://
YouTube 视频(有些站点启用了,比如 gaming.stackexchange.com) -
http://
图片,出现于特权描述中 -
http://
图片,出现于开发者故事中 -
http://
图片,出现于工作描述中 -
http://
图片,出现于公司页面中 -
http://
源地址,出现在 JavaScript 代码中.
上面的每一个都带有自己独有的问题,我仅仅会覆盖一下值得一提的部分。注意:我谈论的每一个解决方案都必须扩展到我们这个架构下的几百个站点和数据库上。
在上面的所有情况中(除了代码片段),要消除混合内容的第一步工作就是:你必须先消除新的混合内容。否则,这个清理过程将会无穷无尽。要做到这一点,我们开始全网强制仅允许内嵌 https://
图片。一旦这个完成之后,我们就可以开始清理了。
对于问题、答案以及其他帖子形式中,我们需要具体问题具体分析。我们先来搞定 90% 以上的情况:stack.imgur.com
。在我来之前 Stack Overflow 就已经有自己托管的 Imgur 实例了。你在编辑器中上传的图片就会传到那里去。绝大部分的帖子都是用的这种方法,而他们几年前就为我们添加了 HTTPS 支持。所以这个就是一个很直接的查找替换(我们称为帖子 markdown 重处理)。
然后我们通过通过 Elasticsearch 对所有内容的索引来找出所有剩下的文件。我说的我们其实指的是 Samo。他在这里处理了大量的混合内容工作。当我们看到大部分的域名其实已经支持 HTTPS 了之后,我们决定:
- 对于每个
<img>
的源地址都尝试替换成https://
。如果能正常工作则替换帖子中的链接 - 如果源地址不支持
https://
,将其转一个链接
当然,并没有那么顺利。我们发现用于匹配 URL 的正则表达式其实已经坏了好几年了,并且没有人发现……所以我们修复了正则,重新做了索引。
有人问我们:「为什么不做个代理呢?」呃,从法律和道德上来说,代理对我们的内容来说是个灰色地带。比如,我们 photo.stackexchange.com 上的摄像师会明确声明不用 Imgur 以保留他们的权利。我们充分理解。如果我们开始代理并缓存全图,这在法律上有点问题。我们后来发现在几百万张内嵌图片中,只有几千张即不支持 https://
也没有 404 失效的。这个比例(低于 1%)不足于让我们去搭一个代理。
我们确实研究过搭一个代理相关的问题。费用有多少?需要多少存储?我们的带宽足够吗?我们有了一个大体上的估算,当然有点答案也不是很确定。比如我们是否要用 Fastly,还是直接走运营商?哪一种比较快?哪一种比较便宜?哪一种可以扩展?这个足够写另一篇博客了,如果你有具体问题的话可以在评论里提出,我会尽力回答。
所幸,在这个过程中,为了解决几个问题,balpha 更改了用 HTML5 嵌入 YouTube 的方式。我们也就顺便强制了一下 YouTube 的 https://
嵌入。
剩下的几个内容领域的事情差不多:先阻止新的混合内容进来,再替换掉老的。这需要我们在下面几个领域进行更改:
- 帖子
- 个人资料
- 开发故事
- 帮助中心
- 职场
- 公司业务
声明:JavaScript 片段的问题仍然没有解决。这个有点难度的原因是:
- 资源有可能不以
https://
的方式存在(比如一个库) - 由于这个是 JavaScript,你可以自己构建出任意的 URL。这里我们就无力检查了。
- 如果你有更好的方式来处理这个问题,请告诉我们。我们在可用性与安全性上不可兼得。
混合内容:来自我们
并不是处理完用户提交的内容就解决问题了。我们自己还是有不少 http://
的地方需要处理。这些更改本身没什么特别的,但是这至少能解答「为什么花了那么长时间?」这个问题:
- 广告服务(Calculon)
- 广告服务(Adzerk)
- 标签赞助商
- JavaScript 假定
- Area 51(这代码库也太老了)
- 分析跟踪器(Quantcast, GA)
- 每个站点引用的 JavaScript(社区插件)
-
/jobs
下的所有东西(这其实是个代理) - 用户能力
- ……还有代码中所有出现
http://
的地方
JavaScript 和链接比较令人痛苦,所以我在这里稍微提一下。
JavaScript 是一个不少人遗忘的角落,但这显然不能被无视。我们不少地方将主机域名传递给 JavaScript 时假定它是 http://
,同时也有不少地方写死了 meta 站里的 meta.
前缀。很多,真的很多,救命。还好现在已经不这样了,我们现在用服务器渲染出一个站点,然后在页面顶部放入相应的选择:
StackExchange.init({
"locale":"en",
"stackAuthUrl":"https://stackauth.com",
"site":{
"name":"Stack Overflow"
"childUrl":"https://meta.stackoverflow.com",
"protocol":"http"
},
"user":{
"gravatar":"<div class=\"gravatar-wrapper-32\"><img src=\"https://i.stack.imgur.com/nGCYr.jpg\"></div>",
"profileUrl":"https://stackoverflow.com/users/13249/nick-craver"
}
});
这几年来我们在代码里也用到了很多静态链接。比如,在页尾,在页脚,在帮助区域……到处都是。对每一个来说,解决方式都不复杂:把它们改成 <site>.Url("/path")
的形式就好了。不过要找出这些链接有点意思,因为你不能直接搜 "http://"
。感谢 W3C 的丰功伟绩:
<svg xmlns="http://www.w3.org/2000/svg"...
是的,这些是标识符,是不能改的。所以我希望 Visual Studio 在查找文件框中增加一个「排除文件类型」的选项。Visual Studio 你听见了吗?VS Code 前段时间就加了这个功能。我这要求不过分。
这件事情很枯燥,就是在代码中找出一千个链接然后替换而已(包括注释、许可链接等)。但这就是人生,我们必须要做。把这些链接改成 .Url()
的形式之后,一旦站点支持 HTTPS 的时候,我们就可以让链接动态切换过去。比如我们得等到 meta.*.stackexchange.com
搬迁完成之后再进行切换。插播一下我们数据中心的密码是「煎饼馃子」拼音全称,应该没有人会读到这里吧,所以在这里存密码很安全。当站点迁完之后,.Url()
仍会正常工作,然后用 .Url()
来渲染默认为 HTTPS 的站点也会继续工作。这将静态链接变成了动态。
另一件重要的事情:这让我们的开发和本地环境都能正常工作,而不仅仅是链到生产环境上。这件事情虽然枯燥,但还是值得去做的。对了,因为我们的规范网址(canonical)也通过 .Url()
来做了,所以一旦用户开始用上 HTTPS,Google 也可以感知到。
一旦一个站点迁到 HTTPS 之后,我们会让爬虫来更新站点链接。我们把这个叫修正「Google 果汁」,同时这也可以让用户不再碰到 301。
跳转(301)
当你把站点移动到 HTTPS 之后,为了和 Google 配合,你有两件重要的事情要做:
- 更新规范网址,比如
<link rel="canonical" href="https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454" />
- 把
http://
链接通过 301 跳转至https://
这个不复杂,也不是浩大的工程,但这非常非常重要。Stack Overflow 大部分的流量都是从 Google 搜索结果中过来的,所以我们得保证这个不产生负面影响。这个是我们的生计,如果我们因此丢了流量那我真是要失业了。还记得那些 .internal
的 API 调用吗?对,我们同样不能把所有东西都进行跳转。所以我们在处理跳转的时候需要一定的逻辑(比如我们也不能跳转 POST
请求,因为浏览器处理得不好),当然这个处理还是比较直接的。这里是实际上用到的代码:
public static void PerformHttpsRedirects()
{
var https = Settings.HTTPS;
// If we're on HTTPS, never redirect back
if (Request.IsSecureConnection) return;
// Not HTTPS-by-default? Abort.
if (!https.IsDefault) return;
// Not supposed to redirect anyone yet? Abort.
if (https.RedirectFor == SiteSettings.RedirectAudience.NoOne) return;
// Don't redirect .internal or any other direct connection
// ...as this would break direct HOSTS to webserver as well
if (RequestIPIsInternal()) return;
// Only redirect GET/HEAD during the transition - we'll 301 and HSTS everything in Fastly later
if (string.Equals(Request.HttpMethod, "GET", StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(Request.HttpMethod, "HEAD", StringComparison.InvariantCultureIgnoreCase))
{
// Only redirect if we're redirecting everyone, or a crawler (if we're a crawler)
if (https.RedirectFor == SiteSettings.RedirectAudience.Everyone
|| (https.RedirectFor == SiteSettings.RedirectAudience.Crawlers && Current.IsSearchEngine))
{
var resp = Context.InnerHttpContext.Response;
// 301 when we're really sure (302 is the default)
if (https.RedirectVia301)
{
resp.RedirectPermanent(Site.Url(Request.Url.PathAndQuery), false);
}
else
{
resp.Redirect(Site.Url(Request.Url.PathAndQuery), false);
}
Context.InnerHttpContext.ApplicationInstance.CompleteRequest();
}
}
}
注意我们并不是默认就跳 301(有一个 .RedirectVia301
设置),因为我们做一些会产生永久影响的事情之前必须仔细测试。我们会晚一点来讨论 HSTS 以及后续影响。
Websockets
这一块会过得快一点。Websocket 不难,从某种角度来说,这是我们做过的最简单的事情。我们用 websockets 来处理实时的用户影响力变化、收件箱通知、新问的问题、新增加的答案等等。这也就说基本上每开一个 Stack Overflow 的页面,我们都会有一个对应的 websocket 连接连到我们的负载均衡器上。
所以怎么改呢?其实很简单:安装一个证书,监听 :443
端口,然后用 wss://qa.sockets.stackexchange.com
来代替 ws://
。后者其实早就做完了(我们用了一个专有的证书,但是这不重要)。从 ws://
到 wss://
只是配置一下的问题。一开始我们还用 ws://
作为 wss://
的备份方案,不过后来就变成仅用 wss://
了。这么做有两个原因:
- 不用的话在
https://
下面会有混合内容警告 - 可以支持更多用户。因为很多老的代理不能很好地处理 websockets。如果使用加密流量,大多数代理就只是透传而不会弄乱流量。对移动用户来说尤其是这样。
最大的问题就是:「我们能处理了这个负载吗?」我们全网处理了不少并发 websocket,在我写这估的时候我们有超过 600000 个并发的连接。这个是我们 HAProxy 的仪表盘在 Opserver 中的界面:
HAProxy Websockets不管是在终端、抽象命名空间套接字还是前端来说都有很多连接。由于启用了 TLS 会话恢复,HAProxy 本身的负载也很重。要让用户下一次重新连接更快,第一次协商之后用户会拿到一个令牌,下一次会把这个令牌发送过来。如果我们的内存足够并且没有超时,我们会恢复上次的会话而不是再开一个。这个操作可以节省 CPU,对用户来说有性能提升,但会用到到更多内存。这个多因 key 大小而异(2048,4096 或是更多?)我们现在用的是 4096 位的 key。在开了 600000 个 websocket 的情况下,我们只用掉了负载均衡器 64GB 内存里的 19GB。这里面 12GB 是 HAProxy 在用,大多数为 TLS 会话缓存。所以结果来说还不错,如果我们不得不买内存的话,这也会是整个 HTTPS 迁移中最便宜的东西。
HAProxy Websocket Memory未知
我猜现在可能是我们来谈论一些未知问题的时候。有些问题是在我们尝试之前无法真正知道的:
- Google Analytics 里的流量表现怎么样?(我们会失去 referer 吗?)
- Google Webmasters 的转换是否平滑?(301 生效吗?规范域名呢?要多长时间?)
- Google 搜索分析会怎么工作(我们会在搜索分析中看到
https://
吗?) - 我们搜索排名会下降吗?(最恐怖的)
有很多人都谈过他们转化成 https://
的心得,但对我们却有点不一样。我们不是一个站点。我们是多个域名下的多个站点。我们不知道 Google 会怎么对待我们的网络。它会知道 stackoverflow.com
和 superuser.com
有关联吗?不知道。我们也不能指望 Google 来告诉我们这些。
所以我们就做测试。在我们全网发布 中,我们测试了几个域名:
对,这些是 Samo 和我会了仔细讨论出来的结果,花了有三分钟那么久吧。Meta 是因为这是我们最重要的反馈网站。Security 站上有很多专家可能会注意到相关的问题,特别是 HTTPS 方面。最后一个,Super User,我们需要知道搜索对我们内容的影响。比起 meta 和 security 来说法,Super User 的流量要大得多。最重要的是,它有来自 Google 的原生流量。
我们一直在观察并评估搜索的影响,所以 Super User 上了之后其他网站过了很久才跟上。到目前为止我们能说的是:基本上没影响。搜索、结果、点击还有排名的周变化都在正常范围内。我们公司依赖于这个流量,这对我们真的很重要。所幸,没有什么值得我们担心的点,我们可以继续发布。
错误
如果不提到我们搞砸的部分,这篇文章就还不够好。错误永远是个选择。让我们来总结一下这一路让我们后悔的事情:
错误:相对协议 URL
如果你的一个资源有一个 URL 的话,一般来说你会看到一些 http://example.com
或者 https://example.com
之类的东西,包括我们图片的路径等等。另一个选项就是你可以使用 //example.com
。这被称为相对协议 URL。我们很早之前就在图片、JavaScript、CSS 等中这么用了(我们自有的资源,不是指用户提交)。几年后,我们发现这不是一个好主意,至少对我们来说不是。相对协议链接中的「相对」是对于页面而言。当你在 http://stackoverflow.com
时,//example.com
指的是 http://example.com
;如果你在 https://stackoverflow.com
时,就和 https://example.com
等同。那么这个有什么问题呢?
问题在于,图片 URL 不仅是用在页面中,它们还用在邮件、API 还有移动应用中。当我们理了一下路径结构然后在到处都使用图片路径时我们发现不对了。虽然这个变化极大降低了代码冗余,并且简化了很多东西,结果却是我们在邮件中使用了相对 URL。绝大多数邮件客户端都不能处理相对协议 URL 的图片。因为它们不知道是什么协议。Email 不是 http://
也不是 https://
。只有你在浏览器里查看邮件,有可能是预期的效果。
那该怎么办?我们把所有的地方都换成了 https://
。我把我们所有的路径代码统一到两个变量上:CDN 根路径,和对应特定站点的文件夹。例如 Stack Overflow 的样式表在 https://cdn.sstatic.net/Sites/stackoverflow/all.css
上(当然我们有缓存中断器),换成本地就是 https://local.sstatic.net/Sites/stackoverflow/all.css
。你能看出其中的共同点。通过拼接路径,逻辑简单了不少。则 通过强制 https://
,用户还可以在整站切换之前就享受 HTTP/2 的好处,因为所有静态资源都已经就位。都用 https://
也表示我们可以在页面、邮件、移动还有 API 上使用同一个属性。这种统一也意味着我们有一个固定的地方来处理所有路径——我们到处都有缓存中断器。
注意:如果你像我们一样中断缓存,比如 https://cdn.sstatic.net/Sites/stackoverflow/all.css?v=070eac3e8cf4
,请不要用构建号。我们的缓存中断使用的是文件的校验值,也就是说只有当文件真正变化的时候你才会下载一个新的文件。用构建号的话可能会稍微简单点,但同时也会对你的费用还有性能有所损伤。
能做这个当然很好,可我们为什么不从一开始就做呢?因为 HTTPS 在那个时候性能还不行。用户通过 https://
访问会比 http://
慢很多。举一个大一点的例子:我们上个月在 sstatic.net
上收到了四百万个请求,总共有 94TB。如果 HTTPS 性能不好的话,这里累积下来的延迟就很可观了。不过因为我们上了 HTTP/2,以及设置好 CDN/代理层,性能的问题已经好很多了。对于用户来说更快了,对我们来说则更简单,何乐不为呢!
错误:API 及 .internal
当我们把代理架起来开始测试的时候发现了什么?我们忘了一件很重要的事,准确地说,我忘了一件很重要的事。我们在内部 API 里大量地使用了 HTTP。当然这个是正常工作的,只是它们变得更慢、更复杂、也更容易出问题了。
比方说一个内部 API 需要访问 stackoverflow.com/some-internal-route
,之前,节点是这些:
- 原始 app
- 网关/防火墙(暴露给公网)
- 本地负载均衡器
- 目标 web 服务器
这是因为我们是可以解析 stackoverflow.com
的,解析出来的 IP 就是我们的负载均衡器。当有代理的情况下,为了让用户能访问到最近的节点,他们访问到的是不同的 IP 和目标点。他们的 DNS 解析出来的 IP 是 CDN/代理层 (Fastly)。糟了,这意识着我们现在的路径是这样的:
- 原始 app
- 网关/防火墙(暴露给公网)
- 我们的外部路由器
- 运营商(多节点)
- 代理(Cloudflare/Fastly)
- 运营商(到我们的代理路)
- 我们的外部路由器
- 本地负载均衡器
- 目标 web 服务器
嗯,这个看起来更糟了。为了实现一个从 A 调用一下 B,我们多了很多不必要的依赖,同时性能也下降了。我不是说我们的代理很慢,只是原本只需要 1ms 就可以连到我们数据中心……好吧,我们的代理很慢。
我们内部讨论了多次如何用最简单的方法解决这个问题。我们可以把请求改成 internal.stackoverflow.com
,但是这会产生可观的修改(也许也会产生冲突)。我们也创建一个 DNS 来专门解析内部地址(但这样会产生通配符继承的问题)。我们也可以在内部把 stackoverflow.com
解析成不同的地址(这被称为水平分割 DNS),但是这一来不好调试,二来在多数据中心的场景下不知道该到哪一个。
最终,我们在所有暴露给外部 DNS 的域名后面都加了一个 .internal
后续。比如,在我们的网络中,stackoverflow.com.internal
会解析到我们的负载均衡器后面(DMZ)的一个内部子网内。我们这么做有几个原因:
- 我们可以在内部的 DNS 服务器里覆盖且包含一个顶级域名服务器(活动目录)
- 当请求从 HAProxy 传到 web 应用中时,我们可以把
.internal
从Host
头中移除(应用层无感知) - 如果我们需要内部到 DMZ 的 SSL,我们可以用一个类似的通配符组合
- 客户端 API 的代码很简单(如果在域名列表中就加一个
.internal
)
我们客户端的 API 代码是大部分是由 Marc Gravell 写的一个 StackExchange.Network
的 NuGet 库。对于每一个要访问的 URL,我们都用静态的方法调用(所以也就只有通用的获取方法那几个地方)。如果存在的话就会返回一个「内部化」URL,否则保持不变。这意味着一次简单的 NuGet 更新就可以把这个逻辑变化部署到所有应用上。这个调用挺简单的:
uri = SubstituteInternalUrl(uri);
这里是 stackoverflow.com
DNS 行为的一个例子:
- Fastly:151.101.193.69, 151.101.129.69, 151.101.65.69, 151.101.1.69
- 直连(外部路由):198.252.206.16
- 内部:10.7.3.16
记得我们之前提到的 dnscontrol 吗?我们可以用这个快速同步。归功于 JavaScript 的配置/定义,我们可以简单地共享、简化代码。我们匹配所有所有子网和所有数据中心中的所有 IP 的最后一个字节,所以用几个变量,所有 AD 和外部的 DNS 条目都对齐了。这也意味着我们的 HAProxy 配置更简单了,基本上就是这样:
stacklb::external::frontend_normal { 't1_http-in':
section_name => 'http-in',
maxconn => $t1_http_in_maxconn,
inputs => {
"${external_ip_base}.16:80" => [ 'name stackexchange' ],
"${external_ip_base}.17:80" => [ 'name careers' ],
"${external_ip_base}.18:80" => [ 'name openid' ],
"${external_ip_base}.24:80" => [ 'name misc' ],
综上,API 路径更快了,也更可靠了:
- 原始 app
- 本地负载均衡器(DMZ)
- 目标 web 服务器
我们解决了几个问题,还剩下几百个等着我们。
错误:301 缓存
在从 http://
301 跳到 https://
时有一点我们没有意识的是,Fastly 缓存了我们的返回值。在 Fastly 中,默认的缓存键并不考虑协议。我个人不同意这个行为,因为在源站默认启用 301 跳转会导致无限循环。这个问题是这样造成的:
- 用户访问
http://
上的一个网络 - 通过 301 跳转到了
https://
- Fastly 缓存了这个跳转
- 任意一个用户(包括 #1 中的那个)以
https://
访问同一个页面 - Fastly 返回一个跳至
https://
的 301,尽量你已经在这个页面上了
这就是为什么我们会有无限循环。要解决这个问题,我们得关掉 301,清掉 Fastly 缓存,然后开始调查。Fastly 建议我们在 vary 中加入 Fastly-SSL
,像这样:
sub vcl_fetch {
set beresp.http.Vary = if(beresp.http.Vary, beresp.http.Vary ",", "") "Fastly-SSL";
在我看来,这应该是默认行为。
错误:帮助中心的小插曲
记得我们必须修复的帮助文档吗?帮助文档都是按语言区分,只有极少数是按站点来分,所以本来它们是可以共享的。为了不产生大量重复代码及存储结构,我们做了一点小小的处理。我们把实际上的帖子对象(和问题、答案一样)存在了 meta.stackexchange.com
或者是这篇帖子关联的站点中。我们把生成的 HelpPost
存在中心的 Sites
数据库里,其实也就是生成的 HTML。在处理混合内容的时候,我们也处理了单个站里的帖子,简单吧!
当原始的帖子修复后,我们只需要为每个站点去再生成 HTML 然后填充回去就行了。但是这个时候我犯了个错误。回填的时候拿的是当前站点(调用回填的那个站点),而不是原始站。这导致 meta.stackexchange.com
里的 12345 帖子被 stackoverflow.com
里的 12345 帖子所替代。有的时候是答案、有的时候是问题,有的时候有一个 tag wiki。这也导致了一些很有意思的帮助文档。这里有一些相应的后果。
我只能说,还好修复的过程挺简单的:
Me being a dumbass再一次将数据填充回去就能修复了。不过怎么说,这个当时算是在公共场合闹了个笑话。抱歉。
开源
这里有我们在这个过程中产出的项目,帮助我们改进了 HTTPS 部署的工作,希望有一天这些能拯救世界吧:
- BlackBox (在版本控制中安全存储私密信息)作者 Tom Limoncelli
- capnproto-net(不再支持 —— .NET 版本的 Cap’n Proto)作者 Marc Gravell
- DNSControl(控制多个 DNS 提供商)作者 Craig Peterson and Tom Limoncelli
- httpUnit (网站集成测试) 作者 Matt Jibson and Tom Limoncelli
- Opserver (支持 Cloudflare DNS) 作者 Nick Craver
- fastlyctl(Go 语言的 Fastly API 调用)作者 Jason Harvey
- fastly-ratelimit(基于 Fastly syslog 流量的限流方案)作者 Jason Harvey
下一步
我们的工作并没有做完。接下去还有一此要做的:
- 我们要修复我们聊天域名下的混合内容,如 chat.stackoverflow.com,这里有用户嵌入的图片等
- 如果可能的话,我们把所有适用的域名加进 Chrome HSTS 预加载列表
- 我们要评估 HPKP 以及我们是否想部署(这个很危险,目前我们倾向于不部署)
- 我们需要把聊天移到
https://
- 我们需要把所有的 cookies 迁移成安全模式
- 我们在等能支持 HTTP/2 的 HAProxy 1.8(大概在九月出来)
- 我们需要利用 HTTP/2 的推送(我会在六月与 Fastly 讨论这件事情——他们还现在不支持跨域名推送)
- 我们需要把 301 行为从 CDN/代理移出以达到更好的性能(需要按站点发布)
HSTS 预加载
HSTS 指的是「HTTP 严格传输安全」。OWASP 在这里有一篇很好的总结。这个概念其实很简单:
- 当你访问
https://
页面的时候,我们给你发一个这样的头部:Strict-Transport-Security: max-age=31536000
- 在这个时间内(秒),你的浏览器只会通过
https://
访问这个域名
哪怕你是点击一个 http://
的链接,你的浏览器也会直接跳到 https://
。哪怕你有可能已经设置了一个 http://
的跳转,但你的浏览器不会访问,它会直接访问 SSL/TLS。这也避免了用户访问不安全的 http://
而遭到劫持。比如它可以把你劫持到一个 https://stack<长得很像o但实际是个圈的unicode>verflow.com
上,那个站点甚至有可能部好了 SSL/TLS 证书。只有不访问这个站点才是安全的。
但这需要我们至少访问一次站点,然后才能有这个头部,对吧?对。所以我们有 HSTS 预加载,这是一个域名列表,随着所有主流浏览器分发且由它们预加载。也就是说它们在第一次访问的时候就会跳到 https://
去,所以永远不会有任何 http://
通信。
很赞吧!所以要怎么才能上这个列表呢?这里是要求:
- 要有一个有效的证书
- 如果你监听 80 端口的话,HTTP 应该跳到同一个主机的 HTTPS 上
- 所有子域名都要支持 HTTPS
- 特别是如果有 DNS 纪录的话,www 子域名要支持 HTTPS
- 主域名的 HSTS 头必要满足如下条件:
- max-aget 至少得是十八周(10886400 秒)
- 必须有 includeSubDomains 指令
- 必须指定 preload 指令
- 如果你要跳转到 HTTPS 站点上,跳转也必须有 HSTS 头部(而不仅仅是跳过去的那个页面)
这听起来还行吧?我们所有的活跃域名都支持 HTTPS 并且有有效的证书了。不对,我们还有一个问题。记得我们有一个 meta.gaming.stackexchange.com
吧,虽然它跳到 gaming.meta.stackexchange.com
,但这个跳转本身并没有有效证书。
以 meta 为例,如果我们在 HSTS 头里加入 includeSubDomains
指令,那么网上所有指向旧域名的链接都会踩坑。它们本该跳到一个 http:///
站点上(现在是这样的),一旦改了就会变成一个非法证书错误。昨天我们看了一下流量日志,每天仍有 8 万次访问的是通过 301 跳到 meta 子域上的。这里有很多是爬虫,但还是有不少人为的流量是从博客或者收藏夹过来的……而有些爬虫真的很蠢,从来不根据 301 来更新他们的信息。嗯,你还在看这篇文章?我自己写着写着都已经睡着 3 次了。
我们该怎么办呢?我们是否要启用 SAN 证书,加入几百个域名,然后调整我们的基础架构使得 301 跳转也严格遵守 HTTPS 呢?如果要通过 Fastly 来做的话就会提升我们的成本(需要更多 IP、证书等等)。Let’s Encrypt 倒是真的能帮上点忙。获取证书的成本比较低,如果你不考虑设置及维护的人力成本的话(因为我们由于上文所述内容并没有在使用它).
还有一块是上古遗留问题:我们内部的域名是 ds.stackexchange.com
。为什么是 ds.
?我不确定。我猜可能是我们不知道怎么拼 data center 这个词。这意味着 includeSubDomains
会自动包含所有内部终端。虽然我们大部分都已经上了 https://
,但是如果什么都走 HTTPS 会导致一些问题,也会带来一定延时。不是说我们不想在内部也用 https://
,只不过这是一个整体的项目(大部分是证书分发和维护,还有多级证书),我们不想增加耦合。那为什么不改一下内部域名呢?主要还是时间问题,这一动迁需要大量的时间和协调。
目前,我们将 HSTS 的 max-age
设为两年,并且不包括 includeSubDomains
。除非迫不得以,我不会从代码里移除这个设定,因为它太危险了。一旦我们把所有 Q&A 站点的 HSTS 时间都设置好之后,我们会和 Google 聊一下是不是能在不加 includeSubDomains
的情况下把我们加进 HSTS 列表中,至少我们会试试看。你可以看到,虽然很罕见,但目前的这份列表中还是出现了这种情况的。希望从加强 Stack Overflow 安全性的角度,他们能同意这一点。
聊天
为了尽快启用 安全
cookie(仅在 HTTPS 下发送),我们会将聊天(chat.stackoverflow.com、[chat.stackexchange.com 及 chat.meta.stackexchange.com)跳转至 https://
。 正如我们的通用登录所做的那样,聊天会依赖于二级域名下的 cookie。如果 cookie 仅在 https://
下发送,你就只能在 https://
下登录。
这一块有待斟酌,但其实在有混合内容的情况下将聊天迁至 https://
是一件好事。我们的网络更加安全了,而我们也可以处理实时聊天中的混合内容。希望这个能在接下去的一两周之内实施,这在我的计划之中。
今天
不管怎么说,这就是我们今天到达的地步,也是我们过去四年中一直在做的事情。确实有很多更高优先级的事情阻挡了 HTTPS 的脚步——这也远远不是我们唯一在做的事情。但这就是生活。做这件事情的人们还在很多你们看不见的地方努力着,而涉及到的人也远不止我所提到的这些。在这篇文章中我只提到了一些花了我们很多时间的、比较复杂的话题(否则就会太长了),但是这一路上不管是 Stack Overflow 内部还是外部都有很多人帮助过我们。
我知道你们会有很多的疑问、顾虑、报怨、建议等等。我们非常欢迎这些内容。本周我们会关注底下的评论、我们的 meta 站、Reddit、Hacker News 以及 Twitter,并尽可能地回答/帮助你们。感谢阅读,能全文读下的来真是太棒了。(比心)