定义下我当时所用的版本,都是最新的稳定版
nginx_version=”1.18.0”
openssl_version=”1.1.1h”
很喜欢这些东西,想记录一下,废话不多说,直接开始

编译 Nginx

这里重新审视了我的行为,我认为是不妥的,编译安装对新手来说不是一个好选择,通过包管理器来安装软件在 Linux 永远都是最推荐的。具体原因在下一篇文章讲

依赖部分不多讲述,使用 GNU/Linux 上通用的 build-essential 就可以了

1
2
3
4
5
6
7
//在 /usr/local/src 目录下载源码包
wget https://nginx.org/download/nginx-1.18.0.tar.gz
wget https://www.openssl.org/source/openssl-1.1.1h.tar.gz

//解压
tar -zxf nginx-1.18.0.tar.gz
tar -zxf openssl-1.1.1h.tar.gz

之后正式开始编译

1
2
3
4
5
6
7
8
9
10
cd nginx-1.18.0
//编译检查,生成 Makefile,为下一步的编译做准备
./configure \
--with-http_ssl_module \
--with-openssl=../openssl-"1.1.1h" \
--//省略其余参数
//正式编译
make
//编译完无报错,安装
make install

之后参考官方文档 Building nginx from Sources 编写对应目录的 systemd 启动服务脚本
systemctl start nginx 就可以运行了

配置 Nginx

我最初的目标是配置出一个仅支持 TLS 1.3 的网站,而在这个过程中踩了一些坑,虽然解决了,但仍然不能明确知道为何解决了,所以记录一下
我最开始的配置是

1
2
3
4
5
6
7
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_protocols TLSv1.3;
ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256;
ssl_prefer_server_ciphers on;
}

甚至不能通过 nginx -t 的配置检查,提示 no cipher match,搜索了一下,也有人遇到同样的问题并向官方汇报了,得到的回复是 OpenSSL 不喜欢这些字符串?或许我不应该直接指定 cipher suites(密码套件)?
而且 TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256; 这个写法是直接借鉴了一些文章,但我四处搜索也没有找到这个写法的出处,可能是 2018 当年 TLS 1.3 刚刚发表时留下的一些历史遗留?故我将 cipher suites 改为我从 OpenSSL 查到的标准写法 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;,仍然不能通过 nginx -t 的配置检查。无奈,我只能将支持的协议版本改为

1
ssl_protocols TLSv1.2 TLSv1.3;

并添加了一个 TLS 1.2 的 cipher suites,这回总算能通过配置检查了,Nginx 成功启动,在 SSL Labs 得到的结果是站点仅支持我指定的那种 TLS 1.2 的 cipher suites,看来还是有点效果,但未完全解决问题,于是寻找原因
1.编译参数。最开始我的编译参数中并没有加上 --with-openssl-opt=enable-tls1_3,因为在一些文章中提到这个参数是默认开启的,故我加上了这个参数并重新编译(后来想想确实不是这个问题)
2.继续搜索相关问题,找到了 Let’s Encrypt 社区的一个回答 Let’s Encrypt Community - TLSv1.3 not working on Fedora 31 and Nginx 1.16.1,这里面解释说,如果你的 default_server 没有开启 ssl_protocols TLSv1.3,所有配置都会 Default 到 TLSv1.3 之下,因为我确实还多监听了 default_server(防止扫描证书

1
2
3
4
5
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
}

怎么 Default

有人说是 ssl_ciphers 对 TLS 1.3 无效,其实不准确,ssl_ciphers 是可以控制 TLS 1.3 的密码套件选择顺序的。经过我的测试,来讲讲如果不为 default_server 开启 ssl_protocols TLSv1.3 会怎么样

1
2
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256;

网站仅会支持 ECDHE-ECDSA-AES128-GCM-SHA256 这一种 cipher suites

1
ssl_protocols TLSv1.2 TLSv1.3;

网站会支持所有的 TLSv1.2 cipher suites,包括有安全隐患的 CBC 模式密码和非 AEAD 算法

1
ssl_protocols TLSv1.3;

