Add tls-groups to control TLS named groups
Project: redis/redis | Language: C | Concepts: api-design, configuration-management, security, testing-strategy | Difficulty: mid
Link: https://github.com/redis/redis/pull/15556
The problem
Redis allowed configuration of TLS protocols, ciphers, and ciphersuites, but did not expose a way to control the set of TLS named groups (also known as elliptic curves or key exchange groups) used in handshakes. Some deployments require strict control over which groups are allowed for compliance or interoperability. Without this, administrators could not enforce a group policy, and mismatched group support between client and server could cause handshake failures with no way to adjust the configuration.
Why it's non-trivial
A naive fix might try to set the group preferences without checking OpenSSL support, or silently ignore unsupported options. However, OpenSSL provides different APIs for group selection in different versions (SSL_CTX_set1_groups_list in newer versions, SSL_CTX_set1_curves_list in older), and some builds may not support group selection at all. Failing to handle these cases could result in silent misconfiguration, runtime errors, or security gaps. Additionally, the feature must be exposed consistently in both the server and client tools, and tested for both correct and incorrect configurations.
The approach
Expose the tls-groups configuration and wire it into server context creation:
+ createStringConfig("tls-groups", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.groups, NULL, NULL, applyTlsCfg),
Adds a new configuration option, tls-groups, to the Redis server, making it modifiable at runtime and tied to the TLS context.
Apply the group preference when creating or reconfiguring the OpenSSL context:
+ if (ctx_config->groups) {
+#if CONN_TLS_SUPPORTS_GROUPS
+ if (!redisTlsCtxSetGroupsList(ctx, ctx_config->groups)) {
+ serverLog(LL_WARNING, "Failed to configure TLS groups: %s", ctx_config->groups);
+ goto error;
+ }
+#else
+ serverLog(LL_WARNING, "Failed to configure TLS groups: not supported by this build");
+ goto error;
+#endif
+ }
When a group list is configured, the code tries to set it using the appropriate OpenSSL API. If the API is not available or the group list is invalid, context creation fails with a clear error.
Detect OpenSSL API support at compile time and handle unsupported builds:
+#if defined(TLS_NO_GROUPS)
+#define CONN_TLS_SUPPORTS_GROUPS 0
+#elif defined(SSL_CTX_set1_groups_list)
+#define CONN_TLS_SUPPORTS_GROUPS 1
+#define redisTlsCtxSetGroupsList(ctx, list) SSL_CTX_set1_groups_list((ctx), (list))
+#elif defined(SSL_CTX_set1_curves_list)
+#define CONN_TLS_SUPPORTS_GROUPS 1
+#define redisTlsCtxSetGroupsList(ctx, list) SSL_CTX_set1_curves_list((ctx), (list))
+#else
+#error "tls-groups requires OpenSSL with SSL_CTX_set1_groups_list or SSL_CTX_set1_curves_list. Define TLS_NO_GROUPS to build without TLS groups."
+#endif
Uses preprocessor checks to ensure the feature is only compiled if supported by the linked OpenSSL version. If neither API is available, compilation fails unless explicitly opted out with TLS_NO_GROUPS.
Expose the feature in CLI tools only when supported:
+#if CLI_TLS_SUPPORTS_GROUPS
+ } else if (!strcmp(argv[i],"--tls-groups")) {
+ if (lastarg) goto invalid;
+ config.sslconfig.groups = strdup(argv[++i]);
+#endif
Both redis-cli and redis-benchmark gain a --tls-groups option, but only if the build supports it. The help text is also conditionally included.
Extend the test suite to cover configuration, handshake success, and failure:
+ test {TLS: Verify tls-groups validates group names} {
+ assert_equal {OK} [r CONFIG SET tls-groups "prime256v1"]
+
+ catch {r CONFIG SET tls-groups "invalid-group"} e
+ assert_match {*Unable to update TLS configuration*} $e
+
+ r CONFIG SET tls-groups ""
+ }
+
+ test {TLS: Verify tls-groups with a common group} {
+ ...
+ assert_equal {PONG} [tls_redis_cli [srv 0 host] [srv 0 port] prime256v1]
+ ...
+ }
+
+ test {TLS: Verify tls-groups with disjoint groups} {
+ ...
+ assert_equal 1 [catch {tls_redis_cli [srv 0 host] [srv 0 port] secp384r1} e]
+ assert_match {*sslv3 alert handshake failure*} $e
+ ...
+ }
Tests that valid and invalid group names are handled correctly, and that handshakes succeed or fail depending on whether the client and server have overlapping groups.
Document the new option, version requirements, and opt-out path:
+The `tls-groups` option controls the OpenSSL named groups used
+during TLS handshakes. ...
+This option requires OpenSSL 1.0.2 or newer, which provides the
+`SSL_CTX_set1_curves_list()` API used by Redis. ...
+To build Redis without this option, define `TLS_NO_GROUPS`, for
+example:
+
+ make BUILD_TLS=yes CFLAGS=-DTLS_NO_GROUPS
Documentation in TLS.md and redis.conf explains the purpose, syntax, version requirements, and opt-out mechanism for the new feature.
From the review thread
hristostaykov-del: For better code coverage I would recommend adding at least the following 2 tests:
- Server and client have one common configured group - handshake succeeds
- Server and client have disjoint groups - handshake fails
hristostaykov-del: We should document the OpenSSL version dependency explicitly, similarly to
tls-expected-peer-name, whose documentation notes that the required API is available starting with OpenSSL 1.0.2.For handling unsupported OpenSSL versions, I think
tls-curve-preferencesshould follow the same approach established fortls-expected-peer-name: when building against an older OpenSSL version that does not provide the required API, compilation should fail with a clear error unless the user explicitly defines a build-time opt-out macro, such asTLS_NO_CURVE_PREFERENCESfor example.
AliasJeff: Yes, the way I see it, since we just passing the string to SSL_CTX_set1_groups_list() without any filtering on our side, it should support whatever groups the openssl recognizes, including PQC like X25519MLKEM768 (available in OpenSSL 3.5+)
Takeaways
-
When exposing configuration for security primitives, always check and document the underlying library's API and version support.
Failing to do so can lead to silent misconfiguration or runtime errors. -
Fail fast and explicitly at build time if a requested security feature is unsupported, unless the user opts out.
This avoids silent security gaps and makes deployment requirements clear. -
Test both the positive and negative handshake scenarios when adding TLS configuration options.
It's important to verify that both correct and incorrect configurations behave as expected, especially for interoperability. -
Expose new configuration options in client tools only when the underlying build supports them.
This keeps the user interface consistent and avoids runtime surprises.