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 访问了。

为 nginx 启用 TLS 1.3,并传递给 FastCGI 后端

OpenSSL 在最新的测试版中提供了 TLS 1.3 的实验性支持,包括了一系列的性能和安全性优化。最新的 nginx 1.13 系列也相应的添加了 TLS 1.3 的相关选项。

不过由于 TLS 1.3 还处在草案状态,现在使用还是要踩一些坑的:

  1. TLS 1.3 目前有 18、19、20 三个版本的草案已经被 OpenSSL 等实现,而且它们互不兼容,也没有一个 SSL 的库把它们三合一。
  2. 目前 Chrome、Firefox 等浏览器广泛使用的是 18 版草案,但是这个版本的 OpenSSL 不支持 TLS 扩展,而 Certificate Transparency 需要用到它。
  3. 虽然最新的 nginx-ct 插件增加了 TLS 1.3 的 Certificate Transparency 支持,但是由于第二条,它无法与草案 18 的 OpenSSL 一同工作,会出现编译失败的情况。因此必须退回到 nginx-ct 项目 release 中的 1.3.2 版本,而这个版本对 TLS 1.3 不生效。

我部署 nginx 使用的 Dockerfile 如下:

FROM debian:jessie-slim
MAINTAINER Lan Tian "lantian@lantian.pub"
ENV NGINX_VERSION=1.13.3 OPENSSL_VERSION=tls1.3-draft-18 NGINX_CT_VERSION=1.3.2
RUN cd /tmp \
    && apt-get update -q \
    && apt-get -y upgrade \
    && apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev \
    && wget http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
      && tar xf nginx-${NGINX_VERSION}.tar.gz \
    && git clone https://github.com/bagder/libbrotli.git \
      && cd /tmp/libbrotli && ./autogen.sh && ./configure && make && make install && cd /tmp \
      && ln -s /usr/local/lib/libbrotlienc.so.1 /usr/lib/libbrotlienc.so.1 \
    && git clone https://github.com/google/ngx_brotli.git \
      && cd /tmp/ngx_brotli && git submodule update --init && cd /tmp/ \
    && wget https://github.com/grahamedgecombe/nginx-ct/archive/v${NGINX_CT_VERSION}.zip \
      && unzip v${NGINX_CT_VERSION}.zip \
    && wget https://github.com/openssl/openssl/archive/${OPENSSL_VERSION}.zip \
      && unzip ${OPENSSL_VERSION}.zip \
    && cd nginx-${NGINX_VERSION} \
    && wget https://github.com/cloudflare/sslconfig/raw/master/patches/nginx__1.13.0_http2_spdy.patch \
      && patch -p1 < nginx__1.13.0_http2_spdy.patch \
    && wget https://github.com/cloudflare/sslconfig/raw/master/patches/nginx__1.11.5_dynamic_tls_records.patch \
      && patch -p1 < nginx__1.11.5_dynamic_tls_records.patch \
    && ./configure \
       --with-threads \
       --with-file-aio \
       --with-ipv6 \
       --with-http_ssl_module \
       --with-http_spdy_module \
       --with-http_v2_module \
       --with-http_gzip_static_module \
       --with-http_gunzip_module \
       --with-http_stub_status_module \
       --with-http_sub_module \
       --add-module=/tmp/nginx-ct-${NGINX_CT_VERSION} \
       --add-module=/tmp/ngx_brotli \
       --with-openssl=/tmp/openssl-${OPENSSL_VERSION} \
       --with-openssl-opt='enable-ec_nistp_64_gcc_128 enable-weak-ssl-ciphers enable-tls1_3 -ljemalloc' \
       --with-ld-opt="-ljemalloc" \
       --with-cc-opt="-O3 -flto -fPIC -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wno-deprecated-declarations" \
    && make \
    && make install \
    && cd / && rm -rf /tmp/* \
    && apt-get purge -y unzip git autoconf libtool automake build-essential \
    && apt-get autoremove -y --purge \
    && apt-get clean \
    && ln -sf /usr/local/nginx/sbin/nginx /usr/local/nginx \
    && ln -sf /dev/stdout /usr/local/nginx/logs/access.log \
      && ln -sf /dev/stderr /usr/local/nginx/logs/error.log
#EXPOSE 80 443
ENTRYPOINT /usr/local/nginx

该 Dockerfile 编译的 nginx 包含了 Brotli 压缩算法,Certificate Transparency(仅 TLS 1.2 及以下),OpenSSL 1.1.1 draft 18,以及 CloudFlare 的 SPDY 支持补丁和 Dynamic TLS Record 补丁。

如果你不使用 Docker,但是使用 Debian 或者 Ubuntu,可以直接用 RUN 后面的一长串命令来直接安装 nginx。

对配置文件的修改

nginx 的配置文件也需要修改,以开启 TLS 1.3 功能。在 nginx.conf 的 server 块里:

  1. 修改 ssl_protocols,添加 TLSv1.3
  2. 修改 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

修改完成的示例如下:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
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:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';

FastCGI 检测 TLS 版本

由于 SSL 连接往往并不直接建立到程序语言服务端上,例如 PHP 等 FastCGI 服务端,这些服务端往往无法得知用户连接的 TLS 版本号和加密方式。但是,nginx 可以得知这些信息,并且把它们作为变量存储起来。简单的修改配置文件,就能把这些信息传递到 FastCGI 后端。

打开你 nginx 配置目录下的 fastcgi_params 文件,并向里面加入两行:

fastcgi_param  SSL_CIPHER         $ssl_cipher;
fastcgi_param  SSL_PROTOCOL       $ssl_protocol;

重新加载 nginx 配置即可。以 PHP 为例,这些信息可以通过类似 $_SERVER['SSL_CIPHER'] 的方式来获取。

配置完成的 Qualys SSL Labs 检查结果

插图

插图