[ssl_ecdh_curve] Post-quantum groups stop NGINX from starting on older OpenSSL
What this check looks for
This plugin flags ssl_ecdh_curve values that name a post-quantum group without the ? prefix — anything containing MLKEM or KYBER, such as X25519MLKEM768, SecP256r1MLKEM768, or the pre-standard X25519Kyber768Draft00.
Why this is a problem
NGINX hands the value of ssl_ecdh_curve straight to OpenSSL:
/* src/event/ngx_event_openssl.c */
if (SSL_CTX_set1_curves_list(ssl->ctx, (char *) name->data) == 0) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"SSL_CTX_set1_curves_list(\"%s\") failed", name->data);
return NGX_ERROR;
}
Two properties combine badly:
- OpenSSL rejects the entire list when a single group name is unknown. There is no partial acceptance —
X25519MLKEM768:X25519:prime256v1fails as a whole, even though the other two groups are universally supported. - NGINX treats that as
NGX_LOG_EMERGand returns an error, so the configuration never loads. A cold start fails outright; a reload logs the error and silently keeps serving the old configuration, so a change you believe you deployed was never applied.
This is a startup failure, not a graceful downgrade to a classical curve.
Post-quantum hybrid groups arrived in OpenSSL 3.5. The distributions most production NGINX runs on ship older:
| Distribution | OpenSSL | X25519MLKEM768 |
|---|---|---|
| Debian 12 (bookworm) | 3.0 | NGINX will not start |
| Ubuntu 24.04 LTS | 3.0 | NGINX will not start |
| RHEL / AlmaLinux / Rocky 9 | 3.2 | NGINX will not start |
Check what you actually have with openssl version; openssl list -tls-groups shows the exact set the linked library accepts.
Bad configuration
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
}
On OpenSSL 3.0:
$ nginx -t
nginx: [emerg] SSL_CTX_set1_curves_list("X25519MLKEM768:X25519:prime256v1") failed
(SSL: error:0A080106:SSL routines::passed invalid argument:group
'X25519MLKEM768' cannot be set)
nginx: configuration file /etc/nginx/nginx.conf test failed
Better configuration
If every target runs OpenSSL 3.3 or newer, prefix the post-quantum groups with ?. OpenSSL then skips names it does not recognise instead of failing the list:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_ecdh_curve ?X25519MLKEM768:X25519:prime256v1;
}
A host with OpenSSL 3.5+ negotiates X25519MLKEM768; one with 3.3 or 3.4 falls back to X25519 and keeps serving.
The ? prefix has its own version gate
? was added in OpenSSL 3.3. On OpenSSL 3.0 and 3.2, ?X25519MLKEM768 is itself an unparseable group name and fails in exactly the same way:
$ nginx -t
nginx: [emerg] SSL_CTX_set1_curves_list("?X25519MLKEM768:X25519") failed
(SSL: error:0A080106:SSL routines::passed invalid argument:group
'?X25519MLKEM768' cannot be set)
So ? is not a universal escape hatch:
- OpenSSL 3.5+ — name post-quantum groups directly, or with
?. - OpenSSL 3.3 / 3.4 — use
?so the configuration is forward-compatible. - OpenSSL 3.0 / 3.2 (Debian 12, Ubuntu 24.04, RHEL 9) — do not name post-quantum groups at all. Leave
ssl_ecdh_curve auto;, which is the default, or list only classical curves.
If one configuration is deployed to a mixed fleet, gate the directive at build or template time rather than assuming ? saves you.
Additional notes
ssl_ecdh_curve auto;is special-cased by NGINX and returned early before OpenSSL sees a list, so it is never flagged.- Classical curve lists (
X25519:prime256v1:secp384r1) are not flagged. - Groups that already carry the
?prefix are not flagged, on the assumption that a config author who wrote?knows the OpenSSL 3.3 requirement. X25519Kyber768Draft00and the other pre-standard Kyber draft names were never accepted by upstream OpenSSL at all — not even 3.5+ — so they are flagged regardless of how new the library is.