Lan Tian @ Blog

nginx:TLS 1.3 多版本草案和 HPACK

距离我之前给 nginx 启用 TLS 1.3 已经过了 11 个月了。快一年过后,许多与 nginx 相关的程序、补丁都有了很大的变化:

  1. OpenSSL 已经在发布 1.1.1 的测试版,写本文时最新版本是 1.1.1-pre8(也就是 Beta 6)。
  2. nginx 已经更新到 1.15.1。
  3. nginx 的 HPACK 补丁(HTTP 头压缩补丁)的 bug 已经有另外的补丁的补丁修复,使用原先的 HPACK 补丁会导致网站访问不正常,体现为每个网站只能打开一个页面,第二个页面开始就出现协议错误。
  4. 有大佬发布了 OpenSSL 的补丁,可以让最新版 OpenSSL 同时支持 TLS 1.3 的 draft 23,26,28 三个版本。
  5. Lets Encrypt 证书已经自带 Certificate Transparency 信息了,不需要 nginx-ct 了。
  6. 2018 年 7 月 1 日起,TLS 1.0 不再被建议使用。

因此我重新调整了 nginx 的编译和运行配置,以适应 8102 年的需要。

Dockerfile

我依然使用 Docker 部署 nginx。与之前的 Dockerfile 相比,新的 Dockerfile 只是改了下版本号,添加了几个补丁,整体并没有大的变化。

为了节省篇幅,我将 Dockerfile 上传到了 https://github.com/xddxdd/dockerfiles/blob/master/nginx/Dockerfile.amd64。你也可以直接 docker pull xddxdd/nginx 来使用。

这个 Dockerfile 包含了如下内容:

  • nginx 1.15.1
  • OpenSSL 1.1.1-pre8
  • kn007 大佬的 SPDY、HPACK、Dynamic TLS Record 三合一补丁,他的项目地址在此访问
  • kn007 大佬的 HPACK 补丁的修复补丁
  • Brotli 压缩算法
  • hakasenyang 大佬的 TLS 1.3 三版本草案补丁,他的项目地址在此访问
  • nginx headers-more 模块

配置改变

首先禁用 TLS 1.0。

#ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;

然后,因为 OpenSSL 修改了一大堆加密算法的名称,因此如果直接沿用之前的 ssl_ciphers 会出现 ERR_SSL_VERSION_OR_CIPHER_MISMATCH 错误,意思是没有服务器和客户端同时支持的加密算法。因此修改 ssl_ciphers:

#ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:!DSS';
ssl_ciphers '[TLS13+AESGCM+AES128|TLS13+CHACHA20]:TLS13+AESGCM+AES256:[EECDH+ECDSA+AESGCM+AES128|EECDH+ECDSA+CHACHA20]:EECDH+ECDSA+AESGCM+AES256:EECDH+ECDSA+AES128+SHA:EECDH+ECDSA+AES256+SHA:[EECDH+aRSA+AESGCM+AES128|EECDH+aRSA+CHACHA20]:EECDH+aRSA+AESGCM+AES256:EECDH+aRSA+AES128+SHA:EECDH+aRSA+AES256+SHA:RSA+AES128+SHA:RSA+AES256+SHA';

然后?

重新启动 nginx,用最新稳定版的 Chrome 和 Firefox 就可以以 TLS 1.3 draft 23 访问了,用最新开发版的 Chrome 和 Firefox 就可以以 TLS 1.3 draft 28 访问了。

如何在 SSLLabs 测试中冲满分

Qualys SSL Labs 是一个测试服务器 SSL 功能的网站,我们在配置服务器时经常用它作为参考。一般来说我们只参考它的评级(A+,A,B,C,D,E,F,T)等,达到 A+ 就认为服务器的配置足够优秀。不过,SSLLabs 也在评级右侧给出了分项分数,而我的主站并没有把它们全部冲到满分。如果把 SSLLabs 的各项分项分数全部达到满分,会是什么效果,有什么实际意义吗?我用一台不运行网站的 VPS 安装了 nginx 并进行了一些配置,成功刷到了全满分,如图或者这里所示