这种写法,网站会 Default 到支持所有的 TLSv1.0、TLSv1.1、TLSv1.2 cipher suites,如果你这时去 SSL Labs 测试,会看到一大片被标记为黄色的 WEAK 警告

测试开启仅 TLS 1.3

虽然仍不能百分百确定原因,但我暂时也不想再去编译 Nginx 了,因为我已经可以实现最初的想法了,配置出一个仅支持 TLS 1.3 的网站,根本不需要指定什么 cipher suites,简单的在你所有 server {} 块里加上 ssl_protocols TLSv1.3; 即可。遵循 Mozilla Wiki - Security/Server Side TLS 的建议,最终,我的配置如下

开启仅 TLS 1.3 在如今仍是非常激进的做法,这只是我的一个实验。出于对旧设备和软件的兼容,不推荐读者这样配置密码套件

1
2
3
4
5
6
7
8
9
server {
# 支持 TLS 1.3 且不需要向后兼容
ssl_protocols TLSv1.3;
# cipher suites 偏好由客户端选择
ssl_prefer_server_ciphers off;
# 为所有的 header(包括错误响应)设置两年的 HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
······
}

虽说问题与 cipher suites 无关,但误打误撞,我对密码学稍微有了一些了解

一点简介

在 Linux 系统中安装 openssl 之后,使用 openssl ciphers -v -stdname | column -t 查看密码套件列表,一共有 60 行
去除掉非 AEAD 算法、TLS 1.2 以下的 ciphers、不具备前向安全性的 RSA 密钥交换算法后,陈列如下

1
2
3
4
5
6
7
8
9
10
11
12
TLS_AES_256_GCM_SHA384                         -  TLS_AES_256_GCM_SHA384         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(256)             Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 - TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256 - TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 - DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 - DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(128) Mac=AEAD

看起来也不是很多对吧,其中第一列就是可以在 Nginx 中指定的 cipher suites,当然也可以指定类,比如 ECDH+ECDSA,会包含这类密码套件
呃,本来想写点东西,可这块着实有些艰深,我肯定讲的会没有深度甚至有错误,所以只简单谈点经验了

名词介绍

1
2
3
4
5
6
7
8
ECC   Elliptic-curve cryptography     椭圆曲线密码学
ECDSA EC Digital Signature Algorithm 椭圆曲线数字签名算法
FS Forward Secrecy 前向安全
PFS Perfect Forward Secrecy 完全前向安全
AES Advanced Encryption Standard 高级加密标准
SHA Secure Hash Algorithm 安全散列算法
DH Diffie–Hellman 迪菲-赫尔曼密钥交换算法
ECDH Elliptic Curve Diffie–Hellman 椭圆曲线迪菲-赫尔曼密钥交换算法

经验

不要把 RSA 用于密钥交换,没有前向安全性。应该使用 DHE/ECDHE,E 表示 Ephemeral,临时 DH/ECDH,能够提供前向安全性,同时 TLS 1.3 也只支持 (EC)DHE 密钥协商算法,并且椭圆曲线优先,即 ECDHE 优先

针对证书公钥的选择,有 RSA 和 ECC 可选。根据 GnuPG 的建议,如果选择 RSA 算法做非对称加密,2048 位就已足够,如果你非要使用更高位的 RSA,那你应该去使用 ECC,256 位 ECC 便可提供相当于 3072 位 RSA 的密码强度。因为 RSA 算法随着位数的增长,资源消耗是指数上升的。无论在安全等级还是计算速度,ECC 都占绝对的优势
“Because it gives us almost nothing, while costing us quite a lot/收获很少,却付出了很多”

仅出于个人,我更喜欢 ECDHE_ECDSA,同样是因为密钥更小,传输更快。ECDSA 仅需要 256 位大小的公钥即可提供相当于 RSA 3072 位的安全级别,如果使用 ECC 证书,或许应该首选 ECDHE_ECDSA

在曲线的选择上应尽量避开 NIST 的几条曲线,以 X25519 优先,X25519 在设计上是完全透明的并且安全性更好

参考
Halfrost-Field 冰霜之地 - Protocol
Tech Explorer - TLS协议分析 与 现代加密通信协议设计
Jerry Qu - 开始使用 ECC 证书
Shell’s Home - 安全协议的设计