How To Enable Cloudflare Post-Quantum X25519Kyber768 Key Exchange Support in Centmin Mod Nginx

Cloudflare turned 13 this year. For Cloudflare Birthday week, they announced support for Post-Quantum key exchange connections (KEX) to Cloudflare origin servers using X25519Kyber768Draft00. For this to work, origin servers need to support Post-Quantum key exchanges. This post outlines how Centmin Mod LEMP stack’s Nginx server can be configured to support Post-Quantum KEX.

Centmin Mod LEMP stack supports various Nginx crypto libraries – OpenSSL 1.1.1 (default), OpenSSL 3.0/3.1, BoringSSL, LibreSSL, QuicTLS OpenSSL 1.1.1/3.0/3.1 and Quiche/BoringSSL For HTTP/3 QUIC. For Cloudflare Post-Quantum key exchange agreement support and connection to origin server using preferred curve, X25519Kyber768Draft00, you need to switch Centmin Mod Nginx from default OpenSSL 1.1.1 to BoringSSL crypto library as outlined below.

1. Switch from default Centmin Mod Nginx built with OpenSSL 1.1.1 crypto library to BoringSSL crypto library by enabling BoringSSL support, by setting in the persistent config file /etc/centminmod/


2. Run cmupdate and menu option 4 commands to update local Centmin Mod code to latest and then recompile Centmin Mod Nginx with BoringSSL

Centmin Mod Menu 130.00beta01
1). Centmin Install
2). Add Nginx vhost domain
3). NSD setup domain name DNS
4). Nginx Upgrade / Downgrade
5). PHP Upgrade / Downgrade
6). Option Being Revised (TBA)
7). Option Being Revised (TBA)
8). Option Being Revised (TBA)
9). Option Being Revised (TBA)
10). Memcached Server Re-install
11). MariaDB MySQL Upgrade & Management
12). Zend OpCache Install/Re-install
13). Install/Reinstall Redis PHP Extension
14). SELinux disable
15). Install/Reinstall ImagicK PHP Extension
16). Change SSHD Port Number
17). Multi-thread compression: zstd,pigz,pbzip2,lbzip2
18). Suhosin PHP Extension install
19). Install FFMPEG and FFMPEG PHP Extension
20). NSD Install/Re-Install
21). Data Transfer (TBA)
22). Add WordPress Nginx vhost + Cache Plugin
23). Update Centmin Mod Code Base
24). Exit
Enter option [ 1 - 24 ] 4
Nginx Upgrade - Would you like to continue? [y/n] y

Current Nginx Version: 1.24.0 (260923-074355-almalinux9-332935a-br-659b4b3)

Install which version of Nginx? (version i.e. type 1.25.2): 1.25.2

Do you still want to continue? [y/n] y

You can ignore warning for nginx: [warn] "ssl_stapling" ignored, not supported as BoringSSL doesn’t support OSCP Stapling.

Check that Nginx version output shows built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)

nginx -V
nginx version: nginx/1.25.2 (290923-192121-almalinux9-332935a-br-659b4b3)
built by gcc 12.2.1 20221121 (Red Hat 12.2.1-7) (GCC)
built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --with-ld-opt='-Wl,-E -L/usr/local/zlib-cf/lib
-L/opt/boringssl/.openssl/lib -lcrypto -lssl -L/usr/local/nginx-dep/lib -lrt
-ljemalloc -Wl,-z,relro
ep/lib -flto=8 -fuse-ld=gold' --with-cc-opt='-I/opt/boringssl/.openssl/include
-I/usr/local/zlib-cf/include -I/usr/local/nginx-dep/include -m64 -march=native
-g -O3 -fstack-protector-strong -flto=8 -fuse-ld=gold --param=ssp-buffer-size=4
-Wformat -Werror=format-security -Wno-pointer-sign -Wimplicit-fallthrough=0
-Wno-implicit-function-declaration -Wno-int-conversion -Wno-error=unused-result
-Wno-unused-result -fcode-hoisting -Wno-cast-function-type
-Wno-format-extra-args -Wp,-D_FORTIFY_SOURCE=2'
--sbin-path=/usr/local/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf
--build=290923-192121-almalinux9-332935a-br-659b4b3 --with-compat
--without-pcre2 --with-http_stub_status_module --with-http_secure_link_module
--with-libatomic --with-http_gzip_static_module
--add-dynamic-module=../ngx_brotli --add-module=../ngx_http_geoip2_module
--with-http_sub_module --with-http_addition_module
--with-http_image_filter_module=dynamic --with-http_geoip_module
--with-stream_geoip_module --with-stream_realip_module
--with-stream_ssl_preread_module --with-threads --with-stream
--with-stream_ssl_module --with-http_realip_module
--add-module=../ngx_http_redis-0.4.0-cmm --add-module=../memc-nginx-module-0.19
--add-dynamic-module=../headers-more-nginx-module-0.34 --with-pcre-jit
--with-zlib=../zlib-cloudflare-1.3.0 --with-http_ssl_module