插图

作为对比,这是本站的评分,除看图外也可在此处看到

插图

本文有关 SSLLabs 评分标准全部来自于 SSLLabs 官方的评分标准文档 在本文写成之日的版本。

准备工作

我们要先安装好 nginx。因为这台 VPS 是 OpenVZ 的,没法装 Docker,所以没法像我的其它 VPS 一样一键部署。不过因为我的 VPS 和镜像都是 Debian,所以我就把之前写过的 Dockerfile 里的指令直接复制过来用了。

证书要求

SSL Labs 要求证书不能有以下情况:

  • 域名不匹配
  • 证书还未生效或者已到期
  • 使用自签名证书等不被信任的证书
  • 使用被注销(Revoked)的证书
  • 不安全的证书签名或者密钥

基本上,只要你的证书是近期申请的,并且浏览器能正常打开你的网站,那么就没问题。

协议支持

协议支持在评级中占 30% 的比重,算法是根据服务器支持的协议打分,取最高分和最低分的平均数。

各个协议评分如下:

  • SSL 2.0: 0 分
  • SSL 3.0: 80 分
  • TLS 1.0: 90 分
  • TLS 1.1: 95 分
  • TLS 1.2: 100 分
  • TLS 1.3: 100 分

(注:TLS 1.3 的得分在评分标准中未给出,但是实际按 100 分计)(此处指 TLS 1.3 草案 18(draft 18))

SSL 2.0 现在看来是个筛子,必须严格禁止使用。SSL 3.0 也没好到哪去,但是有些网站需要它来支持老旧的浏览器(对,我说的就是 IE6)。

TLS 1.0 也有点问题,不过在各大浏览器厂商的合作下,这个问题可以当成已经解决。TLS 1.1、1.2 和 1.3 目前来看没有问题。

为了在此项得到满分,必须关闭除 TLS 1.2 以外的全部协议(包括 TLS 1.3,原因等会讲),在 nginx 中如此配置:

ssl_protocols TLSv1.2;

密钥交换

密钥交换是服务器和客户端验证彼此身份,并且产生用于当前会话的对称加密密钥的方式。其评分标准同样是最高和最低的平均数。详细分数如下:

  • 匿名密钥交换:0 分(因为无法验证身份,极易被中间人攻击)
  • 密钥强度 0 到 511 bit: 20 分
  • 出口密钥交换:40 分(这些是早期美国限制出口密钥交换算法时的产物,都很弱,容易破解)
  • 密钥强度 512 到 1023 bit:40 分
  • 密钥强度 1024 到 2047 bit:80 分
  • 密钥强度 2048 到 4095 bit:90 分
  • 密钥强度 4096 bit 及以上:100分

此处的强度是指 RSA 的等效强度。如果你用的是 RSA 证书(一般都是这种),要将密钥强度调成 4096 bit。如果你用的是 ECC 证书(比较新的证书格式),只需要 384 bit 就能有 4096 bit 的 RSA 等效强度。

如果你用的是 Let's Encrypt 以及其官方客户端 Certbot,用以下命令生成密钥即可:

certbot certonly --webroot -w /var/www/ -d vmbox.lantian.pub --rsa-key-size 4096

生成 ECC 证书需要第三方客户端,这里先不展开。(我比较懒就跳过了)

但是安装上 4096 bit 证书后,你这项的评分仍然可能没满。这就取决于 nginx 配置文件中的密钥交换算法了。先来看下这张图,这是本站的加密算法列表:

插图

算法后面有一些标记,其意义如下:

  • ECDH secp256r1 (eq. 3072 bits RSA):ECDH 256 bit 算法,等效 3072 bit RSA
  • ECDH secp384r1 (eq. 7680 bits RSA):ECDH 384 bit 算法,等效 7680 bit RSA
  • DH 4096 bit:RSA 4096 bit 算法

