# OpenSSL dh key too small: fix weak DHE on the server

> dh key too small means the server offered Diffie-Hellman parameters below your OpenSSL security level. Use ECDHE or 2048-bit DH on the server.

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

**`error:0A00018A:SSL routines::dh key too small` means the server picked a DHE cipher suite and sent Diffie-Hellman parameters smaller than your OpenSSL security level allows.** The client is behaving correctly. Fix the server: prefer ECDHE or TLS 1.3, or use DH parameters of 2048 bits or more. On the client, offer only ECDHE suites.

## What the error means

The server offered DH parameters below your OpenSSL security level, so the client refused them. In a TLS 1.2 handshake with a `DHE-*` cipher suite, the server sends a prime and generator that both sides use for a Diffie-Hellman key exchange. If that prime is small, an attacker with enough computing power can break the exchange; the 2015 Logjam research broke 512-bit groups in practice and warned that 1024-bit groups are within reach of well-funded attackers (see [Logjam](https://ssl.sh/glossary/logjam/)).

OpenSSL enforces minimum sizes through security levels. According to the OpenSSL manual, level 1 prohibits DH keys shorter than 1024 bits, and level 2 prohibits DH keys shorter than 2048 bits. OpenSSL 3.2 changed the library default from level 1 to level 2, and a distribution or administrator can raise the level further in `openssl.cnf`. That is why a server whose 1024-bit DH file worked for years suddenly fails after a client upgrade.

The same reason appears in different tools:

```text
error:0A00018A:SSL routines::dh key too small            # openssl, curl (exit code 35)
ssl.SSLError: [SSL: DH_KEY_TOO_SMALL] dh key too small (_ssl.c:1006)   # Python
```

## Causes, most common first

### 1. The server is configured with old 1024-bit (or smaller) DH parameters

The usual sources are a `dhparam.pem` generated with 1024 bits years ago, a default from an old distribution, or an appliance with fixed parameters. On Nginx this can only happen when `ssl_dhparam` is set, because since Nginx 1.11.0 DHE suites are not used without it.

- **Confirm:** see what the server sends. Lowering the security level here is only for diagnosis:

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

On the server itself: `openssl dhparam -in /etc/nginx/dhparam.pem -noout -text | head -n 1`.

- **Fix:** regenerate with at least 2048 bits, or remove DHE suites (commands below).

### 2. The server offers only DHE suites, or prefers them

If the server's cipher list has no ECDHE suites, or it enforces its own order with DHE first, the client cannot avoid DHE. The [SSL checker](https://ssl.sh/tools/ssl-checker/) lists the suites and their order; the [cipher suite lookup](https://ssl.sh/tools/cipher-suite-lookup/) explains each one.

- **Fix:** put ECDHE AEAD suites first and enable TLS 1.3.

### 3. The client's security level went up

Upgrading to OpenSSL 3.2 or later, or to a distribution that sets `CipherString = DEFAULT@SECLEVEL=2`, raises the bar from 1024 to 2048 bits. This is the trigger, not the bug.

- **Confirm:** `openssl version` and `grep -i seclevel "$(openssl version -d | cut -d'"' -f2)/openssl.cnf"`.

### 4. Mail and other non-HTTPS servers

SMTP, IMAP, LDAP and database servers often have their own, older DH settings. Test them with STARTTLS, for example `openssl s_client -connect mail.example.com:587 -starttls smtp`, or use the [STARTTLS checker](https://ssl.sh/tools/starttls-checker/).

## Fix commands by platform

Every server fix removes small DH groups: prefer ECDHE, or set 2048-bit parameters.

**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;
# Either drop DHE entirely (as above), or keep it with a 2048-bit file:
# ssl_dhparam /etc/nginx/dhparam.pem;
```

Then `nginx -t && systemctl reload nginx`.

**Apache httpd 2.4.7+:** append 2048-bit parameters generated with `openssl dhparam 2048` to the end of the first file named in `SSLCertificateFile`, or remove DHE from `SSLCipherSuite`.

**Java servers (Tomcat, Jetty, Spring Boot):** start the JVM with `-Djdk.tls.ephemeralDHKeySize=2048`, and keep ECDHE suites enabled.

**Appliances and hosted services:** update the firmware or pick a newer TLS policy; the [TLS config generator](https://ssl.sh/tools/tls-config-generator/) shows a modern baseline.

**Client side, when you cannot change the server yet:** offer only ECDHE suites, which keeps the security level intact:

```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 suites are unaffected
```

## Don't disable certificate verification

`curl -k` or `verify=False` will not help: the failure happens during key exchange, not certificate validation. They only add the risk that someone in the path impersonates the server and reads your traffic.

Avoid the other tempting shortcut too: `DEFAULT@SECLEVEL=1` in a global `openssl.cnf`, or `@SECLEVEL=0`. Level 0 accepts any key size, and lowering the level system-wide also re-enables weak RSA keys and SHA-1 signatures for every program. If a server offers nothing but small DH groups, scope `@SECLEVEL=1` to that single command or context, keep certificate verification on, and file a ticket to fix the server. In OpenSSL 3.x, TLS 1.0 and 1.1 only work at security level 0, so a server that is also stuck on TLS 1.0 needs a server-side fix.

## Verify the fix

```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"
```

With default client settings the first command should show an `ECDH`/`X25519` temp key or `DH, 2048 bits`, and the second should negotiate TLS 1.3. Re-run your original curl or Python command without any cipher overrides; it should succeed. Background on how key exchange and cipher suites fit together is in [cipher suites explained](https://ssl.sh/learn/cipher-suites/).
