使用TLS-ALPN-01验证签发证书

背景

Let’s Encrypt提供了3类验证(Challenge)方式,用于颁发证书:

  • HTTP-01:通过HTTP访问服务器80端口的.well-known/acme-challenge验证。
  • DNS-01:在DNS中添加_acme-challenge开头的TXT记录,这种方式因为能签发通配符证书(Wildcard)而被大范围使用。
  • TLS-SNI-01TLS-ALPN-01:通过TLS的方式对443端口访问进行验证。

DNS-01HTTP-01能在极少改动服务器配置的情况下,完成验证。然而,对应某些特殊环境下,80端口难以开放或DNS记录难以更改,只能通过443端口验证。

TLS-SNI-01的漏洞

TLS-SNI-01,顾名思义,是使用SNI进行验证。通过配置特定域名的SNI(例如773c7d.13445a.acme.invalid),生成临时证书进行验证。

然而,对于共享同一个IP的虚拟主机,一旦没有上传证书的SNI验证,攻击者就能轻而易举通过指向同一IP的域名的证书颁发验证。

因此,Let’s Encrypt在2018.1.9收到报告停止了新证书颁发。2019.2.13,TLS-SNI-01验证将被终止

替代

TLS-SNI-02和TLS-SNI-01具有同样的问题,TLS-SNI-03还在开发中,TLS-SNI验证短期内难以再次使用。

TLS-ALPN-01给出了一种替代。ALPN(应用层协议协商),是在HTTP/2中被引入、通过HTTPS进行协议协商的机制。TLS-ALPN-01利用了这个机制,将协议设为acme-tls进行验证。这避免了SNI的问题。2018.7.13,TLS-ALPN-01可用于Let’s Encrypt生产环境验证。

然而,根据讨论可以看出,TLS-ALPN-01支持的客户端非常少。对于提供自动HTTPS加密的Caddy,TLS-ALPN-01支持依旧未能合并。因此,下面我将谈谈的Nginx方案。

原理

Nginx不仅是HTTP/HTTPS服务器,跟提供了全面的TLS、UDP甚至是邮件协议支持。

Nginx的ngx_stream_ssl_preread_module模块提供了ClientHello访问。通过$ssl_preread_alpn_protocols变量,即可实现不同协议的分流。

配置

使用包含ngx_stream_ssl_preread_module的Nginx官方源安装Nginx。

参考dehydrated的TLS-ALPN-01配置

  1. 配置nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
stream {
tcp_nodelay on;
map $ssl_preread_alpn_protocols $tls_addr {
~\bacme-tls/1\b 127.0.0.1:10443; # TLS-ALPN-01验证服务的地址
default 127.0.0.1:8443; # HTTPS服务地址,此处以本机8443为例
}
server {
listen 443;
listen [::]:443;
proxy_pass $tls_addr; # 使用对应变量进行访问
proxy_protocol on; # 开启PROXY protocol,见后文
ssl_preread on;
}
}
  1. 安装dehydrated并配置
1
2
3
4
5
6
wget -O /usr/sbin/dehydrated -c https://raw.githubusercontent.com/lukas2511/dehydrated/master/dehydrated
chmod +x /usr/sbin/dehydrated

wget -O /etc/dehydrated/config -c https://github.com/lukas2511/dehydrated/raw/master/docs/examples/config
wget -O /etc/dehydrated/domains.txt -c https://github.com/lukas2511/dehydrated/raw/master/docs/examples/domains.txt
wget -O /etc/dehydrated/hook.sh -c https://github.com/lukas2511/dehydrated/raw/master/docs/examples/hook.sh

config修改如下参数

1
2
CHALLENGETYPE="tls-alpn-01"
HOOK="${BASEDIR}/hook.sh"

修改domains.txt中为所需域名。

  1. 配置Python验证服务

页面上的Example responder保存到/etc/dehydrated/tls.py

值得注意的是,FALLBACK的两个参数中的ssl-snakeoil证书是由ssl-cert这一包生成的,可以安装其来生成或自行处理。

接着,将其配置为服务tls-alpn.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=Python responder for tls-alpn-01

[Service]
Restart=on-abnormal

; User and group the process will run as.
User=root
Group=root

; 使用本机的Python3地址
ExecStart=python3 /etc/dehydrated/tls.py
ExecReload=/bin/kill -USR1 $MAINPID

[Install]
WantedBy=multi-user.target
  1. 配置钩子(hook)实现自动化

修改hook.sh中的以下函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
deploy_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

systemctl start tls-alpn
}

clean_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

systemctl stop tls-alpn
}

deploy_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"

cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
systemctl reload nginx
}

/usr/sbin/dehydrated -c放入cron中定时运行即可实现自动更新证书。

  1. 进行证书获取
1
2
dehydrated --register --accept-terms
dehydrated -c
  1. 配置证书
1
2
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
  1. 配置PROXY protocol以实现客户端IP获取

Haproxy提出的PROXY protocol能实现TLS的客户端IP等信息的安全传递。

在http的server中,修改下列配置:

1
2
3
4
5
6
7
# 启用proxy_protocol监听
listen 8443 ssl http2 proxy_protocol;

# 记录proxy_protocol_addr变量
log_format main '$proxy_protocol_addr ( $remote_addr ) - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

结语

certbot从v0.26.0开始,进行了不完整支持,同上面类似,需要搭配cme-alpn-proxy等服务端食用。

但是这种使用ssl_preread分流的做法,需要PROXY protocol才能接近原效果,也只有为数不多的Web服务器支持。希望Caddy能早日集成TLS-ALPN-01支持。