Choosing TLS Versions and Cipher Suites in OpenSSL 3.x: A Decision Guide
A decision guide for configuring TLS protocol versions and cipher suites in OpenSSL 3.x: enable TLS 1.2 and 1.3 only, understand how cipher selection differs between versions, deploy dual RSA/ECDSA certificates, and validate with concrete openssl commands.
13 Aug 2026, 07:54 UTC

The Decision: Enable TLS 1.2 and 1.3 Only
For new server deployments in 2024 and later, the practical baseline is to enable TLS 1.2 and TLS 1.3 while explicitly disabling TLS 1.0 and TLS 1.1. RFC 8996 deprecated the older versions in 2021, and major browsers and libraries have removed support. The remaining question is whether to drop TLS 1.2 later—only do so after confirming that no legitimate clients require it.
Constraints That Shape the Choice
- Legacy client support: Pre-2018 libraries (Java 7, older OpenSSL 1.0.x, some embedded firmware) cannot negotiate TLS 1.3. Audit actual
User-Agentand TLS version logs before enforcing TLS 1.3-only. - Compliance mandates: PCI DSS 4.0, NIST SP 800-52 Rev. 2, and BSI TR-02102-2 all require TLS 1.2+ with approved cipher suites. FIPS 140-2/3 validated modules further restrict algorithms.
- Certificate infrastructure: ECDSA (P-256) certificates are smaller and faster to verify than RSA-2048, but some legacy clients only trust RSA. Dual RSA+ECDSA deployment lets modern clients use ECDSA while older ones fall back to RSA.
- Provider model: OpenSSL 3.x loads algorithms through providers. The default provider covers all standard TLS algorithms; the FIPS provider restricts available ciphers and key sizes. Do not assume a default build is FIPS-capable.
Protocol Version Comparison
| Version | Status | Forward Secrecy | Cipher Configuration | Typical Use Case |
|---|---|---|---|---|
| TLS 1.0 / 1.1 | Deprecated (RFC 8996) | Optional, often missing | Cipher string (legacy) | Disable everywhere |
| TLS 1.2 | Widely supported | Depends on suite (ECDHE = yes) | Cipher string via SSL_CTX_set_cipher_list | Required for legacy clients |
| TLS 1.3 | Mandatory for new deployments | Always (ephemeral keys only) | Fixed suites via SSL_CTX_set_ciphersuites | Default for modern clients |
How Cipher Selection Differs Between TLS 1.2 and 1.3
This is the most common source of misconfiguration. In OpenSSL 3.x:
- TLS 1.2 and below use the traditional cipher list string (e.g.,
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384). This string is ignored for TLS 1.3 connections. - TLS 1.3 has a fixed set of five ciphersuites defined in RFC 8446. You enable or disable them with a separate API (
SSL_CTX_set_ciphersuites) or theCiphersuitesconfig option. The three most common are:TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_GCM_SHA256
All TLS 1.3 suites provide forward secrecy and authenticated encryption. There is no RSA key transport, no CBC mode, and no static Diffie-Hellman in TLS 1.3.
Recommended TLS 1.2 Cipher String
For TLS 1.2, prefer ECDHE key exchange with AES-GCM or ChaCha20-Poly1305. Avoid CBC-mode suites (padding oracle history) and RSA key transport (no forward secrecy). A practical string for OpenSSL 3.x:
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:
ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:
ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
Order matters: the list is negotiated in preference order. Place ECDSA suites before RSA if you deploy dual certificates and want modern clients to prefer the smaller, faster ECDSA path.
Concrete Implementation: openssl.cnf Configuration
System-wide defaults are set in the OpenSSL config file. Location varies by build—run openssl version -a and check OPENSSLDIR. Common paths: /etc/ssl/openssl.cnf, /usr/local/ssl/openssl.cnf, or a custom prefix.
Add a [system_default_sect] section (or modify the existing one) to enforce protocol floors and cipher policy:
[system_default_sect]
MinProtocol = TLSv1.2
MaxProtocol = TLSv1.3
CipherSuites = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
Ciphers = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
Options = PrioritizeChaCha
PrioritizeChaCha (available since OpenSSL 1.1.1) lets the server prefer ChaCha20-Poly1305 when the client indicates hardware acceleration for it (common on mobile).
For per-application control, use the programmatic API in your server code:
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
SSL_CTX_set_cipher_list(ctx,
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256");
SSL_CTX_set_ciphersuites(ctx,
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256");
Note: TLS_server_method() is the generic version-flexible method in 3.x. The older TLSv1_2_server_method() style is deprecated.
Certificate Deployment: Dual RSA + ECDSA
If you control the certificate chain, deploy both an RSA-2048 (or RSA-3072) and an ECDSA P-256 certificate for the same hostname. Configure the server to present both (OpenSSL 3.x supports multiple certificates per SSL_CTX via SSL_CTX_add0_chain_cert and SSL_CTX_add1_chain_cert). Modern clients will select the ECDSA path; legacy clients fall back to RSA.
Verify the chain order: leaf first, then intermediates, root omitted. Use openssl x509 -noout -text -in cert.pem to confirm key type and curve.
FIPS Provider Considerations
If you must run in FIPS mode, load the FIPS provider explicitly (statically or via openssl.cnf). The FIPS provider:
- Disables non-approved algorithms (e.g., TLS 1.3
TLS_CHACHA20_POLY1305_SHA256is not FIPS-approved; only AES-GCM suites remain). - Enforces minimum key sizes (RSA ≥ 2048, ECDSA ≥ P-256).
- Requires a validated module—do not assume a standard OpenSSL 3.x build includes it.
Test with openssl list -providers to confirm the FIPS provider is active and openssl ciphers -v -provider fips to see the restricted suite list.
Validation: Confirm the Deployed Configuration
Before and after deployment, run these checks from a client machine (not the server) to verify what the server actually negotiates.
1. Confirm library version and config directory
openssl version -a
# Note OPENSSLDIR and ENGINESDIR; confirm the config file you edited is the one being read.
2. Test each protocol version explicitly
# Should succeed (TLS 1.3)
openssl s_client -connect your.host:443 -tls1_3
# Should succeed (TLS 1.2)
openssl s_client -connect your.host:443 -tls1_2
# Should fail with "wrong version number" or handshake failure
openssl s_client -connect your.host:443 -tls1_1
openssl s_client -connect your.host:443 -tls1
Check the output lines Protocol : TLSv1.3 or Protocol : TLSv1.2 and Cipher : TLS_AES_256_GCM_SHA384 (or the TLS 1.2 equivalent).
3. Enumerate effective cipher lists before deployment
# TLS 1.2 suites that the cipher string expands to
openssl ciphers -v 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
# TLS 1.3 suites available
openssl ciphers -s -tls1_3 -v
Cross-reference the output with your intended list. A mismatch indicates a syntax error or provider restriction.
4. Verify session resumption behavior
# TLS 1.3: tickets are single-use by default (forward secrecy)
openssl s_client -connect your.host:443 -tls1_3 -sess_out /tmp/sess1.pem
openssl s_client -connect your.host:443 -tls1_3 -sess_in /tmp/sess1.pem
# Second connection should show "Reused, TLSv1.3" only if server allows ticket reuse
# TLS 1.2: session IDs can be reused multiple times
openssl s_client -connect your.host:443 -tls1_2 -sess_out /tmp/sess2.pem
openssl s_client -connect your.host:443 -tls1_2 -sess_in /tmp/sess2.pem
If resumption rates drop after enabling TLS 1.3, check whether your load balancer or ticket key rotation policy aligns with single-use tickets.
Limitations and Rollback
- Cipher string syntax drift: A string tuned for OpenSSL 1.1.1 may silently drop suites in 3.x if algorithm names changed. Always run
openssl ciphers -vafter upgrades. - No rollback for config file changes: Editing
openssl.cnfaffects all applications using the system default context. Test in a staging environment first. If you must revert, restore the previous config file and restart dependent services. - FIPS mode is one-way per process: Once the FIPS provider is activated in a process, you cannot switch back to the default provider without restarting.
- Client audit is ongoing: Schedule quarterly log reviews of negotiated TLS versions. Disable TLS 1.2 only when the legacy client fraction is acceptably low (e.g., < 0.1% of traffic).
Quick Reference Checklist
- Run
openssl version -ato anchor version andOPENSSLDIR. - Set
MinProtocol = TLSv1.2andMaxProtocol = TLSv1.3inopenssl.cnfor via API. - Configure TLS 1.2 cipher string (ECDHE + AES-GCM/ChaCha20 only).
- Configure TLS 1.3 ciphersuites explicitly (AES-256-GCM, ChaCha20, AES-128-GCM).
- Deploy dual RSA+ECDSA certificates if legacy RSA-only clients exist.
- Validate with
openssl s_client -tls1_2,-tls1_3, andopenssl ciphers -v. - Monitor negotiated versions in production logs for at least one full traffic cycle before considering TLS 1.2 removal.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.