给 Nginx 添加 TLSv1.3 支持

前言

TLSv1.3 草案出来有很长一段时间了, Chrome, Firefox 以及 Cloudflare 的 CDN 也早加入了实验性支持, 所以就很想试试, 结果找了半天并没有找到解决方案, 很气. 直到后来无意看到 OpenSSL 的代码库 多了个 tls1.3-draft-18 的分支 就贼高兴, 折腾了半天之后终于成功给 Nginx 开启了 TLSv1.3.

注: Nginx 1.13.0 开始 正式支持 ssl_protocols 的 TLSv1.3 的选项. 在此之前 TLSv1.2 选项会自动使用 TLSv1.3.

OpenSSL 目前有 draft-18 分支, pre2 的 draft-23, pre7-pre8 的 draft-28, 以及 pre9+ 的 Final, 通过 tls1.h (include/openssl/tls1.h 第35行) 可以查看目前的 Draft…

从 Chrome 65 开始会默认开启并使用 TLSv1.3 Draft 23, 从 Chrome 68 开始支持 Draft 28 (Firefox Nightly 应该也支持), Chrome 70.0.35xx 开始支持 Final , 但同版本号 Linux 会默认使用 Final, 而 Windows 则使用 Draft 23, 很迷.

编译安装

依赖

sudo apt update
sudo apt install -y build-essential autoconf automake liblua5.1-dev libluajit-5.1-dev libgeoip-dev

mkdir ~/Swift && cd ~/Swift

# Nginx 1.17.2
wget https://nginx.org/download/nginx-1.17.2.tar.gz
tar zxf nginx-1.17.2.tar.gz

# SPDY, HTTP2 HPACK, Dynamic TLS Record, Fix Http2 Push Error Patch
# 此补丁不一定适用于最新的 Nginx 已测试通过的版本请查看 https://github.com/kn007/patch
pushd nginx-1.17.2
curl https://raw.githubusercontent.com/kn007/patch/d6bd9f7e345a0afc88e050a4dd991a57b7fb39be/nginx.patch | patch -p1
popd

# Strict-SNI Patch
# 使用方式见下文
pushd nginx-1.17.2
curl https://raw.githubusercontent.com/hakasenyang/openssl-patch/master/nginx_strict-sni_1.15.10.patch | patch -p1
popd

# OpenSSL 1.1.1b (LTS)
wget https://www.openssl.org/source/openssl-1.1.1b.tar.gz
tar zxf openssl-1.1.1b.tar.gz

# OpenSSL Patch
# BoringSSL's Equal Preference Patch
# Weak 3DES and not using ECDHE ciphers is not used in TLSv1.1 or later.
# 根据 OpenSSL 版本决定, 具体见 https://github.com/hakasenyang/openssl-patch
pushd openssl-1.1.1b
curl https://raw.githubusercontent.com/hakasenyang/openssl-patch/master/openssl-equal-1.1.1b_ciphers.patch | patch -p1
popd

# CHACHA20-POLY1305-OLD Patch
pushd openssl-1.1.1b
curl https://raw.githubusercontent.com/hakasenyang/openssl-patch/master/openssl-1.1.1b-chacha_draft.patch | patch -p1
popd

# jemalloc
git clone https://github.com/jemalloc/jemalloc.git
pushd jemalloc
./autogen.sh
make -j$(nproc --all)
touch doc/jemalloc.html
touch doc/jemalloc.3
sudo make install
echo '/usr/local/lib' | sudo tee /etc/ld.so.conf.d/local.conf
sudo ldconfig

# zlib (Cloudflare)
git clone https://github.com/cloudflare/zlib.git
pushd zlib
./configure
popd

# libatomic_ops
git clone https://github.com/ivmai/libatomic_ops.git
pushd libatomic_ops
./autogen.sh
./configure
make -j$(nproc --all)
make install
sudo ldconfig
popd

# pcre
wget https://ftp.pcre.org/pub/pcre/pcre-8.42.zip
unzip pcre-8.42.zip&&rm pcre-8.42.zip
mv pcre-8.42 pcre
pushd pcre
./configure
popd

# ngx_brotli
# 支持使用包管理器安装的 brotli 库
git clone https://github.com/eustas/ngx_brotli.git
pushd ngx_brotli
git submodule update --init
popd

编译

使用 --with-openssl=../openssl-****** 来指定 OpenSSL 路径

目前 OpenSSL 开启 TLS1.3 需要 加入 --with-openssl-opt=enable-tls1_3