nginx -t
nginx: [warn] "ssl_stapling" ignored, not supported
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

ldd /usr/local/sbin/nginx (0x00007ffd7494b000) => /opt/boringssl/.openssl/lib/ (0x00007fa90b0ba000) => /opt/boringssl/.openssl/lib/ (0x00007fa90b04c000) => /lib64/ (0x00007fa90ac00000) => /lib64/ (0x00007fa90b005000) => /usr/local/nginx-dep/lib/ (0x00007fa90affe000) => /usr/local/nginx-dep/lib/ (0x00007fa90af85000) => /lib64/ (0x00007fa90af46000) => /lib64/ (0x00007fa90af41000) => /lib64/ (0x00007fa90a800000) => /lib64/ (0x00007fa90a400000) => /lib64/ (0x00007fa90ab25000) => /lib64/ (0x00007fa90af24000)
/lib64/ (0x00007fa90b2b4000)

Notice BoringSSL libraries => /opt/boringssl/.openssl/lib/ (0x00007fa90b0ba000) => /opt/boringssl/.openssl/lib/ (0x00007fa90b04c000)

3. Setup Nginx Post-Quantum Key Exchange Support

In existing Centmin Mod Nginx vhost HTTPS config file i.e. /usr/local/nginx/conf/conf.d/ update the following:

The blog mentions 2 additional directives for ssl_protocols and ssl_prefer_server_ciphers but they’re already enabled by Centmin Mod Nginx in /usr/local/nginx/conf/ssl_include.conf include file

In /usr/local/nginx/conf/ssl_include.conf

ssl_session_cache      shared:SSL:10m;
ssl_session_timeout    60m;
ssl_protocols  TLSv1.2 TLSv1.3;

and /usr/local/nginx/conf/conf.d/ Nginx vhost

ssl_prefer_server_ciphers   on;

Then set ssl_ecdh_curve X25519Kyber768Draft00:X25519. You can do this in two ways:

  1. First method is to set in your existing Nginx HTTPS vhost where you have manually set default_server option in Nginx listen directive. This method takes highest priority when Nginx server reads the ssl_ecdh_curve directive set.
  2. Second method, is to set in /usr/local/nginx/conf/nginx.conf http{} context.

Set in only one Nginx HTTPS vhost where listen directive has set default_server and not in all Nginx HTTPS vhosts. If no existing Nginx HTTPS vhosts have set default_server in listen directive, choose a single Nginx HTTPS vhost’s listen directive and set it.

For Nginx 1.25

  listen 443 ssl default_server;
  http2 on;
  ssl_ecdh_curve X25519Kyber768Draft00:X25519;

For Nginx 1.24

  listen 443 ssl http2 default_server;
  ssl_ecdh_curve X25519Kyber768Draft00:X25519;

Or in /usr/local/nginx/conf/nginx.conf http{} context

