命令行工具
dh key too small:OpenSSL 拒绝服务器的弱 DH 参数
error:0A00018A:SSL routines::dh key too smallerror:0A00018A:SSL routines::dh key too small 的意思是:服务器选了一个 DHE 加密套件,发来的 Diffie-Hellman 参数比你的 OpenSSL 安全级别允许的还小。 客户端的行为是正确的。修复应该在服务器上做:优先用 ECDHE 或 TLS 1.3,或者把 DH 参数换成至少 2048 位。客户端这边,安全的临时办法是只提供 ECDHE 套件。
这条报错是什么意思
这条报错说明服务器的 DH 参数低于客户端 OpenSSL 安全级别的下限,要修的是服务器。TLS 1.2 握手如果用的是 DHE-* 套件,服务器会发来一个素数和生成元,双方用它们做 Diffie-Hellman 密钥交换。素数太小,算力足够的攻击者就能破解这次交换。2015 年的 Logjam 研究在实际中破解了 512 位的参数组,并指出 1024 位已经在资源充足的攻击者能力范围之内(见 Logjam)。
OpenSSL 用“安全级别”来强制最小长度。按 OpenSSL 手册,级别 1 禁止小于 1024 位的 DH 密钥,级别 2 禁止小于 2048 位的 DH 密钥。OpenSSL 3.2 把库的默认级别从 1 提高到 2,发行版或管理员还可以在 openssl.cnf 里设得更高。所以一台用了多年 1024 位 DH 文件的服务器,会在客户端升级后突然连不上。
同一个原因在不同工具里的样子:
error:0A00018A:SSL routines::dh key too small # openssl、curl(退出码 35)
ssl.SSLError: [SSL: DH_KEY_TOO_SMALL] dh key too small (_ssl.c:1006) # Python
这个错误只跟 TLS 1.2 及更早版本的 DHE 套件有关。双方都支持 TLS 1.3 时,密钥交换使用标准化的命名参数组,不存在“服务器自定义 DH 参数”这回事;双方都支持 ECDHE 时,服务器只要把 ECDHE 排在前面,也不会走到 DHE。所以需要修的通常是“服务器只剩 DHE 可选”或“服务器偏爱 DHE”这两种配置。
常见原因(按出现频率排序)
最常见的是服务器还在用 1024 位或更小的旧 DH 参数。
1. 服务器配置了旧的 1024 位(或更小)DH 参数
多年前用 1024 位生成的 dhparam.pem、老发行版的默认值、或者参数写死的硬件设备。在 Nginx 上只有设置了 ssl_dhparam 才会出现,因为 Nginx 1.11.0 起没有这个配置就不用 DHE。
- 确认: 看服务器实际发来的参数。这里临时降低安全级别只是为了诊断:
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -cipher 'DHE:@SECLEVEL=0' </dev/null 2>/dev/null | grep "Server Temp Key"
# Server Temp Key: DH, 1024 bits
在服务器上:openssl dhparam -in /etc/nginx/dhparam.pem -noout -text | head -n 1。
- 修复: 重新生成至少 2048 位的参数,或者去掉 DHE 套件(命令见下文)。
2. 服务器只提供 DHE 套件,或者把 DHE 排在前面
服务器的套件列表里没有 ECDHE,或者强制使用自己的顺序且 DHE 在前,客户端就躲不开 DHE。SSL 检测工具会列出套件和顺序,加密套件查询可以解释每个套件的含义。
- 修复: 把 ECDHE 的 AEAD 套件放在最前面,并开启 TLS 1.3。
3. 客户端的安全级别提高了
升级到 OpenSSL 3.2 及以上,或者发行版设置了 CipherString = DEFAULT@SECLEVEL=2,下限就从 1024 位提到了 2048 位。这是触发条件,不是缺陷。
- 确认:
openssl version,以及grep -i seclevel "$(openssl version -d | cut -d'"' -f2)/openssl.cnf"。
4. 邮件服务器和其他非 HTTPS 服务
SMTP、IMAP、LDAP、数据库服务器往往有自己的、更老的 DH 设置。用 STARTTLS 测试,比如 openssl s_client -connect mail.example.com:587 -starttls smtp,或者用 STARTTLS 检测工具。
分平台修复命令
服务器上去掉 DHE 套件,或者把 DH 参数换成 2048 位以上;客户端只在服务器改不了时临时限定 ECDHE。
Nginx(含宝塔面板):
openssl dhparam -out /etc/nginx/dhparam.pem 2048
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# 要么像上面这样完全去掉 DHE,要么保留 DHE 并使用 2048 位参数文件:
# ssl_dhparam /etc/nginx/dhparam.pem;
然后执行 nginx -t && systemctl reload nginx。宝塔面板里在网站的“配置文件”中修改同样的指令,保存后重载 Nginx。
Apache httpd 2.4.7 及以上: 用 openssl dhparam 2048 生成参数,追加到 SSLCertificateFile 指定的第一个文件末尾;或者从 SSLCipherSuite 里去掉 DHE。
Java 服务器(Tomcat、Jetty、Spring Boot): JVM 启动参数加 -Djdk.tls.ephemeralDHKeySize=2048,同时保留 ECDHE 套件。
硬件设备和云服务: 升级固件,或者在云负载均衡、CDN 的 HTTPS 配置里选择较新的 TLS 安全策略。TLS 配置生成器给出了一份现代的基准配置。
客户端(服务器暂时改不了时): 只提供 ECDHE 套件,安全级别保持不变:
curl --ciphers 'ECDHE+AESGCM:ECDHE+CHACHA20' https://example.com/
import ssl
ctx = ssl.create_default_context()
ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20") # 不影响 TLS 1.3 套件
国内常见场景: 对接一些多年前上线的老接口、自建邮件服务器或者内网设备时最容易遇到。客户端换到了带 OpenSSL 3.2 以上的新系统或新版 Docker 镜像,而对方服务器多年没动过,这正是“昨天还好好的”那种情况。先用上面的 Server Temp Key 命令拿到证据,再发给对方运维。
不推荐:关闭证书校验
curl -k、verify=False 没有任何帮助:失败发生在密钥交换阶段,和证书校验无关。它们只会额外引入风险,让链路上的人可以冒充服务器、读取你的流量。
也不要在全局 openssl.cnf 里写 DEFAULT@SECLEVEL=1,或者用 @SECLEVEL=0。级别 0 接受任何长度的密钥;全系统降低级别,还会让所有程序重新接受弱 RSA 密钥和 SHA-1 签名。如果某台服务器确实只提供小 DH 参数组,就把 @SECLEVEL=1 限定在那一条命令或那一个上下文里,证书校验保持开启,同时提工单让对方修服务器。另外,在 OpenSSL 3.x 中 TLS 1.0 和 1.1 只能在安全级别 0 下使用,服务器如果同时还停留在 TLS 1.0,只能在服务器端解决。
怎么确认已经修好
客户端在默认设置下握手成功,协商出 ECDHE、X25519 或 2048 位以上的 DH,就说明修好了。
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null 2>/dev/null | grep -E "Server Temp Key|Cipher is"
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null 2>/dev/null | grep -E "Protocol|Cipher is"
在默认客户端设置下,第一条命令应该显示 ECDH/X25519 临时密钥或 DH, 2048 bits,第二条应该协商出 TLS 1.3。再不加任何套件参数重跑原来的 curl 或 Python 命令,应当成功。密钥交换和加密套件的关系,见加密套件原理。
常见问题
为什么浏览器能打开,curl 和 Python 却报错?
Chrome 在 Chrome 53(2016 年)移除了 DHE 加密套件,现在的浏览器都协商 ECDHE 或 TLS 1.3,碰不到服务器那组弱 DH 参数。基于 OpenSSL 的工具默认仍然提供 DHE 套件,服务器选中它以后,安全级别检查就会拒绝这把小密钥。
2048 位 DH 够用吗?
2048 位满足 OpenSSL 安全级别 2(112 位安全强度)。用 X25519 或 P-256 的 ECDHE 更快也更强,TLS 1.3 更是不再使用自定义 DH 参数,所以现代配置大多直接关闭 DHE。
客户端加一个 @SECLEVEL=1 行不行?
只能作为针对单台主机的短期措施。它同时会重新允许 1024 位 RSA 密钥、握手中的 SHA-1 签名等其他弱参数。把客户端限制为只用 ECDHE 套件通常就能连上,而且不降低任何安全性,因为支持 DHE 的服务器几乎都同时支持 ECDHE。
Java 客户端遇到同样的服务器会报什么?
Java 客户端受 jdk.tls.disabledAlgorithms 约束,其中包含 DH keySize < 1024,DH 参数过小的服务器同样会握手失败,只是报的是 SSLHandshakeException 而不是这条 OpenSSL 消息。Java 服务器可以用 -Djdk.tls.ephemeralDHKeySize=2048 提高自己的临时 DH 长度。
资料来源
- OpenSSL: SSL_CTX_set_security_level (security levels)
- OpenSSL 3.2 NEWS (default security level changed to 2)
- RFC 7919: Negotiated Finite Field DHE Parameters for TLS
- nginx: ngx_http_ssl_module (ssl_dhparam)
- Apache httpd: mod_ssl (custom DH parameters)
- Chrome 53 deprecations and removals (DHE-based ciphers)