Fail fast on invalid certificates at TLS config load (#2999)

This PR adds the certificates validation at TLS load, rejects invalid
(expired/not-yet-valid) certificates:

Apply to all TLS config paths:
- Server certificates `tls-cert-file`
- Server-side client certificates `tls-client-cert-file`
- CA certificate file `tls-ca-cert-file` 
- CA certificate directory `tls-ca-cert-dir` (now eagerly loaded to be
consistent with file-based CAs)

Apply to both scenarios:
- Server startup (initial TLS load)
- Runtime reload vis `CONFIG SET`

### Implementation
- Added `isCertValid` function to check if an X509 certificate is within
its validity period (not expired, not future-dated)
- Added `areAllCaCertsValid` function to iterate through all loaded CA
certificates and validate them
- Added `loadCaCertDir` function to eagerly load all certificates from a
directory into the X509_STORE
- Modified `createSSLContext` to validate:
  - Server/client certificates immediately after loading
  - All CA certificates after loading from file/directory

### Test results

#### 1. Server startup (initial TLS load)
```
tls-cert-file ./tests/tls/server-expired.crt

41522:M 31 Dec 2025 16:13:18.851 # Server TLS certificate is invalid. Aborting TLS configuration.
41522:M 31 Dec 2025 16:13:18.851 # Failed to configure TLS. Check logs for more info.


tls-client-cert-file ./tests/tls/client-expired.crt

41557:M 31 Dec 2025 16:14:43.296 # Client TLS certificate is invalid. Aborting TLS configuration.
41557:M 31 Dec 2025 16:14:43.296 # Failed to configure TLS. Check logs for more info.


tls-ca-cert-file ./tests/tls/ca-expired.crt
tls-ca-cert-dir ./tests/tls/ca-expired

41567:M 31 Dec 2025 16:15:15.635 # One or more loaded CA certificates are invalid. Aborting TLS configuration.
41567:M 31 Dec 2025 16:15:15.635 # Failed to configure TLS. Check logs for more info.
```

#### 2. Runtime reload via CONFIG SET
```
127.0.0.1:6379> config set tls-cert-file ./tests/tls/server-expired.crt
(error) ERR CONFIG SET failed (possibly related to argument 'tls-cert-file') - Unable to update TLS configuration. Check server logs.

62975:M 02 Jan 2026 20:10:43.588 # Server TLS certificate is invalid. Aborting TLS configuration.
62975:M 02 Jan 2026 20:10:43.588 # Failed applying new configuration. Possibly related to new tls-cert-file setting. Restoring previous settings.


127.0.0.1:6379> config set tls-client-cert-file ./tests/tls/client-expired.crt
(error) ERR CONFIG SET failed (possibly related to argument 'tls-client-cert-file') - Unable to update TLS configuration. Check server logs.

62975:M 02 Jan 2026 20:10:57.972 # Client TLS certificate is invalid. Aborting TLS configuration.
62975:M 02 Jan 2026 20:10:57.972 # Failed applying new configuration. Possibly related to new tls-client-cert-file setting. Restoring previous settings.


127.0.0.1:6379> config set tls-ca-cert-file ./tests/tls/ca-expired.crt
127.0.0.1:6379> config set tls-ca-cert-dir ./tests/tls/ca-expired
(error) ERR CONFIG SET failed (possibly related to argument 'tls-ca-cert-file') - Unable to update TLS configuration. Check server logs.

62975:M 02 Jan 2026 20:10:50.175 # One or more loaded CA certificates are invalid. Aborting TLS configuration.
62975:M 02 Jan 2026 20:10:50.175 # Failed applying new configuration. Possibly related to new tls-ca-cert-file setting. Restoring previous settings.
```

---------

Signed-off-by: Yang Zhao <zymy701@gmail.com>
This commit is contained in:
Yang Zhao
2026-01-28 08:50:35 -08:00
committed by GitHub
parent 0422fc66cf
commit eb3f465e50
4 changed files with 373 additions and 11 deletions
+105 -5
View File
@@ -52,6 +52,8 @@
#include <sys/stat.h>
#include <sys/uio.h>
#include <arpa/inet.h>
#include <dirent.h>
#include <limits.h>
#define REDIS_TLS_PROTO_TLSv1 (1 << 0)
#define REDIS_TLS_PROTO_TLSv1_1 (1 << 1)
@@ -210,6 +212,88 @@ static int tlsPasswordCallback(char *buf, int size, int rwflag, void *u) {
return (int)pass_len;
}
/* Check a single X509 certificate validity */
static bool isCertValid(X509 *cert) {
if (!cert) return false;
const ASN1_TIME *not_before = X509_get0_notBefore(cert);
const ASN1_TIME *not_after = X509_get0_notAfter(cert);
if (!not_before || !not_after) return false;
if (X509_cmp_current_time(not_before) > 0 ||
X509_cmp_current_time(not_after) < 0) {
return false;
}
return true;
}
/* Load all certificates from a directory into the X509_STORE
* Returns true on success, false on failure */
static bool loadCaCertDir(SSL_CTX *ctx, const char *ca_cert_dir) {
if (!ca_cert_dir) return true;
DIR *dir;
struct dirent *entry;
char full_path[PATH_MAX];
X509_STORE *store = SSL_CTX_get_cert_store(ctx);
if (!store) {
serverLog(LL_WARNING, "Failed to get X509_STORE from SSL_CTX");
return false;
}
dir = opendir(ca_cert_dir);
if (!dir) {
serverLog(LL_WARNING, "Failed to open CA certificate directory: %s", ca_cert_dir);
return false;
}
while ((entry = readdir(dir)) != NULL) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
snprintf(full_path, sizeof(full_path), "%s/%s", ca_cert_dir, entry->d_name);
FILE *fp = fopen(full_path, "r");
if (!fp) continue;
X509 *cert = PEM_read_X509(fp, NULL, NULL, NULL);
fclose(fp);
if (cert) {
if (X509_STORE_add_cert(store, cert) != 1) {
unsigned long err = ERR_peek_last_error();
if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) {
serverLog(LL_WARNING, "Failed to add CA certificate from %s to store", full_path);
X509_free(cert);
closedir(dir);
return false;
}
ERR_clear_error();
}
X509_free(cert);
}
}
closedir(dir);
return true;
}
/* Iterate over all CA certs in the SSL_CTX and fail-fast if any are invalid */
static bool areAllCaCertsValid(SSL_CTX *ctx) {
X509_STORE *store = SSL_CTX_get_cert_store(ctx);
if (!store) return false;
STACK_OF(X509_OBJECT) *objs = X509_STORE_get0_objects(store);
if (!objs) return false;
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
X509_OBJECT *obj = sk_X509_OBJECT_value(objs, i);
int type = X509_OBJECT_get_type(obj);
if (type == X509_LU_X509) {
X509 *ca_cert = X509_OBJECT_get0_X509(obj);
if (ca_cert && !isCertValid(ca_cert)) {
return false;
}
}
}
return true;
}
/* Create a *base* SSL_CTX using the SSL configuration provided. The base context
* includes everything that's common for both client-side and server-side connections.
*/
@@ -254,17 +338,33 @@ static SSL_CTX *createSSLContext(serverTLSContextConfig *ctx_config, int protoco
goto error;
}
if (!isCertValid(SSL_CTX_get0_certificate(ctx))) {
serverLog(LL_WARNING, "%s TLS certificate is invalid. Aborting TLS configuration.", client ? "Client" : "Server");
goto error;
}
if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) <= 0) {
ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
serverLog(LL_WARNING, "Failed to load private key: %s: %s", key_file, errbuf);
goto error;
}
if ((ctx_config->ca_cert_file || ctx_config->ca_cert_dir) &&
SSL_CTX_load_verify_locations(ctx, ctx_config->ca_cert_file, ctx_config->ca_cert_dir) <= 0) {
ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
serverLog(LL_WARNING, "Failed to configure CA certificate(s) file/directory: %s", errbuf);
goto error;
if (ctx_config->ca_cert_file || ctx_config->ca_cert_dir) {
if (SSL_CTX_load_verify_locations(ctx, ctx_config->ca_cert_file, ctx_config->ca_cert_dir) <= 0) {
ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
serverLog(LL_WARNING, "Failed to configure CA certificate(s) file/directory: %s", errbuf);
goto error;
}
if (!loadCaCertDir(ctx, ctx_config->ca_cert_dir)) {
serverLog(LL_WARNING, "Failed to load CA certificates from directory: %s", ctx_config->ca_cert_dir);
goto error;
}
if (!areAllCaCertsValid(ctx)) {
serverLog(LL_WARNING, "One or more loaded CA certificates are invalid. Aborting TLS configuration.");
goto error;
}
}
if (ctx_config->ciphers && !SSL_CTX_set_cipher_list(ctx, ctx_config->ciphers)) {
+1 -1
View File
@@ -39,7 +39,7 @@ match different external server configurations. All options are listed by
Running with TLS requires the following preparations:
* Build Valkey is TLS support, e.g. using `make BUILD_TLS=yes`, or `make BUILD_TLS=module`.
* Run `./utils/gen-test-certs.sh` to generate a root CA and a server certificate.
* Run `./utils/gen-test-certs.sh` to generate a root CA, server certificates, and invalid certificates for testing.
* Install TLS support for TCL, e.g. the `tcl-tls` package on Debian/Ubuntu.
Additional tests
+106
View File
@@ -329,5 +329,111 @@ start_server {tags {"tls"}} {
}
}
}
proc test_tls_cert_rejection {cert_type cert_path expected_error} {
set tlsdir [file normalize ./tests/tls]
set server_path [file normalize ./src/valkey-server]
set server_cert $tlsdir/server.crt
set server_key $tlsdir/server.key
set client_cert $tlsdir/client.crt
set client_key $tlsdir/client.key
set ca_cert_file $tlsdir/ca.crt
set ca_cert_dir ""
switch -- $cert_type {
server { set server_cert $cert_path }
client { set client_cert $cert_path }
"ca-file" { set ca_cert_file $cert_path }
"ca-dir" { set ca_cert_dir $cert_path; set ca_cert_file "" }
}
set cmd [list $server_path --port 0 --tls-port 16379 \
--tls-cert-file $server_cert --tls-key-file $server_key \
--tls-client-cert-file $client_cert --tls-client-key-file $client_key]
if {$ca_cert_file ne ""} { lappend cmd --tls-ca-cert-file $ca_cert_file }
if {$ca_cert_dir ne ""} { lappend cmd --tls-ca-cert-dir $ca_cert_dir }
if {$::tls_module} {
lappend cmd --loadmodule [file normalize ./src/valkey-tls.so]
}
catch {exec {*}$cmd 2>@1} err
assert_match $expected_error $err
}
test {TLS: Fail-fast on invalid certificates at startup} {
set tlsdir [file normalize ./tests/tls]
# Expired server certificate
test_tls_cert_rejection server $tlsdir/server-expired.crt {*Server TLS certificate is invalid*}
# Not-yet-valid server certificate
test_tls_cert_rejection server $tlsdir/server-notyet.crt {*Server TLS certificate is invalid*}
# Expired client certificate
test_tls_cert_rejection client $tlsdir/client-expired.crt {*Client TLS certificate is invalid*}
# Not-yet-valid client certificate
test_tls_cert_rejection client $tlsdir/client-notyet.crt {*Client TLS certificate is invalid*}
# Expired CA certificate file
test_tls_cert_rejection ca-file $tlsdir/ca-expired.crt {*One or more loaded CA certificates are invalid*}
# Not-yet-valid CA certificate file
test_tls_cert_rejection ca-file $tlsdir/ca-notyet.crt {*One or more loaded CA certificates are invalid*}
# Expired CA certificate directory
test_tls_cert_rejection ca-dir $tlsdir/ca-expired {*One or more loaded CA certificates are invalid*}
# Not-yet-valid CA certificate directory
test_tls_cert_rejection ca-dir $tlsdir/ca-notyet {*One or more loaded CA certificates are invalid*}
}
proc test_tls_cert_rejection_runtime {r cert_type cert_path} {
switch -- $cert_type {
server {
catch {$r CONFIG SET tls-cert-file $cert_path} err
}
client {
catch {$r CONFIG SET tls-client-cert-file $cert_path} err
}
"ca-file" {
catch {$r CONFIG SET tls-ca-cert-file $cert_path} err
}
"ca-dir" {
catch {$r CONFIG SET tls-ca-cert-dir $cert_path} err
}
}
assert_match {*Unable to update TLS*} $err
}
test {TLS: Fail-fast on invalid certificates at runtime} {
set tlsdir [file normalize ./tests/tls]
# Expired server certificate
test_tls_cert_rejection_runtime r server $tlsdir/server-expired.crt
# Not-yet-valid server certificate
test_tls_cert_rejection_runtime r server $tlsdir/server-notyet.crt
# Expired client certificate
test_tls_cert_rejection_runtime r client $tlsdir/client-expired.crt
# Not-yet-valid client certificate
test_tls_cert_rejection_runtime r client $tlsdir/client-notyet.crt
# Expired CA certificate file
test_tls_cert_rejection_runtime r ca-file $tlsdir/ca-expired.crt
# Not-yet-valid CA certificate file
test_tls_cert_rejection_runtime r ca-file $tlsdir/ca-notyet.crt
# Expired CA certificate directory
test_tls_cert_rejection_runtime r ca-dir $tlsdir/ca-expired
# Not-yet-valid CA certificate directory
test_tls_cert_rejection_runtime r ca-dir $tlsdir/ca-notyet
}
}
}
+161 -5
View File
@@ -2,11 +2,16 @@
# Generate some test certificates which are used by the regression test suite:
#
# tests/tls/ca.{crt,key} Self signed CA certificate.
# tests/tls/valkey.{crt,key} A certificate with no key usage/policy restrictions.
# tests/tls/client.{crt,key} A certificate restricted for SSL client usage.
# tests/tls/server.{crt,key} A certificate restricted for SSL server usage.
# tests/tls/valkey.dh DH Params file.
# tests/tls/ca.{crt,key} Self signed CA certificate.
# tests/tls/ca-{expired,notyet}.crt Self signed invalid CA certificates.
# tests/tls/ca-expired/ Directory containing expired CA certificate.
# tests/tls/ca-notyet/ Directory containing not-yet-valid CA certificate.
# tests/tls/valkey.{crt,key} A certificate with no key usage/policy restrictions.
# tests/tls/client.{crt,key} A certificate restricted for SSL client usage.
# tests/tls/client-{expired,notyet}.crt Invalid certificates restricted for SSL client usage.
# tests/tls/server.{crt,key} A certificate restricted for SSL server usage.
# tests/tls/server-{expired,notyet}.crt Invalid certificates restricted for SSL server usage.
# tests/tls/valkey.dh DH Params file.
generate_cert() {
local name=$1
@@ -56,3 +61,154 @@ generate_cert client "Client-only" "-extfile tests/tls/openssl.cnf -extensions c
generate_cert valkey "Generic-cert"
[ -f tests/tls/valkey.dh ] || openssl dhparam -out tests/tls/valkey.dh 2048
echo "Generating invalid TLS test certificates for fail-fast testing..."
CA_CONFIG="tests/tls/ca_temp.cnf"
cat > "$CA_CONFIG" <<EOF
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = tests/tls
database = \$dir/index.txt
new_certs_dir = \$dir
serial = \$dir/serial
default_md = sha256
policy = policy_anything
default_days = 1
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ server_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = server
[ client_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = client
EOF
touch tests/tls/index.txt
echo "01" > tests/tls/serial
# Generate expired server cert (valid Jan 1-2, 2020)
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Server-expired" \
-key tests/tls/server.key \
-out tests/tls/server-expired.csr
openssl ca -batch -config "$CA_CONFIG" \
-in tests/tls/server-expired.csr \
-cert tests/tls/ca.crt \
-keyfile tests/tls/ca.key \
-startdate 20200101000000Z \
-enddate 20200102000000Z \
-extensions server_cert \
-out tests/tls/server-expired.crt
# Generate expired client cert (valid Jan 1-2, 2020)
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Client-expired" \
-key tests/tls/client.key \
-out tests/tls/client-expired.csr
openssl ca -batch -config "$CA_CONFIG" \
-in tests/tls/client-expired.csr \
-cert tests/tls/ca.crt \
-keyfile tests/tls/ca.key \
-startdate 20200101000000Z \
-enddate 20200102000000Z \
-extensions client_cert \
-out tests/tls/client-expired.crt
# Generate not-yet-valid server cert (valid Jan 1-31, 2099)
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Server-notyet" \
-key tests/tls/server.key \
-out tests/tls/server-notyet.csr
openssl ca -batch -config "$CA_CONFIG" \
-in tests/tls/server-notyet.csr \
-cert tests/tls/ca.crt \
-keyfile tests/tls/ca.key \
-startdate 20990101000000Z \
-enddate 20990201000000Z \
-extensions server_cert \
-out tests/tls/server-notyet.crt
# Generate not-yet-valid client cert (valid Jan 1-31, 2099)
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Client-notyet" \
-key tests/tls/client.key \
-out tests/tls/client-notyet.csr
openssl ca -batch -config "$CA_CONFIG" \
-in tests/tls/client-notyet.csr \
-cert tests/tls/ca.crt \
-keyfile tests/tls/ca.key \
-startdate 20990101000000Z \
-enddate 20990201000000Z \
-extensions client_cert \
-out tests/tls/client-notyet.crt
# Generate expired CA certificate (valid Jan 1-2, 2020) using the CA to sign itself
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Certificate Authority Expired" \
-key tests/tls/ca.key \
-out tests/tls/ca-expired.csr
openssl ca -batch -config "$CA_CONFIG" \
-selfsign \
-in tests/tls/ca-expired.csr \
-keyfile tests/tls/ca.key \
-startdate 20200101000000Z \
-enddate 20200102000000Z \
-out tests/tls/ca-expired.crt
# Generate not-yet-valid CA certificate (valid Jan 1-31, 2099)
openssl req -new -sha256 \
-subj "/O=Valkey Test/CN=Certificate Authority Not Yet Valid" \
-key tests/tls/ca.key \
-out tests/tls/ca-notyet.csr
openssl ca -batch -config "$CA_CONFIG" \
-selfsign \
-in tests/tls/ca-notyet.csr \
-keyfile tests/tls/ca.key \
-startdate 20990101000000Z \
-enddate 20990201000000Z \
-out tests/tls/ca-notyet.crt
# Create CA certificate directories for testing tls-ca-cert-dir with invalid certs
mkdir -p tests/tls/ca-expired
mkdir -p tests/tls/ca-notyet
cp tests/tls/ca-expired.crt tests/tls/ca-expired/
cp tests/tls/ca-notyet.crt tests/tls/ca-notyet/
echo "Created CA certificate test directories:"
echo " tests/tls/ca-expired/ (contains expired CA cert)"
echo " tests/tls/ca-notyet/ (contains not-yet-valid CA cert)"
# Clean up temporary files
rm -f tests/tls/*-expired.csr tests/tls/*-notyet.csr tests/tls/ca-expired.csr tests/tls/ca-notyet.csr
rm -f "$CA_CONFIG" tests/tls/index.txt tests/tls/index.txt.attr tests/tls/index.txt.attr.old tests/tls/index.txt.old tests/tls/serial tests/tls/serial.old
rm -f tests/tls/0[1-9].pem tests/tls/[0-9][0-9].pem
echo ""
echo "Verification of generated invalid certificates:"
for crt in tests/tls/ca-expired.crt tests/tls/ca-notyet.crt tests/tls/*-expired.crt tests/tls/*-notyet.crt; do
if [ -f "$crt" ]; then
echo ""
echo "Certificate: $crt"
openssl x509 -in "$crt" -noout -subject -dates
fi
done