密钥交换算法的强度取决于密钥本身和交换算法两者中的较弱者。为了满分,就只好把所有小于 4096 bit RSA 的算法都关了。

再看一下 TLS 1.3 的算法们,它们都是等效 3072 bit RSA 的,只能把它们全禁了。全禁了之后,TLS 1.3 也就没用了,因此我刚刚不保留开启 TLS 1.3。

等等,还没完。ECDH 系列交换算法依赖一个叫 DHparams 的文件,需要运行这条命令生成:

openssl dhparam -out dhparam.pem 4096

这条命令运行奇慢,在我的 i5-3210M 的笔记本上运行了快 1 个小时。生成完后在 nginx 里如下配置:

ssl_dhparam /etc/dhparam.pem;

密钥强度

最后,就是双方交换得到的密钥本身的强度。评分同样是最大和最小的平均值,细则如下:

  • 0 bit(无加密):0 分
  • 1-127 bit:20 分
  • 128-255 bit:80 分
  • 256 bit 及以上:100 分

密钥强度由密钥交换算法确定,因此只留 256 bit 密钥强度的交换算法即可。符合条件的算法是 CHACHA20-POLY1305 和 AES-256。我的配置如下:

ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:!DSS';

其它调整

另外还要打开 HSTS(强制浏览器走 SSL)。最好还要打开 HPKP(要求浏览器只认某几家的证书)。配置如下:

add_header Strict-Transport-Security 'max-age=15768000;includeSubDomains;preload';

(HSTS,注意会对子域名生效,并且允许你的网站提交到 HSTS Preload 列表,也就是浏览器永远走 SSL,即使没访问过你的网站)

add_header Public-Key-Pins 'pin-sha256="9uthMA8OzB/wVGSR3w5bzlt6jAFLWEI533bM+vNDkts=";pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=";pin-sha256="sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=";pin-sha256="58qRu/uxh4gFezqAcERupSkRYBlBAvfcw7mEjGPLnNU=";max-age=2592000';