http {
 ssl_ecdh_curve X25519Kyber768Draft00:X25519;
 include /usr/local/nginx/conf/brotli_inc.conf;
 map_hash_bucket_size 128;
 map_hash_max_size 4096;
 server_names_hash_bucket_size 128;
 server_names_hash_max_size 2048;
 variables_hash_max_size 2048;

If you have ssl_ecdh_curve set in both, as per, the default_server set values will take priority.

  • In OpenSSL before 1.0.2 (before introduction of curve lists), the specified setting properly applied by OpenSSL to name-based virtual servers (as it is stored in cert->ecdh_tmp, and certificates are properly replaced during the SSL context switch in servername callback).
  • In OpenSSL 1.0.2 and above, the list of configured curves is copied from the SSL context when the connection is created (see SSL_new()). As such, configuration from the default server applies to all name-based virtual servers. It is possible to re-apply the setting to the connection in the servername callback though (but see below).
  • In OpenSSL 1.1.1 and above (tested up to OpenSSL 3.1.2) when using TLSv1.3, attempts to re-apply the setting results in handshake failure on the client
  • In BoringSSL, the list of configured curves is copied from the SSL context when the connection is created (similarly to OpenSSL 1.0.2, see SSL_new()). It does, however, work properly with TLSv1.3 when redefined in the servername callback.
  • In LibreSSL (tested with LibreSSL 3.8.0), the list of configured curves is copied from the SSL context when the connection is created (similarly to OpenSSL 1.0.2, see SSL_new()). With TLSv1.2, it does work properly when redefined in the servername callback. With TLSv1.3, attempts to redefine the list of curves in the servername callback are ignored, the configuration from the original SSL context is used instead.

Once updated, run nginx config check

nginx -t
nginx: [warn] "ssl_stapling" ignored, not supported
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

Looks good ignoring OCSP stapling that isn’t supported by BoringSSL.

4. Test Centmin Mod Nginx BoringSSL with Post-Quantum support using bssl client

Use bssl client to test Post-Quantum key exchange support in Centmin Mod Nginx built with BoringSSL.

With default_server set in the Nginx HTTPS vhost config file’s listen directive, I now see the ECDHE group report X25519Kyber768Draft00. If Centmin Mod Nginx doesn’t support Post-Quantum key exchange, it will report X25519 curve instead.

Replace domain assigned variable with your domain name.
echo | bssl client -server-name $domain -connect $domain:443 -curves X25519:X25519Kyber768Draft00 -debug

Example output
echo | bssl client -server-name $domain -connect $domain:443 -curves X25519:X25519Kyber768Draft00 -debug
Connecting to myserverip:443
Handshake started.
Handshake progress: TLS client enter_early_data
Handshake progress: TLS client read_server_hello
Handshake progress: TLS 1.3 client read_hello_retry_request
Handshake progress: TLS 1.3 client read_server_hello
Handshake progress: TLS 1.3 client read_encrypted_extensions
Handshake progress: TLS 1.3 client read_certificate_request
Handshake progress: TLS 1.3 client read_server_certificate
Handshake progress: TLS 1.3 client read_server_certificate_verify
Handshake progress: TLS 1.3 client read_server_finished
Handshake progress: TLS 1.3 client send_end_of_early_data
Handshake progress: TLS 1.3 client send_client_encrypted_extensions
Handshake progress: TLS 1.3 client send_client_certificate
Handshake progress: TLS 1.3 client complete_second_flight
Handshake progress: TLS 1.3 client done
Handshake progress: TLS client finish_client_handshake
Handshake progress: TLS client done
Handshake done.
  Version: TLSv1.3
  Resumed session: no
  Cipher: TLS_AES_128_GCM_SHA256
  ECDHE group: X25519Kyber768Draft00
  Signature algorithm: ecdsa_secp256r1_sha256
  Secure renegotiation: yes
  Extended master secret: yes
  Next protocol negotiated: 
  ALPN protocol: 
  OCSP staple: no
  SCT list: no
  Early data: no
  Encrypted ClientHello: no
  Cert subject: CN =
  Cert issuer: C = US, O = Let's Encrypt, CN = R3

If you do not set ssl_ecdh_curve in either default_server listen directive for one Nginx HTTPS vhost or in nginx.conf http{} context, then bssl tests may return an error like below despite having set ssl_ecdh_curve in a regular Nginx HTTPS vhost (not using default_server listen directive):

Connecting to myserverip:443
Error while connecting: SSLV3_ALERT_HANDSHAKE_FAILURE
25938144:error:10000410:SSL routines:OPENSSL_internal:SSLV3_ALERT_HANDSHAKE_FAILURE:/opt/boringssl/ssl/ alert number 40
25938144:error:1000009a:SSL routines:OPENSSL_internal:HANDSHAKE_FAILURE_ON_CLIENT_HELLO:/opt/boringssl/ssl/

and Nginx error log debug shows

2023/09/30 11:01:22 [info] 1731475#1731475: *1 SSL_do_handshake() failed (SSL: error:1000010a:SSL routines:OPENSSL_internal:NO_SHARED_GROUP) while SSL handshaking, client: myip, server:

which indicates that during the SSL/TLS handshake, the client and server couldn’t agree on a shared elliptic curve group for key exchange.

Seems Cloudflare already has determined my test Centmin Mod Nginx origin with BoringSSL has Post-Quantum key exchange support and is communicating with my origin via X25519Kyber768Draft00. Cloudflare is scanning all active origins for support of Post-Quantum key exchange agreement every 24 hours,. Cloudflare will attempt a series of about ten TLS connections to Cloudflare origin server, to test support and preferences for the various key exchange agreements.

From my Centmin Mod Nginx access logs in JSON format, shows ssl_curve for the request from Cloudflare orange cloud enabled site = X25519Kyber768Draft00

cat access.json | jq -c | tail -1 | jq -r
  "msec": "1696111325.536",
  "connection": "1",
  "connection_requests": "1",
  "pid": "1743334",
  "request_id": "e52516e5a82687420646a8b81c0d8e76",
  "request_length": "650",
  "remote_addr": "",
  "remote_user": "",
  "remote_port": "10544",
  "time_local": "30/Sep/2023:22:02:05 +0000",
  "time_iso8601": "2023-09-30T22:02:05+00:00",
  "request": "GET / HTTP/2.0",
  "request_uri": "/",
  "args": "",
  "status": "304",
  "body_bytes_sent": "0",
  "bytes_sent": "177",
  "http_referer": "",
  "http_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ Safari/537.36 OPR/",
  "http_x_forwarded_for": "2a09:xxx:xxx:xxx:xxx:xxx",
  "http_host": "",
  "server_name": "",
  "request_time": "0.000",
  "upstream": "",
  "upstream_connect_time": "",
  "upstream_header_time": "",
  "upstream_response_time": "",
  "upstream_response_length": "",
  "upstream_cache_status": "",
  "ssl_protocol": "TLSv1.3",
  "ssl_session_reused": ".",
  "ssl_cipher": "TLS_AES_128_GCM_SHA256",
  "ssl_curve": "X25519Kyber768Draft00",
  "ssl_curves": "",
  "scheme": "https",
  "request_method": "GET",
  "server_protocol": "HTTP/2.0",
  "pipe": ".",
  "gzip_ratio": "",
  "http_cf_ray": "80efbf881ea0e9bf-BNE",
  "http_cf_worker": "",
  "http_cf_request_id": "",
  "http_cf_railgun": "",
  "http_accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"

Chrome 116+ based also support experimental Post-Quantum X25519 Kyber768 key exchanges. Here’s an example enabled in Opera 102 browser for above test site behind Cloudflare HTTP/3 QUIC based X25519 Kyber768 draft00 key exchange connection.

Opera Post-Quantum X25519 Kyber 768 KEX

Opera Post-Quantum X25519 Kyber 768 KEX

5. Enabling Cloudflare Outbound Connection Post-Quantum Key Exchange Support

Cloudflare is rolling out support for X25519+Kyber for most outbound connections, including origin servers and Cloudflare Workers fetch() calls as per this schedule.

PlanSupport for post-quantum outbound connections
FreeStarted roll-out. Aiming for 100% by the end of the October.
Pro and BusinessStarted roll-out. Aiming for 100% by the end of year.
EnterpriseStart roll-out February 2024. 100% by March 2024

You can skip the roll-out and opt-in your Cloudflare domain zone today, or opt-out ahead of time, using an API described below. Before rolling out this support for enterprise customers in February 2024, a toggle on the dashboard to opt out will be added.

To enable a Post-Quantum connection between Cloudflare and your origin server today, opt-in your Cloudflare domain zone to skip the gradual roll-out you can switch from the default value of default to preferred – though my free Cloudflare plan domain zone seems to default to a value of supported:

curl --request PUT \
  --url \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer (API token)' \
  --data '{"value": "preferred"}'

Replace (zone_id) and (API token) appropriately. Then, make sure your server supports TLS 1.3; enable and prefer the key agreement X25519Kyber768Draft00; and ensure it’s configured as outlined in above mentioned Centmin Mod Nginx instructions.

The following values are supported:

supportedAdvertise support for post-quantum key agreement, but send a classical keyshare in the first ClientHello.When the origin supports and prefers X25519+Kyber, a post-quantum connection will be established, but it incurs an extra roundtrip.This is the most compatible way to enable post-quantum.
preferredSend a post-quantum keyshare in the first ClientHello.When the origin supports X25519+Kyber, a post-quantum connection will be established without an extra roundtrip.This is the most performant way to enable post-quantum.
offDo not send or advertise support for post-quantum key agreement to the origin.
(default)Allow us to determine the best behavior for your zone. (More about that later.)

Additional Notes:

Thanks to Bas Westerbaan from Cloudflare for these additional notes.

  • If you change your Cloudflare domain zone from default setting value of supported to preferred, if an origin server does not support Post-Quantum key exchange via X25519Kyber768Draft00, Cloudflare will fallback to using a supported curve like X25519 for key exchange. An attacker cannot use this to downgrade the connection between Cloudflare and an origin server that does support Post-Quantum KEX – as long as there is not a commonly supported signature scheme that is broken at the time of the handshake.
  • Cloudflare Tunnels have already been updated to support Post-Quantum KEX for connection between Cloudflare and the Tunnel running on origin server if the Cloudflare Tunnel is configured to support quic protocol instead of http2 protocol. FYI, for those interested I did benchmark Cloudflare Tunnels http2 vs quic protocol connections.
  • Cloudflare intends to update Cloudflare Tunnels with http2 protocol to support Post-Quantum KEX between Cloudflare and Tunnel installed at origin once Cloudflare upgrades to Go 1.21+.
  • Currently, for Cloudflare Tunnel to origin connection segment, Post-Quantum KEX is not yet supported.
  • To follow the latest developments of Cloudflare’s deployment of Post-Quantum cryptography, and client/server support, check out and a reminder to share your experiences, or reach out to Cloudflare for any questions:

Optimal Cloudflare To Origin Configuration:

I did some automated Centmin Mod Nginx crypto library benchmarks comparing performance of supported Nginx OpenSSL 1.1.1 vs OpenSSL 3.0 vs QuicTLS OpenSSL 1.1.1 fork vs BoringSSL, and seems Nginx built with BoringSSL is slightly faster. BoringSSL doesn’t support OCSP stapling or dual RSA 2048bit + ECDSA 256bit SSL certificates like OpenSSL does, but if you’re running Centmin Mod Nginx as an origin behind Cloudflare CDN edge servers, then it shouldn’t matter when you’re setup with Cloudflare FULL/FULL Strict SSL modes.

So switching to Cloudflare Post-Quantum key exchange agreement connections to Centmin Mod Nginx origin built with BoringSSL seems like a good combination for both security and performance.

Test done on 2x CPU core Azure Ubuntu 20 instance via Github Workflow automated action tests for Centmin Mod LEMP stack on AlmaLinux 8.8 Docker container using Intel Xeon Platinum 8272CL CPU @2.60GHz using respective openssl and BoringSSL bssl binaries’ speed tests in single threaded mode.

LibraryRSA 2048 Sign/sRSA 2048 Verify/sECDSA 256-bit Sign/sECDSA 256-bit Verify/s
OpenSSL 1.1.11433.249688.040379.613020.1
OpenSSL 3.01465.350169.638818.713159.2
QuicTLS 1.1.11468.649766.640310.413026.5
BoringSSL1482.054519.4 (same key)45143.916039.9

OpenSSL 1.1.1

nginx version: nginx/1.25.2 (061023-210846-almalinux8-hyperv-docker-42d655b)
built by gcc 11.2.1 20220127 (Red Hat 11.2.1-9) (GCC) 
built with OpenSSL 1.1.1w  11 Sep 2023
Benchmarking OpenSSL openssl111...
rsa 2048 bits signs/s: 1433.2
rsa 2048 bits verify/s: 49688.0
256 bits ecdsa (nistp256) signs/s: 40379.6
256 bits ecdsa (nistp256) verify/s: 13020.1

OpenSSL 3.0.11

nginx version: nginx/1.25.2 (061023-211918-almalinux8-hyperv-docker-42d655b)
built by gcc 11.2.1 20220127 (Red Hat 11.2.1-9) (GCC) 
built with OpenSSL 3.0.11 19 Sep 2023
Benchmarking OpenSSL openssl30...
rsa 2048 bits signs/s: 1465.3
rsa 2048 bits verify/s: 50169.6
256 bits ecdsa (nistp256) signs/s: 38818.7
256 bits ecdsa (nistp256) verify/s: 13159.2

QuicTLS OpenSSL 1.1.1 fork

nginx version: nginx/1.25.2 (061023-213612-almalinux8-hyperv-docker-42d655b)
built by gcc 11.2.1 20220127 (Red Hat 11.2.1-9) (GCC) 
built with OpenSSL 1.1.1w+quic  11 Sep 2023
Benchmarking OpenSSL-QUIC...
rsa 2048 bits signs/s: 1468.6
rsa 2048 bits verify/s: 49766.6
256 bits ecdsa (nistp256) signs/s: 40310.4
256 bits ecdsa (nistp256) verify/s: 13026.5


nginx version: nginx/1.25.2 (061023-214630-almalinux8-hyperv-docker-42d655b)
built by gcc 11.2.1 20220127 (Red Hat 11.2.1-9) (GCC) 
built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)
Benchmarking BoringSSL...
Did 1530 RSA 2048 signing operations in 1032358us (1482.0 ops/sec)
Did 55000 RSA 2048 verify (same key) operations in 1008816us (54519.4 ops/sec)
Did 45000 RSA 2048 verify (fresh key) operations in 1001388us (44937.6 ops/sec)
Did 8100 RSA 2048 private key parse operations in 1046914us (7737.0 ops/sec)
Did 46000 ECDSA P-256 signing operations in 1018964us (45143.9 ops/sec)
Did 17000 ECDSA P-256 verify operations in 1059855us (16039.9 ops/sec)