TLSv1.3 is enabled by default in the latest development versions (there is no need to explicitly enable it). To disable it at compile time you must use the “no-tls1_3” option to “config” or “Configure”. https://www.openssl.org/blog/blog/2018/02/08/tlsv1.3/

HTTP2 HPACK 需要加入 --with-http_v2_hpack_enc

SPDY 需要加入 --with-http_spdy_module

其他参数自己按需控制

cd nginx-1.17.2

./configure \
--with-cc-opt='-g -O3 -m64 -march=native -ffast-math -DTCP_FASTOPEN=23 -fPIE -fstack-protector-strong -flto -fuse-ld=gold --param=ssp-buffer-size=4 -Wformat -Werror=format-security -Wno-unused-parameter -fno-strict-aliasing -fPIC -D_FORTIFY_SOURCE=2 -gsplit-dwarf' \
--with-ld-opt='-lrt -L /usr/local/lib -ljemalloc -Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now -fPIC' \
--sbin-path=/usr/sbin/nginx \
--prefix=/usr/share/nginx \
--conf-path=/etc/nginx/nginx.conf \
--http-log-path=/var/log/nginx/access.log \
--error-log-path=/var/log/nginx/error.log \
--lock-path=/var/lock/nginx.lock \
--pid-path=/run/nginx.pid \
--modules-path=/usr/lib/nginx/modules \
--http-client-body-temp-path=/var/lib/nginx/body \
--http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
--http-proxy-temp-path=/var/lib/nginx/proxy \
--http-scgi-temp-path=/var/lib/nginx/scgi \
--http-uwsgi-temp-path=/var/lib/nginx/uwsgi \
--with-threads \
--with-file-aio \
--with-pcre-jit \
--with-http_v2_module \
--with-http_ssl_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_slice_module \
--with-http_geoip_module \
--with-http_gunzip_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_gzip_static_module \
--with-http_degradation_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_random_index_module \
--with-http_auth_request_module \
--with-stream \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-stream_realip_module \
--add-module=../ngx_brotli \
--add-module=../lua-nginx-module \
--add-module=../nginx-dav-ext-module \
--add-module=../ngx_http_geoip2_module \
--add-module=../nginx-sorted-querystring-module \
--add-module=../ngx_http_substitutions_filter_module \
--with-pcre=../pcre \
--with-zlib=../zlib \
--with-libatomic \
--with-openssl=../openssl-1.1.1b \
--with-openssl-opt='zlib -march=native -ljemalloc -Wl,-flto' \
--with-http_v2_hpack_enc \
--with-http_spdy_module
make -j$(nproc --all)
sudo make install

测试

sudo nginx -t

如果都是 OK 那么 一切没问题.

配置 Nginx

Strict SNI

仅可用在 http 块内

# 开启关闭 Strict SNI
strict_sni on/off;
# 开启关闭无效域名的检查
strict_sni_header on/off;
# Strict SNI 最少需要两个 SSL 站点, 无所谓用什么证书.
server { listen 443 ssl;}

配置完成后使用 Chrome 直接访问 https://ip-address 如果返回 ERR_SSL_UNRECOGNIZED_NAME_ALERT 则表示配置正确.

Brotli

在 http 块中加入

brotli on;
brotli_static on;
brotli_min_length 20;
brotli_buffers 32 8k;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml application/rss+xml application/atom+xml image/svg+xml;

TLSv1.3

ssl_protocols 中加入 TLSv1.3 (仅 Nginx 1.13.0 及以上, 低版本用 TLSv1.2 就行)

ssl_protocols TLSv1.2 TLSv1.3;

修改 ssl_ciphers ( openssl-patch 参考 )

ssl_ciphers [TLS13+AESGCM+AES128|TLS13+AESGCM+AES256|TLS13+CHACHA20]:[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:RSA+3DES;

Early data (0-RTT)

ssl_early_data on;

另外请添加 Early-Data 头告知后端, 防止重放攻击

proxy_set_header Early-Data $ssl_early_data;

最后 使用 sudo nginx -t 测试一下 确认无问题

浏览器支持

主流浏览器已支持 TLSv1.3

查看 TLSv1.3 支持表: https://caniuse.com/#feat=tls1-3

另外 Chrome Canary 77.0.3834.0 已支持 Early-Data (0-RTT), 其他版本不清楚. 可以尝试打开 chrome://flags/#enable-tls13-early-data 看看目前使用的版本是否支持.

Chrome-Early-Data

SSL Test 结果

Test Page - (TLS 1.3 final)

SSL Test Result - dev.ssllabs.com

testssl.sh 测试方式

git clone --depth 1 https://github.com/drwetter/testssl.sh.git
cd testssl.sh
./testssl.sh --full https://your_domain