原文地址:HTTPS on Stack Overflow: The End of a Long Road
作者:Nick Craver
译者: 罗晟 & 狄敬超
今天,我们默认在 Stack Overflow 上部署了 HTTPS。目前所有的流量都将跳转到 https://
上。与此同时,Google 链接也会在接下去的几周内更改。启用的过程本身只是举手之劳,但在此之前我们却花了好几年的时间。到目前为止,HTTPS 在我们所有的 Q&A 网站上都默认启用了。
在过去的两个月里,我们在 Stack Exchange 全网维持发布 HTTPS。Stack Overflow 是最后,也是迄今最大的的一个站点。这对我们来说是一个巨大里程碑,但决不意味着是终点。后文会提到,我们仍有很多需要做的事情。但现在我们总算能看得见终点了,耶!
友情提示:这篇文章讲述的是一个漫长的旅程。非常漫长。你可能已经注意到你的滚动条现在非常小。我们遇到的问题并不是只在 Stack Exchange/Overflow 才有,但这些问题的组合还挺罕见。我在文章中会讲到我们的一些尝试、折腾、错误、成功,也会包括一些开源项目——希望这些细节对你们有所帮助。由于它们的关系错综复杂,我难以用时间顺序来组织这篇文章,所以我会将文章拆解成架构、应用层、错误等几个主题。
首先,我们要提一下为什么我们的处境相对独特:
- 我们有几百个域名(大量站点及服务)
- 大量二级域名(stackoverflow.com、stackexchange.com、askubuntu.com等)
- 大量四级域名(如 meta.gaming.stackexchange.com)
- 我们允许用户提交、嵌入内容(比如帖子中的图片和 YouTube 视频)
- 我们仅有一个数据中心(造成单源的延时)
- 我们有广告(及广告网络)
- 我们用 websockets,任何时刻的活跃数都不少于 50 万个(连接数问题)
- 我们会被 DDoSed 攻击(代理问题)
- 我们有不少站点及应用还是通过 HTTP API 通信的(代理问题)
- 我们热衷于性能(好像有点太过了)
开篇
我们早在 2013 年就开始考虑在 Stack Overflow 上部署 HTTPS 了。是的,现在是 2017 年。所以,究竟是什么拖了我们四年?这个问题的答案放在任何一个 IT 项目上都适用:依赖和优先级。老实说,Stack Overflow 在信息安全性上的要求并不像别家那么高。我们不是银行,也不是医院,我们也不涉及信用卡支付,甚至于我们每个季度都会通过 HTTP 和 BT 种子的方式发布我们大部分的数据库。这意味着,从安全的角度来看,这件事情的紧急程度不像它在其他领域里那么高。而从依赖上来说,我们的复杂度比别人要高,在部署 HTTPS 时会在几大领域里踩坑,这些问题的组合是比较特殊的。后文中会看到,有一些域名的问题还是一直存在的。
容易踩坑的几个领域包括:
- 用户内容(用户可以上传图片或者指定 URL)
- 广告网络(合同及支持)
- 单数据中心托管(延迟)
- 不同层级下的几百个域名(证书)
那我们究竟是为什么需要 HTTPS 呢?因为数据并不是唯一需要安全性的东西。我们的用户中有操作员、开发者、还有各个级别的公司员工。我们希望他们到我们站点的通信是安全的。我们希望每一个用户的浏览历史是安全的。某些用户暗地里喜欢 monad 却又害怕被人发现。同时,Google 会提升 HTTPS 站点的搜索排名(虽然我们不知道能提升多少)。
哦,还有性能。我们热爱性能。我热爱性能。你热爱性能。我的狗热爱性能。让我给你一个性能的拥抱。很好。谢谢。你闻起来很香。
懒人包
很多人喜欢情人包,所以我们来一场快速问答(我们喜欢问答!):
- 问:你们支持什么协议?
- 答:TLS 1.0、1.1、1.2(注意:Fastly 准备放弃 TLS 1.0 和 1.1)。我们马上也会支持 TLS 1.3。
- 问:你们支持 SSL v2 或者 v3 吗?
- 答:不支持。这些协议不安全。大家都应该尽早禁用。
- 问:你们支持哪些加密套件?
- 答:CDN 上,我们用的是 Fastly 的默认套件;
- 答:我们自己的负载均衡器上用的是 Mozilla 的现代兼容性套件。
- 问:Fastly 回源走的是 HTTPS 吗?
- 答:是。如果到 CDN 的请求是 HTTPS,回源的请求也是 HTTPS。
- 问:你们支持前向安全性吗?
- 答:是。
- 问:你们支持 HSTS 吗?
- 答:支持。我们正在 Q&A 站点中逐步支持。一旦完成的话我们就会将其移至节点上。
- 问:你们支持 HPKP 吗?
- 答:不支持,应该也不会支持。
- 问:你们支持 SNI 吗?
- 答:不支持。出于 HTTP/2 性能考虑,我们使用是一个合并的通配符证书(详见后文)。
- 问:你们的证书是哪来的?
- 答:我们用的是 DigiCert,他们很棒。
- 问:你们支持 IE 6 吗?
- 答:这次之后终于不再支持了。IE 6 默认不支持 TLS(尽管你可以启用 1.0 的支持),而我们则不支持 SSL。当我们 301 跳转就绪的时候大部分 IE 6 用户就不能访问 Stack Overflow 了。一旦我们弃用 TLS 1.0,所有 IE 6 用户都不行了。
- 问:你们负载均衡器用的什么?
- 问:使用 HTTPS 的动机是什么?
- 答:有人一直攻击我们的管员员路由,如 stackoverflow.com/admin.php。
证书
让我们先聊聊证书,因为这是最容易被误解的部分。不少朋友跟我说,他安装了 HTTPS 证书,因此他们已经完成 HTTPS 准备了。呵呵,麻烦你看一眼右侧那个小小的滚动条,这篇文章才刚刚开始,你觉得真的这么简单么?我有这个必要告诉你们一点人生的经验 :没这么容易的。
一个最常见的问题是:「为何不直接用 Let’s Encrypt?」
答案是:这个方案不适合我们。 Let's Encrypt 的确是一个伟大的产品,我希望他们能够长期服务于大家。当你只有一个或少数几个域名时,它是非常出色的选择。但是很可惜,我们 Stack Exchange 有数百个站点,而 Let’s Encrypt 并不支持通配域名配置。这导致 Let's Encrypt 无法满足我们的需求。要这么做,我们就不得不在每上一个新的 Q&A 站点的时候都部署一个(或两个)证书。这样会增加我们部署的复杂性,并且我们要么放弃不支持 SNI 的客户端(大约占 2% 的流量)要么提供超多的 IP——而我们目前没这么多的 IP。
我们之所以想控制证书,还有另外一个原因是我们想在本地负载均衡器以及 CDN / 代理提供商那边使用完成相同的证书。如果不做到这个,我们无法顺畅地做从代理那里做失效备援(failover)。支持 HTTP 公钥固定(HPKP)的客户端会报认证失败。虽然我们仍在评估是否使用 HPKP,但是如果有一天要用的话我们得提前做好准备。
很多朋友在看见我们的主证书时候会吓得目瞪口呆,因为它包含了我们的主域名和通配符子域名。它看上去长成这样:
为什么这么做?老实说,是我们让 DigiCert 替我们做的。这么做会导致每次发生变化的时候都需要手动合并证书,了 我们为什么要忍受这么麻烦的事呢?首先,我们期望能够尽可能让更多用户使用我们产品。这里面包括了那些还不支持 SNI 的用户(比如在我们项目启动的时候 Android 2.3 势头正猛)。另外,也包括 HTTP/2 与一些现实问题——我们过会儿会谈到这一块。
Meta 子域(meta.*.stackexcange.com)
Stack Exchage 的一个设计理念是,针对每个 Q&A 站点,我们都有一个地方供讨论。我们称之为 “second place”。比如 meta.gaming.stackexchange.com
用来讨论 gaming.stackexchange.com
。这个有什么特别之处呢?好吧,并没有,除了域名:这是一个 4 级域名。
我之前已经说过这个问题,但后来怎么样了呢?具体来说,现在面临的问题是 *.stackexchange.com
包含 gaming.stackexchange.com
(及几百个其它站点),但它并不包含 meta.gaming.stackexchange.com
。RFC 6125 (第 6.4.3 节) 写道:
客户端 不应该 尝试匹配一个通配符在中间的域名(比如,不要匹配
bar.*.example.net
)
这意味着我们无法使用 meta.*.stackexchange.com
,那怎么办呢?
- 方案一:部署 SAN 证书(多域名证书)
- 我们需要准备 3 个证书和 3 个 IP(每张证书支持域名上限是 100),并且会把新站上线复杂化(虽然这个机制已经改了)
- 我们要在 CDN/代理层上部署三个自定义证书
- 我们要给
meta.*
这种形式的域名配置额外的 DNS 词条- 根据 DNS 规则,我们必须给每个这样的站点配置一条 DNS,无法批量配置,从而提高了新站上线和维护代理的成本
- 方案二:将所有域名迁移到
*.meta.stackexchange.com
- 方案三:啥都不做,放弃
- 这个方案最简单,然而这是假方案
我们部署了 全局登录系统,然后将子 meta 域名用 301 重定向到新地址,比如 https://gaming.meta.stackexchange.com。做完这个之后我们才意识到,因为这些域名曾经存在过,所以对于 HSTS 预加载来说是个很大的问题。这件事情还在进行中,我会在文章最后面讨论这个问题。这类问题对于 meta.pt.stackoverflow.com
等站点也存在,不过还好我们只有四个非英语版本的 Stack Overflow,所以问题没有被扩大。
对了,这个方案本身还存在另一个问题。由于将 cookies 移动到顶级目录,然后依赖于子域名对其的继承,我们必须调整一些其他域名。比如,在我们新系统中,我们使用 SendGrid 来发送邮件(进行中)。我们从 stackoverflow.email
这个域名发邮件,邮件内容里的链接域名是 sg-links.stackoverflow.email
(使用 CNAME 管理),这样你的浏览器就不会将敏感的 cookie 发出去。如果这个域名是 links.stackoverflow.com
,那么你的浏览器会将你在这个域名下的 cookie 发送出去。 我们有不少虽然使用我们的域名,但并不属于我们自己的服务。这些子域名都需要从我们受信的域名下移走,否则我们就会把你们的 cookie 发给非我们自有的服务器上。如果因为这种错误而导致 cookie 数据泄露,这将是件很丢人的事情。
我们有试过通过代理的方式来访问我们的 Hubspot CRM 网站,在传输过程中可以将 cookies 移除掉。但是很不幸 Hubspot 使用 Akamai,它会判定我们的 HAProxy 实例是机器人,并将其封掉。头三次的时候还挺有意思的……当然这也说明这个方式真的不管用。我们后来再也没试过了。
你是否好奇为什么 Stack Overflow 的博客地址是 https://stackoverflow.blog/?没错,这也是出于安全目的。我们把博客搭在一个外部服务上,这样市场部门和其他团队能够更便利地使用。正因为这样,我们不能把它放在有 cookie 的域名下面。
上面的方案会牵涉到子域名,引出 HSTS 预加载 和 includeSubDomains
命令问题,我们一会来谈这块内容。
性能:HTTP/2
很久之前,大家都认为 HTTPS 更慢。在那时候也确实是这样。但是时代在变化,我们说 HTTPS 的时候不再是单纯的 HTTPS,而是基于 HTTPS 的 HTTP/2。虽然 HTTP/2 不要求加密,但事实上却是加密的。主流浏览器都要求 HTTP/2 提供加密连接来启用其大部分特性。你可以来说 spec 或者规定上不是这么说的,但浏览器才是你要面对的现实。我诚挚地期望这个协议直接改名叫做 HTTPS/2,这样也能给大家省点时间。各浏览器厂商,你们听见了吗?
HTTP/2 有很多功能上的增强,特别是在用户请求之前可以主动推送资源这点。这里我就不展开了,Ilya Grigorik 已经写了一篇非常不错的文章。我这里简单罗列一下主要优点:
咦?怎么没提到证书呢?
一个很少人知道的特性是,你可以推送内容到不同的域名,只要满足以下的条件:
- 这两个域名需要解析到同一个 IP 上
- 这两个域名需要使用同一张 TLS 证书(看到没!)
让我们看一下我们当前 DNS 配置:
λ dig stackoverflow.com +noall +answer
; <<>> DiG 9.10.2-P3 <<>> stackoverflow.com +noall +answer
;; global options: +cmd
stackoverflow.com. 201 IN A 151.101.1.69
stackoverflow.com. 201 IN A 151.101.65.69
stackoverflow.com. 201 IN A 151.101.129.69
stackoverflow.com. 201 IN A 151.101.193.69
λ dig cdn.sstatic.net +noall +answer
; <<>> DiG 9.10.2-P3 <<>> cdn.sstatic.net +noall +answer
;; global options: +cmd
cdn.sstatic.net. 724 IN A 151.101.193.69
cdn.sstatic.net. 724 IN A 151.101.1.69
cdn.sstatic.net. 724 IN A 151.101.65.69
cdn.sstatic.net. 724 IN A 151.101.129.69
嘿,这些 IP 都是一致的,并且他们也拥有相同的证书!这意味着你可以直接使用 HTTP/2 的服务器推送功能,而无需影响 HTTP/1.1 用户。 HTTP/2 有推送的同时,HTTP/1.1 也有了域名共享(通过 sstatic.net
)。我们暂未部署服务器推送功能,但一切都尽在掌握之中。
HTTPS 是我们实现性能目标的一个手段。可以这么说,我们的主要目标是性能,而非站点安全性。我们想要安全性,但光是安全性不足以让我们花那么多精力来在全网部署 HTTPS。当我们把所有因素都考虑在一起的时候,我们可以评估出要完成这件事情需要付出的巨大的时间和精力。在 2013 年,HTTP/2 还没有扮演那么重要的角色。而现在形势变了,对其的支持也多了,最终这成为了我们花时间调研 HTTPS 的催化剂。
值得注意的是 HTTP/2 标准在我们项目进展时还在持续发生变化。它从 SPDY 演化为 HTTP/2,从 NPN 演化为 ALPN。我们这里不会过多涉及到这部分细节,因为我们并没有为其做太多贡献。我们观望并从中获准,但整个互联网却在推进其向前发展。如果你感兴趣,可以看看 Cloudflare 是怎么讲述其演变的。
HAProxy:支持 HTTPS
我们最早在 2013 年开始在 HAProxy 中使用 HTTPS。为什么是 HAProxy 呢?这是历史原因,我们已经在使用它了,而它在 2013 年 的 1.5 开发版中支持了 HTTPS,并在 2014 年发布了正式版。曾经有段时间,我们把 Nginx 放置在 HAProxy 之前(详情看这里)。但是简单些总是更好,我们总是想着要避免在链路、部署和其他问题上的复杂问题。
我不会探讨太多细节,因为也没什么好说的。HAProxy 在 1.5 之后使用 OpenSSL 支持 HTTPS,配置文件也是清晰易懂的。我们的配置方式如下:
- 跑在 4 个进程上
- 1 个用来做 HTTP/前端处理
- 2-4 个用来处理 HTTPS 通讯
- HTTPS 前端使用 socket 抽象命名空间来连接至 HTTP 后端,这样可以极大减少资源消耗
- 每一个前端或者每一「层」都监听了 :433 端口(我们有主、二级、websockets 及开发环境)
- 当请求进来的时候,我们在请求头上加入一些数据(也会移除掉一些你们发送过来的),再将其转发给 web 层
- 我们使用 Mozilla 提供的加密套件。注意,这和我们 CDN 用的不是同样的套件。
HAProxy 比较简单,这是我们使用一个 SSL 证书来支持 :433 端口的第一步。事后看来,这也只是一小步。
这里是上面描述情况下的架构图,我们马上来说前面的那块云是怎么回事:
CDN/代理层:通过 Cloudflare 和 Fastly 优化延迟
我对 Stack Overflow 架构的效率一直很自豪。我们很厉害吧?仅用一个数据中心和几个服务器就撑起了一个大型网站。不过这次不一样了。尽管效率这件事情很好,但是在延迟上就成了个问题。我们不需要那么多服务器,我们也不需要多地扩展(不过我们有一个灾备节点)。这一次,这就成为了问题。由于光速,我们(暂时)无法解决延迟这个基础性问题。我们听说有人已经在处理这个问题了,不过他们造的时间机器好像有点问题。
让我们用数字来理解延迟。赤道长度是 40000 公里(光绕地球一圈的最坏情况)。光速在真空中是 299,792,458 米/秒。很多人用这个数字,但光纤并不是真空的。实际上光纤有 30-31% 损耗,所以我们的这个数字是:(40,075,000 m) / (299,792,458 m/s * .70) = 0.191s,也就是说最坏情况下绕地球一圈是 191ms,对吧?不对。这假设的是一条理想路径,而实际上两个网络节点的之间几乎不可能是直线。中间还有路由器、交换机、缓存、处理器队列等各种各样的延迟。累加起来的延迟相当可观。
这些和 Stack Overflow 有什么关系呢?云主机的优势出来了。如果你用一家云供应商,你访问到的就是相对较近的服务器。但对我们来说不是这样,你离服务部署在纽约或丹佛(主备模式)越远,延迟就越高。而使用 HTTPS,在协商连接的时候需要一个额外的往返。这还是最好的情况(使用 0-RTT 优化 TLS 1.3)。Ilya Grigorik 的 这个总结 讲的很好。
来说 Cloudflare 和 Fastly。HTTPS 并不是闭门造车的一个项目,你看下去就会知道,我们还有好几个项目在并行。在搭建一个靠近用户的 HTTPS 终端(以降低往返时间)时,我们主要考虑的是:
- 终端 HTTPS 支持
- DDoS 防护
- CDN 功能
- 与直连等同或更优的性能
优化代理层的准备:客户端性能测试
开始正式启用终端链路加速之前,我们需要有性能测试报告。我们在浏览器搭好了一整套覆盖全链路性能数据的测试。 浏览器里可以通过 JavaScript 从 window.performance
取性能耗时。打开你浏览器的审查器,你可以亲手试一下。我们希望这个过程透明,所以从第一天开始就把详细信息放在了 teststackoverflow.com 上。这上面并没有敏感信息,只有一些由页面直接载入的 URI 和资源,以及它们的耗时。每一张记录下来的页面大概长这样:
我们目前对 5% 的流量做性能监控。这个过程没有那么复杂,但是我们需要做的事情包括:
- 把耗时转成 JSON
- 页面加载后上传性能测试数据
- 将性能测试上传给我们后台服务器
- 在 SQL Server 中使用 clustered columnstore 存储数据
- 使用 Bosun (具体是 BosunReporter.NET) 汇集数据
最终的结果是我们有了一份来自于全球真实用户的很好的实时汇总。这些数据可供我们分析、监控、报警,以及用于评估变化。它大概长这样:
幸好,我们有持续的流量来获取数据以供我们决策使用,目前的量级是 50 亿,并且还在增长中。这些数据概览如下:
OK,我们已经把基础工作准备好了,是时候来测试 CDN/代理层供应商了。