# dh key too small：OpenSSL 拒绝服务器的弱 DH 参数

> dh key too small 表示服务器给出的 Diffie-Hellman 参数低于客户端 OpenSSL 的安全级别。应在服务器改用 ECDHE 或 2048 位以上的 DH 参数。

Source: https://ssl.sh/zh/errors/openssl-dh-key-too-small/
Last updated: 2026-11-10

**`error: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](https://ssl.sh/zh/glossary/logjam/)）。

OpenSSL 用“安全级别”来强制最小长度。按 OpenSSL 手册，级别 1 禁止小于 1024 位的 DH 密钥，级别 2 禁止小于 2048 位的 DH 密钥。OpenSSL 3.2 把库的默认级别从 1 提高到 2，发行版或管理员还可以在 `openssl.cnf` 里设得更高。所以一台用了多年 1024 位 DH 文件的服务器，会在客户端升级后突然连不上。

同一个原因在不同工具里的样子：

```text
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。

- **确认：** 看服务器实际发来的参数。这里临时降低安全级别只是为了诊断：

```bash
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 检测工具](https://ssl.sh/zh/tools/ssl-checker/)会列出套件和顺序，[加密套件查询](https://ssl.sh/zh/tools/cipher-suite-lookup/)可以解释每个套件的含义。

- **修复：** 把 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 检测工具](https://ssl.sh/zh/tools/starttls-checker/)。

## 分平台修复命令

服务器上去掉 DHE 套件，或者把 DH 参数换成 2048 位以上；客户端只在服务器改不了时临时限定 ECDHE。

**Nginx（含宝塔面板）：**

```bash
openssl dhparam -out /etc/nginx/dhparam.pem 2048
```

```nginx
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 配置生成器](https://ssl.sh/zh/tools/tls-config-generator/)给出了一份现代的基准配置。

**客户端（服务器暂时改不了时）：** 只提供 ECDHE 套件，安全级别保持不变：

```bash
curl --ciphers 'ECDHE+AESGCM:ECDHE+CHACHA20' https://example.com/
```

```python
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，就说明修好了。

```bash
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 命令，应当成功。密钥交换和加密套件的关系，见[加密套件原理](https://ssl.sh/zh/learn/cipher-suites/)。
