Nginx 配置双证书 / Chacha20 / TLS1.3 / HTTP2.0
修订记录
- 2021.01.06 - 更新了自用的 Nginx 配置
- 2016.03.03 - 补充了 Chacha20 和 Certificate Transparency 相关的配置
- 2015.12.07 - 关于 ECC 证书的补充
- 2015.05.28 - 初稿完成
TLS 新特性
文章首次发布是在 2015 年,那时候刚刚开始普及 TLS 证书,当时的 HTTPS 网站普遍存在首次握手慢,服务器性能开销略高的情况,小站长忘记续期 HTTPS 证书等等问题。
这些问题在如今的 2021 年基本都得到了解决,TLS 协议更新到了 1.3,HTTP/2.0 也在变得流行起来,这在一定程度上解决了服务器和客户端资源开销的问题。acme.sh 为代表的一众自动化脚本,更是彻底解决了证书更新的问题,自动化脚本甚至支持部署证书到其他机器上,和以前需要自己创建 CSR 等步骤比起来真的是天壤之别。
HTTP/2
相比廉颇老矣的 HTTP/1.x,HTTP/2 在底层传输做了很大的改动和优化包括有:
- 每个服务器只用一个连接,节省多次建立连接的时间,在TLS上效果尤为明显
- 加速 TLS 交付,HTTP/2 只耗时一次 TLS 握手,通过一个连接上的多路利用实现最佳性能
- 更安全,通过减少 TLS 的性能损失,让更多应用使用 TLS,从而让用户信息更安全
在 Akamai 的 HTTP/2 DEMO中,加载300张图片,HTTP/2 的优越性极大的显现了出来,在 HTTP/1.X 需要 14.8s 的操作中,HTTP/2 仅需不到1s。
HTTP/2 现在已经获得了绝大多数的现代浏览器的支持。只要我们保证 Nginx 版本大于 1.9.5 即可。当然建议保持最新的 Nginx 稳定版本以便更新相关补丁。同时 HTTP/2 在现代浏览器的支持上还需要 OpenSSL 版本大于 1.0.2。
TLS 1.3
TLS 1.3 相较之前版本的优化内容有:
- 握手时间:同等情况下,TLSv1.3 比 TLSv1.2 少一个 RTT
- 应用数据:在会话复用场景下,支持 0-RTT 发送应用数据
- 握手消息:从 ServerHello 之后都是密文。
- 会话复用机制:弃用了 Session ID 方式的会话复用,采用 PSK 机制的会话复用。
- 密钥算法:TLSv1.3 只支持 PFS (即完全前向安全)的密钥交换算法,禁用 RSA 这种密钥交换算法。对称密钥算法只采用 AEAD 类型的加密算法,禁用CBC 模式的 AES、RC4 算法。
- 密钥导出算法:TLSv1.3 使用新设计的叫做 HKDF 的算法,而 TLSv1.2 是使用PRF算法,稍后我们再来看看这两种算法的差别。
总结一下就是在更安全的基础上还做到了更快,目前 TLS 1.3 的重要实现是 OpenSSL 1.1.1 开始支持了,并且 1.1.1 还是一个 LTS 版本,未来的 RHEL8、Debian 10 都将其作为主要支持版本。在 Nginx 上的实现需要 Nginx 1.13+。
ECC 证书
// Generate RSA key
openssl genrsa -out private.key 4096
// or
// Generate ECC key
openssl ecparam -genkey -name secp384r1 -out private-ecc.key
// Generate CSR
openssl req -new -sha384 -key private-ecc.key -out www_pupboss_com.csr
关于 ECC 证书,256 位的证书相当于 3072 位的 RSA 证书,还避免了性能消耗以及网络带宽传输。secp384r1 是其中一个曲线名,openssl ecparam -list_curves
可以看所有的曲线名。经过实测 ECC 的性能消耗跟加密程度一样强悍,384 位的 ECC 给 CPU 造成的压力和 3000 位左右的 RSA 一样。
文章发表的时候,谷歌搜 ECC 证书
的第一页已经被沃通承包了,声称对移动设备友好,兼容 XP Android 2.1.x 等等等等,其实生产环境并没发现有什么优势,正常的 ECC 反而不兼容 Android 2.x,XP。沃通颁发的 ECC 证书是经过特殊处理的(如果他的 ECC 证书真的兼容 XP Android 2.1.x),猜测是采用了传说中的双证书。
我相信大部分看到这篇博客的人都是奔着 SSL Labs 评分 A+ 去的,后面我会分享具体的配置。
准备步骤
申请完 SSL 证书,应该有以下几样东西
- private.key 私钥
- cert.csr CSR
- www_pupboss.crt 公钥
- xxxx.crt 证书商的中级证书和根证书
首先把证书商提供的根证书中级证书添加到自己的证书后面,不然有的浏览器可能报错,我的习惯是域名证书 + 中级证书。如果是 acme.sh 这类自动化脚本则不需要手动合并证书了。
SSL 协议
SSLv2
是不安全的,绝对不能用,SSLv3
能不用则不用,正常情况下用不到的,推荐使用 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3
,如果你很激进或者你只配置 ECC 证书,那么推荐用 TLSv1.2 TLSv1.3
附配置代码:
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ECC RSA 双证书
双证书的配置有一点需要注意,部分老旧的系统支持 RSA 证书,但是不支持 TLS 1.2,比如 IE8-10,XP 系统上最新的 Chrome 51 等等。也就是说你如果想让 RSA 证书起作用,最好是最低兼容到 TLSv1
否则 RSA 证书配了也白配。
ssl_certificate jetl/ssl/pupboss.com.crt;
ssl_certificate_key jetl/ssl/pupboss.com.key;
ssl_certificate jetl/ssl/pupboss.com.rsa.crt;
ssl_certificate_key jetl/ssl/pupboss.com.rsa.key;
需要注意的是,部分 Linux 操作系统比如 Debian 11 里面的 openssl 默认禁用了 TLSv1.2 以下的协议,具体可以在 /etc/ssl/openssl.cnf
里看到:
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT@SECLEVEL=2
解决方案就是把最低的协议改为 TLSv1,这样 Nginx 就可以最低支持到 TLSv1。
加密算法
OpenSSL 记得更新到最新版,可以避免很多麻烦。ECC 证书的部署和 RSA 不大一样,用错之后会影响向前安全性 (Forward Secrecy)。下面的套件同时支持 RSA 和 ECC 证书,并且支持双证书。附代码:
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:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:secp384r1;
如果你的 OpenSSL 版本比较旧,不可用的加密算法会被自动丢弃。最好使用完整套件,让 OpenSSL 自动选择。所以套件的顺序就灰常重要
- ECDHE+AESGCM 加密是首选的。它们是 TLS 1.2 加密算法,现在还没有广泛支持。当然也没有破解的方案。
- PFS 加密套件好一些,首选 ECDHE,然后是 DHE。
- AES 128 要好于 AES 256。AES 256 会造成更大的性能消耗,但是带来的安全提升是有限的。反之还能承受更大压力。
- 在向后兼容的加密套件里面,AES 要优于 3DES。在 TLS 1.1及其以上,减轻了针对 AES 的野兽攻击(BEAST)的威胁,而在 TLS 1.0上则难以实现该攻击。在非向后兼容的加密套件里面,不支持 3DES。
- RC4 整个不支持了。3DES 用来向后兼容。
强制丢弃的算法
- aNULL 包含了非验证的 Diffie-Hellman 密钥交换,这会受到中间人(MITM)攻击
- eNULL 包含了无加密的算法(明文)
- EXPORT 是老旧的弱加密算法,是被美国法律标示为可出口的
- RC4 包含使用了已弃用的 ARCFOUR 的加密算法
- DES 包含使用了弃用的数据加密标准(DES)的加密算法
- SSLv2 包含了定义在旧版本 SSL 标准中的所有算法,现已弃用
- MD5 包含了使用已弃用的 MD5 的所有算法
前向安全性 (Forward Secrecy)
首先 Nginx 配置上如下代码:
ssl_dhparam /your/path/to/dhparam.pem;
pem 文件是这样生成的:
openssl dhparam -out dhparam.pem 4096
然后会显示
This is going to take a long time
.......+...........................+............................+............................+............++*++*++*
当时这个命令,VPS 上单核执行了 100 分钟,我的电脑 i7 4750HQ 处理器,只能跑一个核,用了大概 70 分钟,VPS 上生成的 ..+... 字符数大概在 50000 个,我的电脑产生了 74900 多个,不知道什么原因
dhparam 算法是在 2^4096 个数字中找出两个质数,所以需要的时间挺长。.....
意思是有可能的质数,+
是正在测试的质数,*
是已经找到的质数。-- 来自 @justwd 的邮件
前向安全性 (Forward Secrecy) 的概念很简单:客户端和服务器协商一个永不重用的密钥,并在会话结束时销毁它。服务器上的 RSA 私钥用于客户端和服务器之间的 Diffie-Hellman 密钥交换签名。从 Diffie-Hellman 握手中获取的预主密钥会用于之后的编码。因为预主密钥是特定于客户端和服务器之间建立的某个连接,并且只用在一个限定的时间内,所以称作短暂模式(Ephemeral)。
如果使用前向安全机制,攻击者取得了一个服务器的私钥,他是不能解码之前的通讯信息的。这个私钥仅用于 Diffie Hellman 握手签名,并不会泄露预主密钥。Diffie Hellman 算法会确保预主密钥绝不会离开客户端和服务器,而且不能被中间人攻击所拦截。
所有版本的 nginx 都依赖于 OpenSSL 给 Diffie-Hellman 的输入参数。如果不特别声明,将使用 OpenSSL 的默认设置,1024 位密钥。这绝壁是不安全的,因为我们正在使用 2048 位证书,所以要有一个更强大的 DH
HTTP 严格传输安全(HSTS)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
接下来的半年,都会保持直接发送 HTTPS。
配置 OCSP 装订
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 [2001:4860:4860::8888] 223.5.5.5 [2400:3200::1] valid=300s;
resolver_timeout 5s;
连接到一个服务器时,客户端应该使用证书吊销列表(CRL)或在线证书状态协议(OCSP)记录来校验服务器证书的有效性。CRL 存在一个问题,它已经增长的太快,永远也下载不完。
OCSP 更轻量一些,只需发一个请求。但是副作用是访问一个站点时必须对第三方 OCSP 响应服务器发起 OCSP 请求,这就增加了延迟带来的潜在隐患。事实上,CA 所运营的 OCSP 响应服务器非常不可靠,浏览器如果不能及时收到答复,就会静默失败。攻击者通过 DDoS 攻击一个 OCSP 响应服务器可以禁用其校验功能,这样就降低了安全性。
解决方法是允许服务器在 TLS 握手中发送缓存的 OCSP 记录,以绕开 OCSP 响应服务器。这个机制减少了客户端和 OCSP 响应服务器之间的通讯,称作 OCSP 装订。
客户端会在它的 CLIENT HELLO 中告知其支持 status_request TLS 扩展,服务器仅在客户端请求它的时候才发送缓存的 OCSP 响应。
大多数服务器最长会缓存 OCSP 48 小时。服务器会按照常规的间隔连接到 CA 的 OCSP 响应服务器来获取刷新的 OCSP 记录。OCSP 响应服务器的位置可以从签名的证书中 Authority Information Access 字段中获得。
我的服务器是这么配置的:
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_dhparam jetl/ssl/dhparam.pem;
ssl_certificate jetl/ssl/pupboss.com.crt;
ssl_certificate_key jetl/ssl/pupboss.com.key;
ssl_certificate jetl/ssl/pupboss.com.rsa.crt;
ssl_certificate_key jetl/ssl/pupboss.com.rsa.key;
ssl_dhparam ssl/dhparam.pem;
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:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 [2001:4860:4860::8888] 223.5.5.5 [2400:3200::1] valid=300s;
resolver_timeout 5s;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval'" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
正常情况下,这样配置完之后,测试就会显示 A+ 了。
重启免密码
有的时候生成 key 的时候,手贱输了密码,敲入如下指令可以解除:
openssl rsa -in pupboss.key -out pupboss_unsecure.key
强制 HTTPS
加上如下代码
server {
listen 80 default;
server_name www.pupboss.com;
return 301 https://$server_name$request_uri;
}
配置 Chacha20 和 Certificate Transparency
Chacha20 和 Certificate Transparency 相关的配置网上都有,我再写也写的差不多,看着跟抄袭的一样,我先附上参考链接:
写的都挺好的,我就直接贴一个脚本吧。具体教程上他们博客看去。
下面的脚本适用于 Debian x64 系统,并且使用 apt-get 安装 mainline version 的 Nginx 的机器,其他的可能得根据自己编译的参数,重新改改~编译完的二进制大概是 12M,A 机器上编译的,拷贝到 B 机器,chmod +x 之后,是可以运行的,只要系统版本一样。
注意:Certificate Transparency 相关的需要一台境外服务器的支持,因为谷歌被 Ban 了。
#!/bin/bash
cd /opt/chacha20
NG_VERSION=nginx-1.9.11
wget http://nginx.org/download/$NG_VERSION.tar.gz
tar -zx -f $NG_VERSION.tar.gz
wget -O nginx-ct.zip https://github.com/grahamedgecombe/nginx-ct/archive/master.zip
unzip nginx-ct.zip
wget -c https://github.com/openssl/openssl/archive/OpenSSL_1_0_2-stable.zip
git clone https://github.com/cloudflare/sslconfig
unzip OpenSSL_1_0_2-stable.zip && mv openssl-OpenSSL_1_0_2-stable openssl
cd openssl && patch -p1 < ../sslconfig/patches/openssl__chacha20_poly1305_cf.patch
cd /opt/chacha20/$NG_VERSION/
./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=%{_libdir}/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-threads --with-stream --with-stream_ssl_module --with-http_slice_module --with-mail --with-mail_ssl_module --with-file-aio --with-http_v2_module --with-cc-opt='-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-z,relro -Wl,--as-needed' --with-ipv6 --with-openssl=../openssl --add-module=../nginx-ct-master
make
service nginx stop
mv objs/nginx /usr/sbin/nginx
service nginx start
cd /opt/chacha20
rm -rf *