Letting people type in raw OpenSSL cipher‐suite strings is a terrible idea.
I’ve seen too many well-meaning admins try to “fix” TLS by pasting in a random string they found on the internet, only to make things worse.
Free-form cipher strings are a bad user interface, modern TLS defaults are usually just fine, and a simple “checkbox” approach where each box adds or removes a property can give you both flexibility and safety.
Why “cipher=foo:bar:baz” Is a Bad User Experience
Think back a few years, when the BEAST and POODLE attacks were all over the news. People panicked and grabbed “RC4” because someone said “RC4 fixes BEAST.” They never stopped to ask “But is RC4 safe?” The answer turned out to be “No, it’s broken, too.” By fiddling with a raw cipher string, lots of admins basically traded one weakness for another.
Here’s the thing: cipher-suite syntax (in OpenSSL or similar libraries) is basically its own tiny programming language. You need to know:
- Which key exchanges exist (ECDHE, DHE, RSA, etc.), and which ones are safe today
- Which bulk ciphers are strong (AES-GCM, ChaCha20-Poly1305) versus which are broken (RC4, 3DES)
- Which hashing algorithms to prefer (SHA-256, SHA-384) versus those to avoid (MD5, SHA-1)
- How to combine or exclude pieces with special symbols (think
!
or+
in OpenSSL’s syntax)
That’s a lot to learn. The minute you let someone write their own string, they usually just Google “best OpenSSL ciphers,” copy a block of text, and paste it into their config. Six months later, they’ve forgotten it’s even there, and by then some of those ciphers are already obsolete or broken.
Today’s TLS libraries such as OpenSSL 1.1.x and above, Go’s crypto/tls
, BoringSSL, etc. already ship with sensible defaults. They favor forward-secure key exchanges (ECDHE or X25519), use AEAD ciphers (AES-GCM or ChaCha20-Poly1305), and reject known-broken options. If you’re using a modern TLS library, 99 percent of the time you should just leave the defaults alone.
When You Might Actually Need to Tweak Ciphers
Now, I’m not saying “never touch your TLS settings.” There are valid reasons to override defaults, mainly compliance requirements. For example:
- FIPS 140-3 often requires you to use only algorithms that have passed NIST’s validation. Some curves, like Curve25519, were only approved in early 2023.
- Minimum security level. Some policies insist on “at least 256 bits of security” in the handshake. That usually means RSA 3072 + AES-256 or P-521 + AES-256 in TLS 1.2, or a curve that actually delivers ≥ 256 bits of ECC strength.
- Forward secrecy is sometimes mandated so that session keys can’t be retroactively compromised.
- There might even be future rules demanding post-quantum key exchange.
In short, if you must obey a checklist of “NIST-approved curves,” or “only 256 bits or higher,” or “use PQ hybrid KEMs,” you do need to control which algorithms get used. But there is a better way than asking people to type in a giant cipher string themselves, especially if you want them to mix “FIPS” and “Post-quantum” together.
Checkboxes Instead of Cryptic Strings
Imagine if, instead of pasting in raw cipher names like ECDHE-RSA-AES128-GCM-SHA256:…
, you simply saw a list of boxes, each one adds or removes a category of algorithms from the allowed set:
- FIPS 140-3 approved
- ≥ 256 bit security
- Forward secrecy
- Post-quantum KEM (experimental)
- Disable legacy RSA key exchange (TLS 1.2 only)
- Disable TLS 1.0 and TLS 1.1
- Disable deprecated/broken ciphers (RC4, 3DES, MD5, SHA-1)
- Include GOST/KCMVP/GMTLS algorithms
Each box corresponds to a property or a category. Checking it means “add these algorithms to my allowed list,” except for boxes labeled “Disable…,” which remove those algorithms. If you check multiple boxes, you get the union of all the “add” categories, minus any “disable” categories. For example:
- If you check FIPS 140-3 approved, you’ll include everything NIST-validated.
- If you also check Post-quantum KEM, you’ll add in any experimental PQ KEMs so you can test hybrid PQ+classical designs, even though those PQ KEMs aren’t FIPS-approved yet.
- If you check Disable legacy RSA key exchange, you’ll remove any
TLS_RSA_*
suites under TLS 1.2. - If you check Disable deprecated/broken ciphers, you’ll remove RC4, 3DES, MD5, and SHA-1 from whatever else you selected.
Your users don’t have to memorize every single cipher name or know exactly which suite corresponds to which property. They just say “I want FIPS” and “I want PQ” and “no old broken stuff,” and let the code handle the details.
Advantages of the Checkbox Approach
-
Plain-English intent Each checkbox describes a property. You understand “post-quantum KEM” or “256 bits of security” at a glance, without needing to parse a colon-separated list of hyphenated tokens.
-
Mix categories freely Want to run a FIPS-validated configuration and experiment with ML-KEM hybrids while also removing legacy RSA? Just check FIPS 140-3 approved, Post-quantum KEM, and Disable legacy RSA key exchange. The allowed set becomes (FIPS algorithms) ∪ (PQ KEMs) minus (
TLS_RSA_*
suites) and minus any “deprecated” bits if that box is checked. -
Future-proofing When a new algorithm gets approved (for example, a new PQ KEM or a new curve), you tag it internally as “post-quantum,” “FIPS,” or “GOST.” The UI never changes: admins just see that those checkboxes now pull in the new algorithms automatically.
-
Reduced human error Nobody accidentally leaves RC4 or 3DES in the mix. Checking Disable deprecated/broken ciphers makes sure none of those can slip in, even if you also checked another “add” category.
-
Compliance made easy Instead of sending auditors a page of tiny cipher names, you can show them exactly which boxes are checked. “FIPS 140-3 approved + PQ KEM + Disable legacy RSA + No TLS 1.0/1.1” is far more transparent than a colon-separated string.
Pitfalls You Have to Watch Out For
Of course, “checkboxes” doesn’t magically solve every corner case. You still need to build a correct mapping from checkboxes to real cipher lists. Here are a few snag points:
-
Defining “≥ 256 bits” properly A lot of people assume “X25519 + AES-256” is 256 bits, but it isn’t. X25519 has about 128 bits of elliptic-curve security. If you truly want 256 bits, you need RSA 3072 + AES-256 (≈112-bit RSA 3072 vs. AES-256) or a P-521 curve (≈256-bit ECC) with AES-256. Be crystal clear about how you measure “security bits” in each category.
- TLS 1.3 vs. TLS 1.2 differences
- In TLS 1.3, cipher suites are just AEAD+hash (e.g.
TLS_AES_256_GCM_SHA384
); key exchange is negotiated separately viasupported_groups
(curves or KEMs). - In TLS 1.2, a “cipher suite” bundles together key exchange + bulk cipher + MAC. Filtering for “forward secrecy” means including only
ECDHE_*
orDHE_*
suites; if you also check “Include GOST,” you add GOST KEX suites, and so on.
Your code has to handle these two worlds separately. If Post-quantum KEM is checked, you only enable TLS 1.3 (since TLS 1.2 can’t do standard PQ KEMs). If Disable legacy RSA key exchange is checked, you remove
TLS_RSA_*
suites under TLS 1.2 but can still add other categories side by side. - In TLS 1.3, cipher suites are just AEAD+hash (e.g.
-
Contradictory or empty selections Since checkboxes combine additions and removals, you’ll still need rules for “disable deprecated” or “disable TLS 1.0/1.1.” If someone checks Disable TLS 1.0/1.1 but leaves every other box unchecked, you should default to “just use the library’s normal TLS 1.2+1.3 defaults.” If they check something that yields no available ciphers (for instance, “≥ 256 bits” on a library that only supports 128-bit curves), you need to detect that at startup and error out:
“No cipher suites match your selection. Please revise your boxes.”
- Keeping your taxonomy up to date Internally, you need a maintained list of “tags” for each algorithm: “FIPS,” “security_256bit,” “forward_secrecy,” “post_quantum,” “deprecated,” “gost,” etc. That list must be refreshed whenever standards bodies approve new stuff (NIST FIPS 140-3 updates, PQ milestones, or new GOST releases). If you tag something incorrectly, admins might inadvertently violate compliance.
How I’d Build This in Practice
Here’s a rough sketch of the steps I’d take if I were writing a TLS library or an admin tool that used a checkbox approach instead of raw cipher strings.
-
Maintain a Tag Table Create a simple JSON or YAML file that lists every cipher suite, AEAD, and key‐exchange group you support. For example:
tls13_cipher_suites: - name: TLS_AES_256_GCM_SHA384 tags: ["fips", "forward_secrecy", "security_256bit"] - name: TLS_CHACHA20_POLY1305_SHA256 tags: ["experimental", "forward_secrecy", "security_256bit"] - name: TLS_AES_128_GCM_SHA256 tags: ["fips", "forward_secrecy", "security_128bit"] tls12_cipher_suites: - name: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 tags: ["fips", "forward_secrecy", "security_256bit"] - name: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 tags: ["experimental", "forward_secrecy", "security_256bit"] - name: TLS_RSA_WITH_AES_256_GCM_SHA384 tags: ["legacy_no_forward_secrecy", "security_256bit"] - name: TLS_RSA_WITH_AES_128_CBC_SHA tags: ["deprecated", "security_128bit"] # …and so on…
And another list for key exchanges / curves / KEMs:
kex_groups:
- name: X25519
tags: ["forward_secrecy", "security_128bit", "fips_as_of:2023-02-01"]
- name: p256
tags: ["forward_secrecy", "security_128bit", "fips"]
- name: mlkem768x25519
tags: ["post_quantum", "experimental", "not_fips"]
- name: gost2001
tags: ["gost", "legacy_no_forward_secrecy"]
-
Present the Checkboxes In your config UI or CLI, show something like:
[ ] FIPS 140-3 approved [ ] Minimum 256 bits of security [ ] Forward secrecy [ ] Post-quantum KEM (TLS 1.3 only – experimental) [ ] Disable legacy RSA key exchange (TLS 1.2 only) [ ] Disable TLS 1.0 and TLS 1.1 [ ] Disable deprecated/broken ciphers (RC4, 3DES, MD5, SHA-1) [ ] Include GOST/KCMVP/GMTLS algorithms
Each box is an “inclusion” category except for the ones prefixed with “Disable,” which are explicit exclusions. When boxes are checked, your final allowed set is:
(Union of all suites/groups matching any checked “include” box) minus (anything matching any checked “disable” box).
If no “include” boxes are checked at all, default to your library’s built-in “sane defaults” (which cover most use cases).
-
Filter at Startup When the server or client starts, read the user’s selections. Then:
-
Build an “include_set” by union-filtering your tables:
- If FIPS 140-3 approved is checked, add everything tagged “fips.”
- If ≥ 256 bits is checked, add everything tagged “security_256bit.”
- If Post-quantum KEM is checked, add everything tagged “post_quantum.”
- If Forward secrecy is checked, add everything tagged “forward_secrecy.”
- If Include GOST is checked, add everything tagged “gost.”
(If no include-boxes are checked at all, skip this step and just pull in “defaults.”)
-
Build an “exclude_set”:
- If Disable legacy RSA key exchange is checked, add everything tagged “legacy_no_forward_secrecy.”
- If Disable deprecated/broken ciphers is checked, add everything tagged “deprecated.”
- If Disable TLS 1.0 and 1.1 is checked, exclude any suite or KEX group that only works in TLS 1.0/1.1.
-
Compute
allowed = include_set – exclude_set
.-
If that’s empty (and at least one include-box was checked), error out:
“No cipher suites match your selection. Please revise your boxes.”
-
If no include-boxes were checked, use your TLS library’s defaults (after applying any exclude-boxes).
-
-
Separate by TLS version:
- For TLS 1.3, parse
allowed
to pick only AEAD+hash tuples (e.g.TLS_AES_256_GCM_SHA384
,TLS_CHACHA20_POLY1305_SHA256
, etc.). Also buildallowed_groups
from PQ or ECC KEMs (X25519, ML-KEM, etc.). - For TLS 1.2, parse
allowed
to pick only the full suites (ECDHE_RSA_WITH_AES_256_GCM_SHA384
,RSA_WITH_AES_256_CBC_SHA
, etc.).
- For TLS 1.3, parse
If either TLS 1.3 or TLS 1.2 ends up with no available ciphers (and the admin expects that version to be enabled), you should warn or error accordingly.
-
-
Apply to the Library For OpenSSL, build strings:
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-RSA-AES256-GCM-SHA384:..."); SSL_CTX_set_ciphersuites(ctx, "TLS_AES_256_GCM_SHA384:..."); // Then set SSL_CTX_set_groups_list(ctx, "X25519:secp521r1:mlkem768");
The key is that everything in
allowed
gets wired in, and everything inexcluded
is dropped. -
Offer Presets for Novices On top of checkboxes, provide “Modern,” “Intermediate,” and “Legacy” presets, just like Mozilla’s SSL Config Generator. Each preset simply checks a known combination:
- Modern: TLS 1.3 only, Forward secrecy, Disable TLS 1.0/1.1, Disable deprecated/broken ciphers
- Intermediate: TLS 1.3 + TLS 1.2, Forward secrecy, Disable deprecated/broken ciphers
- Strict-FIPS: FIPS 140-3 approved, ≥ 256 bit security, Disable TLS 1.0/1.1, Disable deprecated/broken ciphers
- PQ-Experiment: Post-quantum KEM, Forward secrecy, Disable deprecated/broken ciphers (might produce TLS 1.3 with ML-KEM hybrids only)
But when someone clicks “Advanced,” they see all the individual checkboxes and can tweak further, adding Post-quantum on top of FIPS, or enabling Disable legacy RSA alongside everything else.
A Few Hard Reality Checks
-
Checkbox ≠ Magic. You still have to maintain that tagging table carefully. If a new PQ KEM or GOST variant appears, you must tag it correctly. If you mis-tag something “post_quantum” as “fips,” admins will think they’re doing a FIPS-compliant setup when they’re not.
-
User Education Still Matters. Even with checkboxes, some choices are subtle. “≥ 256 bits” often confuses people, because they think “AES-256 = 256 bits” and “X25519 = 256 bits,” when in reality X25519 is about 128 bits. You need tooltips or documentation explaining:
To get true 256-bit security, you must use a 521-bit ECC curve (P-521) or at least RSA 3072 + AES-256. X25519 + AES-256 is only about 128 bits net.
-
TLS 1.3 Is Different. In TLS 1.3, cipher suites are just AEADs, so your checkboxes pull in AEADs (AES-GCM, ChaCha20-Poly1305) and separate KEM groups (X25519, PQ). Make sure your UI reflects that separation; otherwise people will wonder why “checking ≤ 128 bits” didn’t filter out some TLS 1.3 suites, etc.
-
“Disable TLS 1.0/1.1” Is Still a Bit Odd. If that box is checked, you simply set
MinVersion = TLS1_2
. But some older implementations might not let you fully remove TLS 1.1 at runtime; you have to ensure the code path really enforces that.
Why This Matters Today
Most people today don’t—and shouldn’t—touch their cipher lists. Go’s TLS library removed the ability to override TLS 1.3 ciphers because their defaults are strong enough. OpenSSL’s defaults (since 1.1.x) are quite sensible. Browsers do the right thing. Android, iOS, and Windows ship a solid TLS stack.
But a small minority of operators will do anything they can to “tune performance” or “optimize security” by hand. They go down the rabbit hole of “Is AES-GCM faster than ChaCha?” or “Which curve should I use?” Most of the time, they end up with a brittle, outdated configuration that nobody ever revisits.
A checkbox approach still lets them satisfy real compliance requirements: FIPS, government regulations, PCI, or “we need at least 256 bits of security.” It also lets them opt into new things like PQ KEMs without having to learn every single cipher name. And when an algorithm gets deprecated or broken, your code removes that tag so nobody picks it again.
In short, ditch free-form cipher strings. Let your users pick what properties they need: FIPS, security bits, forward secrecy, PQ, rather than forcing them to pick exactly which ciphers. You’ll save yourself (and future you) countless headaches down the road.
TL;DR
- Stop letting people type raw cipher-suite strings. It’s too easy to misconfigure and forget later
- Modern TLS defaults are already strong. Only tweak ciphers if you have a compliance reason
- Use a checkbox approach for high-level properties (“FIPS 140-3 approved,” “≥ 256 bits,” “forward secrecy,” “post-quantum,” etc.) and compute the union, minus any “disable” categories
- Maintain a simple “tags” table for every cipher suite/AEAD/group. Update it when standards bodies approve or deprecate algorithms
- Provide presets for “Modern,” “Intermediate,” “Strict-FIPS,” and “PQ-Experiment” so novices aren’t overwhelmed
Trust the defaults, and let checkboxes handle the rest. Your users, and future you, will thank you.