(放行了 Let's Encrypt 的两张中级证书,另两张貌似是 TrustAsia 和 Comodo 的,记不清了)

完成以上这些后,SSL Labs 就会显示你的 SSL 满分了。

实用性?

满分固然好看,但是其提供的意义又有多少?在这个过程中,我们使用了明显更慢的 4096 bit 密钥和交换算法,而 2048 bit 对于现在的算力来说已经足够。这就造成了额外的 CPU 开销(加密解密)和网络开销(传输密钥)。同时,我们放弃了很多旧客户端的支持,其中有些我们一般不考虑支持了(例如 IE8),但是根据 SSLLabs 显示的测试结果,有些客户端却很重要:

  • Android 4.3 及之前版本的设备(旧手机,旧智能电视及盒子)
  • 百度搜索索引机器人(根据 SSL Labs 页面上显示,2015 年 1 月的版本)
  • IE 10 及之前的浏览器(一些还没升上级的 Windows 7,以及 Windows Phone 8.0)
  • Java 6、7(不支持 TLS 1.2)
  • Java 8(不支持留下的这些密钥交换算法)
  • Safari 6 及之前版本

其中以 Java 8 最为重要。绝大多数网站完全不需要这么注重安全的配置,大可以允许一些稍微弱一些的算法来提高兼容性。

最后,满分演示站在此:http://vmbox.lantian.pub/

满分报告地址:https://www.ssllabs.com/ssltest/analyze.html?d=vmbox.lantian.pub

nginx 配置并启用SSL和SPDY访问

来自 CloudFlare 博客的最新消息(http://blog.cloudflare.com/google-now-factoring-https-support-into-ranking-cloudflare-on-track-to-make-it-free-and-easy ),Google可能会在算权重的时候把网站支持SSL作为加分项目。因此我就给我的博客启用了SSL。

1.申请证书

SSL,在服务器端必须有证书。这个证书最好不要自己生成,否则大多数浏览器都会提示证书不受信任。

StartSSL是目前唯一一家颁发免费SSL证书并且受到大多数浏览器信任的证书颁发机构,可以根据 http://www.freehao123.com/startssl-ssl/ 这篇文章进行操作。

不过在实际操作中,由于StartSSL连接速度较慢,如果按照向导生成证书,中途可能卡住,而一旦卡住就要全部重来,我们可以自己生成证书请求,然后提交。

  1. 在你的Linux服务器(本例为Debian 7)上输入以下命令产生一个私钥:

  2. openssl genrsa -out privkey.pem 4096


  3. 输入以下命令产生证书请求(CSR文件),其中信息可以乱填,StartSSL不关心其中信息,他只关心你的私钥是多少。

  4. openssl req -new -key privkey.pem -out cert.csr
  5. cat cert.csr


  6. 把第五步的输出复制。

  7. 按照上面这篇文章操作,执行到这一步时选择Skip,然后粘贴入你的证书请求,然后下一步。

插图

2.安装证书

完成上面的步骤后,你应该(至少)有两个文件:一个是你的私钥,一个是证书文件。

如果你手动生成证书请求csr文件,那么privkey.pem就是你的私钥;如果你通过网页向导生成,私钥已经在向导过程中给出。

  1. 把你的证书保存为ssl.crt,以下操作将按照本文件名继续。如果你通过网页向导操作,把你的私钥保存到ssl.key。

  2. 如果通过网页向导操作,输入以下命令对你的私钥解密,密码是你在网页向导上填写的那个。如果不解密,每次nginx启动时都会要求你输入密码。

  3. openssl rsa -in ssl.key -out privkey.pem


  4. Firefox在证书验证上比较奇葩,它要求证书文件中保存有这个证书的颁发机构的证书。因此我们要进行如下操作:

  5. wget http://www.startssl.com/certs/ca.pem
    wget http://www.startssl.com/certs/sub.class1.server.ca.pem
    cat ssl.crt sub.class1.server.ca.pem ca.pem > ssl-unified.crt


  6. 此时你获得一个ssl-unified.crt文件,把它和privkey.pem移动到你喜欢的地方。

  7. (警告:不要复制到能够通过网页访问来下载的地方,比如把他们放在/var/www里就是作死行为!本例以放在/root下为例。)

  8. 编辑你的nginx配置文件。

  9. cd /etc/nginx/sites-enabled
  10. nano default #如果你的网站配置文件名不同,请替换。


  11. 在配置文件里你的网站那一段的listen 80下面输入:

  12. listen 443 ssl;
    ssl_certificate /root/ssl-unified.crt;
    ssl_certificate_key /root/privkey.pem;


  13. 重启nginx,安装结束。。

  14. service nginx restart


3.SPDY的安装和启用

SPDY是Google主导开发的一个网络协议,使用它,可以在一个SSL连接内同时传输好几路数据。

比如,打开网页时,如果没有SPDY,那么浏览器就要同时打开好几路SSL连接下载数据。问题是,这些连接不是同时打开的,一般情况下网页加载到一半,浏览器发现“哎呀我需要这个东西但是还没下载”,才会开新连接下载数据。

而有了SPDY,浏览器可以直接在同一个SSL连接中下载数据,省去了连接和SSL验证的时间。

  1. 更新nginx版本。Debian软件源里默认的nginx不带有SPDY功能,需要把nginx替换成nginx-full。

  2. apt-get install nginx-full


  3. 编辑你的nginx配置文件,把

  4. listen 443 ssl;


  5. 改成

  6. listen 443 ssl spdy;


  7. 完事。

启用后,如果你的网站的图片、CSS、JS等都从你的服务器上下载,将会较大地改善你的网站在SSL下的载入速度。