Repository: redis/redis

Problem statement:
[NEW] Replica TLS certificate
**The problem/use-case that the feature addresses**

Currently replication is not working with right Public Key Infrastructure (in my case with Subordinate CA) because the config file does not have any option to specify the **client** certificate and key.

**Description of the feature**

An option to specify the client certificate and the client key (`tls-client-cert-file` and `tls-client-key-file`).

**Additional information**

Without it, the sentinel changes the Redis instance between master and replica and fails to connect without the client certificate/key (that acts as a client certificate, not server certificate).

Note: This issue is strictly related to the DNS inssue: https://github.com/redis/redis/issues/7928.
For two way TLS authentication, valid domain name is needed. (SAN field in X509.v3)


Hints:
Thank you @mvasi90 for raising this. This definitely makes sense to me as an optional configuration, which should affect all scenarios where Redis establishes a TLS connection as a client:

* Replica connecting to master
* Sentinel connecting to Redis
* Cluster bus client side
* Cluster node migrating keys to remote node

Adding SAN validation also makes sense (again, optionally) but will probably be more difficult to do, as Redis tends to use IPs rather than hostnames in all sorts of places.

Patch:
diff --git a/redis.conf b/redis.conf
index dcb5f624175..9d1ef3ff948 100644
--- a/redis.conf
+++ b/redis.conf
@@ -151,6 +151,18 @@ tcp-keepalive 300
 # tls-cert-file redis.crt 
 # tls-key-file redis.key
 
+# Normally Redis uses the same certificate for both server functions (accepting
+# connections) and client functions (replicating from a master, establishing
+# cluster bus connections, etc.).
+#
+# Sometimes certificates are issued with attributes that designate them as
+# client-only or server-only certificates. In that case it may be desired to use
+# different certificates for incoming (server) and outgoing (client)
+# connections. To do that, use the following directives:
+#
+# tls-client-cert-file client.crt
+# tls-client-key-file client.key
+
 # Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange:
 #
 # tls-dh-params-file redis.dh
diff --git a/src/config.c b/src/config.c
index 07ed91e5fb5..b1d4293d25a 100644
--- a/src/config.c
+++ b/src/config.c
@@ -2420,6 +2420,8 @@ standardConfig configs[] = {
     createBoolConfig("tls-session-caching", NULL, MODIFIABLE_CONFIG, server.tls_ctx_config.session_caching, 1, NULL, updateTlsCfgBool),
     createStringConfig("tls-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.cert_file, NULL, NULL, updateTlsCfg),
     createStringConfig("tls-key-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.key_file, NULL, NULL, updateTlsCfg),
+    createStringConfig("tls-client-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.client_cert_file, NULL, NULL, updateTlsCfg),
+    createStringConfig("tls-client-key-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.client_key_file, NULL, NULL, updateTlsCfg),
     createStringConfig("tls-dh-params-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.dh_params_file, NULL, NULL, updateTlsCfg),
     createStringConfig("tls-ca-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_file, NULL, NULL, updateTlsCfg),
     createStringConfig("tls-ca-cert-dir", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_dir, NULL, NULL, updateTlsCfg),
diff --git a/src/sentinel.c b/src/sentinel.c
index 87fc20e9fbe..591d1c44e40 100644
--- a/src/sentinel.c
+++ b/src/sentinel.c
@@ -46,6 +46,7 @@ extern char **environ;
 
 #ifdef USE_OPENSSL
 extern SSL_CTX *redis_tls_ctx;
+extern SSL_CTX *redis_tls_client_ctx;
 #endif
 
 #define REDIS_SENTINEL_PORT 26379
@@ -2074,7 +2075,7 @@ static int instanceLinkNegotiateTLS(redisAsyncContext *context) {
     (void) context;
 #else
     if (!redis_tls_ctx) return C_ERR;
-    SSL *ssl = SSL_new(redis_tls_ctx);
+    SSL *ssl = SSL_new(redis_tls_client_ctx ? redis_tls_client_ctx : redis_tls_ctx);
     if (!ssl) return C_ERR;
 
     if (redisInitiateSSL(&context->c, ssl) == REDIS_ERR) return C_ERR;
diff --git a/src/server.h b/src/server.h
index 53055897aec..cddf790827d 100644
--- a/src/server.h
+++ b/src/server.h
@@ -1038,8 +1038,10 @@ struct malloc_stats {
  *----------------------------------------------------------------------------*/
 
 typedef struct redisTLSContextConfig {
-    char *cert_file;
-    char *key_file;
+    char *cert_file;                /* Server side and optionally client side cert file name */
+    char *key_file;                 /* Private key filename for cert_file */
+    char *client_cert_file;         /* Certificate to use as a client; if none, use cert_file */
+    char *client_key_file;          /* Private key filename for client_cert_file */
     char *dh_params_file;
     char *ca_cert_file;
     char *ca_cert_dir;
diff --git a/src/tls.c b/src/tls.c
index 25580a4f1e6..e04526e220a 100644
--- a/src/tls.c
+++ b/src/tls.c
@@ -53,7 +53,8 @@
 
 extern ConnectionType CT_Socket;
 
-SSL_CTX *redis_tls_ctx;
+SSL_CTX *redis_tls_ctx = NULL;
+SSL_CTX *redis_tls_client_ctx = NULL;
 
 static int parseProtocolsConfig(const char *str) {
     int i, count = 0;
@@ -151,50 +152,22 @@ void tlsInit(void) {
     pending_list = listCreate();
 }
 
-/* Attempt to configure/reconfigure TLS. This operation is atomic and will
- * leave the SSL_CTX unchanged if fails.
+/* 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.
  */
-int tlsConfigure(redisTLSContextConfig *ctx_config) {
+static SSL_CTX *createSSLContext(redisTLSContextConfig *ctx_config, int protocols,
+                                 const char *cert_file, const char *key_file) {
     char errbuf[256];
     SSL_CTX *ctx = NULL;
 
-    if (!ctx_config->cert_file) {
-        serverLog(LL_WARNING, "No tls-cert-file configured!");
-        goto error;
-    }
-
-    if (!ctx_config->key_file) {
-        serverLog(LL_WARNING, "No tls-key-file configured!");
-        goto error;
-    }
-
-    if (((server.tls_auth_clients != TLS_CLIENT_AUTH_NO) || server.tls_cluster || server.tls_replication) &&
-            !ctx_config->ca_cert_file && !ctx_config->ca_cert_dir) {
-        serverLog(LL_WARNING, "Either tls-ca-cert-file or tls-ca-cert-dir must be specified when tls-cluster, tls-replication or tls-auth-clients are enabled!");
-        goto error;
-    }
-
     ctx = SSL_CTX_new(SSLv23_method());
 
     SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3);
-    SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE);
 
 #ifdef SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS
     SSL_CTX_set_options(ctx, SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS);
 #endif
 
-    if (ctx_config->session_caching) {
-        SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER);
-        SSL_CTX_sess_set_cache_size(ctx, ctx_config->session_cache_size);
-        SSL_CTX_set_timeout(ctx, ctx_config->session_cache_timeout);
-        SSL_CTX_set_session_id_context(ctx, (void *) "redis", 5);
-    } else {
-        SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
-    }
-
-    int protocols = parseProtocolsConfig(ctx_config->protocols);
-    if (protocols == -1) goto error;
-
     if (!(protocols & REDIS_TLS_PROTO_TLSv1))
         SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1);
     if (!(protocols & REDIS_TLS_PROTO_TLSv1_1))
@@ -212,31 +185,21 @@ int tlsConfigure(redisTLSContextConfig *ctx_config) {
     SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
 #endif
 
-#ifdef SSL_OP_NO_CLIENT_RENEGOTIATION
-    SSL_CTX_set_options(ctx, SSL_OP_NO_CLIENT_RENEGOTIATION);
-#endif
-
-    if (ctx_config->prefer_server_ciphers)
-        SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
-
     SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
     SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
-#if defined(SSL_CTX_set_ecdh_auto)
-    SSL_CTX_set_ecdh_auto(ctx, 1);
-#endif
 
-    if (SSL_CTX_use_certificate_chain_file(ctx, ctx_config->cert_file) <= 0) {
+    if (SSL_CTX_use_certificate_chain_file(ctx, cert_file) <= 0) {
         ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
-        serverLog(LL_WARNING, "Failed to load certificate: %s: %s", ctx_config->cert_file, errbuf);
+        serverLog(LL_WARNING, "Failed to load certificate: %s: %s", cert_file, errbuf);
         goto error;
     }
-        
-    if (SSL_CTX_use_PrivateKey_file(ctx, ctx_config->key_file, SSL_FILETYPE_PEM) <= 0) {
+
+    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", ctx_config->key_file, 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));
@@ -244,6 +207,77 @@ int tlsConfigure(redisTLSContextConfig *ctx_config) {
         goto error;
     }
 
+    if (ctx_config->ciphers && !SSL_CTX_set_cipher_list(ctx, ctx_config->ciphers)) {
+        serverLog(LL_WARNING, "Failed to configure ciphers: %s", ctx_config->ciphers);
+        goto error;
+    }
+
+#ifdef TLS1_3_VERSION
+    if (ctx_config->ciphersuites && !SSL_CTX_set_ciphersuites(ctx, ctx_config->ciphersuites)) {
+        serverLog(LL_WARNING, "Failed to configure ciphersuites: %s", ctx_config->ciphersuites);
+        goto error;
+    }
+#endif
+
+    return ctx;
+
+error:
+    if (ctx) SSL_CTX_free(ctx);
+    return NULL;
+}
+
+/* Attempt to configure/reconfigure TLS. This operation is atomic and will
+ * leave the SSL_CTX unchanged if fails.
+ */
+int tlsConfigure(redisTLSContextConfig *ctx_config) {
+    char errbuf[256];
+    SSL_CTX *ctx = NULL;
+    SSL_CTX *client_ctx = NULL;
+
+    if (!ctx_config->cert_file) {
+        serverLog(LL_WARNING, "No tls-cert-file configured!");
+        goto error;
+    }
+
+    if (!ctx_config->key_file) {
+        serverLog(LL_WARNING, "No tls-key-file configured!");
+        goto error;
+    }
+
+    if (((server.tls_auth_clients != TLS_CLIENT_AUTH_NO) || server.tls_cluster || server.tls_replication) &&
+            !ctx_config->ca_cert_file && !ctx_config->ca_cert_dir) {
+        serverLog(LL_WARNING, "Either tls-ca-cert-file or tls-ca-cert-dir must be specified when tls-cluster, tls-replication or tls-auth-clients are enabled!");
+        goto error;
+    }
+
+    int protocols = parseProtocolsConfig(ctx_config->protocols);
+    if (protocols == -1) goto error;
+
+    /* Create server side/generla context */
+    ctx = createSSLContext(ctx_config, protocols, ctx_config->cert_file, ctx_config->key_file);
+    if (!ctx) goto error;
+
+    if (ctx_config->session_caching) {
+        SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER);
+        SSL_CTX_sess_set_cache_size(ctx, ctx_config->session_cache_size);
+        SSL_CTX_set_timeout(ctx, ctx_config->session_cache_timeout);
+        SSL_CTX_set_session_id_context(ctx, (void *) "redis", 5);
+    } else {
+        SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
+    }
+
+#ifdef SSL_OP_NO_CLIENT_RENEGOTIATION
+    SSL_CTX_set_options(ctx, SSL_OP_NO_CLIENT_RENEGOTIATION);
+#endif
+
+    if (ctx_config->prefer_server_ciphers)
+        SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
+
+#if defined(SSL_CTX_set_ecdh_auto)
+    SSL_CTX_set_ecdh_auto(ctx, 1);
+#endif
+    SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE);
+
     if (ctx_config->dh_params_file) {
         FILE *dhfile = fopen(ctx_config->dh_params_file, "r");
         DH *dh = NULL;
@@ -269,25 +303,22 @@ int tlsConfigure(redisTLSContextConfig *ctx_config) {
         DH_free(dh);
     }
 
-    if (ctx_config->ciphers && !SSL_CTX_set_cipher_list(ctx, ctx_config->ciphers)) {
-        serverLog(LL_WARNING, "Failed to configure ciphers: %s", ctx_config->ciphers);
-        goto error;
+    /* If a client-side certificate is configured, create an explicit client context */
+    if (ctx_config->client_cert_file && ctx_config->client_key_file) {
+        client_ctx = createSSLContext(ctx_config, protocols, ctx_config->client_cert_file, ctx_config->client_key_file);
+        if (!client_ctx) goto error;
     }
 
-#ifdef TLS1_3_VERSION
-    if (ctx_config->ciphersuites && !SSL_CTX_set_ciphersuites(ctx, ctx_config->ciphersuites)) {
-        serverLog(LL_WARNING, "Failed to configure ciphersuites: %s", ctx_config->ciphersuites);
-        goto error;
-    }
-#endif
-
     SSL_CTX_free(redis_tls_ctx);
+    SSL_CTX_free(redis_tls_client_ctx);
     redis_tls_ctx = ctx;
+    redis_tls_client_ctx = client_ctx;
 
     return C_OK;
 
 error:
     if (ctx) SSL_CTX_free(ctx);
+    if (client_ctx) SSL_CTX_free(client_ctx);
     return C_ERR;
 }
 
@@ -332,14 +363,21 @@ typedef struct tls_connection {
     listNode *pending_list_node;
 } tls_connection;
 
-connection *connCreateTLS(void) {
+static connection *createTLSConnection(bool client_side) {
+    SSL_CTX *ctx = redis_tls_ctx;
+    if (client_side && redis_tls_client_ctx)
+        ctx = redis_tls_client_ctx;
     tls_connection *conn = zcalloc(sizeof(tls_connection));
     conn->c.type = &CT_TLS;
     conn->c.fd = -1;
-    conn->ssl = SSL_new(redis_tls_ctx);
+    conn->ssl = SSL_new(ctx);
     return (connection *) conn;
 }
 
+connection *connCreateTLS(void) {
+    return createTLSConnection(true);
+}
+
 /* Fetch the latest OpenSSL error and store it in the connection */
 static void updateTLSError(tls_connection *conn) {
     conn->c.last_errno = 0;
@@ -358,7 +396,7 @@ static void updateTLSError(tls_connection *conn) {
  * is not in an error state.
  */
 connection *connCreateAcceptedTLS(int fd, int require_auth) {
-    tls_connection *conn = (tls_connection *) connCreateTLS();
+    tls_connection *conn = (tls_connection *) createTLSConnection(false);
     conn->c.fd = fd;
     conn->c.state = CONN_STATE_ACCEPTING;
 


Test patch:
diff --git a/tests/instances.tcl b/tests/instances.tcl
index d3b1b50cd91..8050a21a79a 100644
--- a/tests/instances.tcl
+++ b/tests/instances.tcl
@@ -76,8 +76,10 @@ proc spawn_instance {type base_port count {conf {}}} {
             puts $cfg "tls-replication yes"
             puts $cfg "tls-cluster yes"
             puts $cfg "port 0"
-            puts $cfg [format "tls-cert-file %s/../../tls/redis.crt" [pwd]]
-            puts $cfg [format "tls-key-file %s/../../tls/redis.key" [pwd]]
+            puts $cfg [format "tls-cert-file %s/../../tls/server.crt" [pwd]]
+            puts $cfg [format "tls-key-file %s/../../tls/server.key" [pwd]]
+            puts $cfg [format "tls-client-cert-file %s/../../tls/client.crt" [pwd]]
+            puts $cfg [format "tls-client-key-file %s/../../tls/client.key" [pwd]]
             puts $cfg [format "tls-dh-params-file %s/../../tls/redis.dh" [pwd]]
             puts $cfg [format "tls-ca-cert-file %s/../../tls/ca.crt" [pwd]]
             puts $cfg "loglevel debug"
@@ -234,8 +236,8 @@ proc parse_options {} {
             package require tls 1.6
             ::tls::init \
                 -cafile "$::tlsdir/ca.crt" \
-                -certfile "$::tlsdir/redis.crt" \
-                -keyfile "$::tlsdir/redis.key"
+                -certfile "$::tlsdir/client.crt" \
+                -keyfile "$::tlsdir/client.key"
             set ::tls 1
         } elseif {$opt eq "--help"} {
             puts "--single <pattern>      Only runs tests specified by pattern."
diff --git a/tests/support/benchmark.tcl b/tests/support/benchmark.tcl
index ed75bfeda7b..3d08b76f53f 100644
--- a/tests/support/benchmark.tcl
+++ b/tests/support/benchmark.tcl
@@ -1,7 +1,7 @@
 proc redisbenchmark_tls_config {testsdir} {
     set tlsdir [file join $testsdir tls]
-    set cert [file join $tlsdir redis.crt]
-    set key [file join $tlsdir redis.key]
+    set cert [file join $tlsdir client.crt]
+    set key [file join $tlsdir client.key]
     set cacert [file join $tlsdir ca.crt]
 
     if {$::tls} {
diff --git a/tests/support/cli.tcl b/tests/support/cli.tcl
index d55487931de..19e306e2486 100644
--- a/tests/support/cli.tcl
+++ b/tests/support/cli.tcl
@@ -1,7 +1,7 @@
 proc rediscli_tls_config {testsdir} {
     set tlsdir [file join $testsdir tls]
-    set cert [file join $tlsdir redis.crt]
-    set key [file join $tlsdir redis.key]
+    set cert [file join $tlsdir client.crt]
+    set key [file join $tlsdir client.key]
     set cacert [file join $tlsdir ca.crt]
 
     if {$::tls} {
diff --git a/tests/support/redis.tcl b/tests/support/redis.tcl
index 847f68e634c..efe0858750c 100644
--- a/tests/support/redis.tcl
+++ b/tests/support/redis.tcl
@@ -44,8 +44,8 @@ proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}}} {
         package require tls
         ::tls::init \
             -cafile "$::tlsdir/ca.crt" \
-            -certfile "$::tlsdir/redis.crt" \
-            -keyfile "$::tlsdir/redis.key" \
+            -certfile "$::tlsdir/client.crt" \
+            -keyfile "$::tlsdir/client.key" \
             {*}$tlsoptions
         set fd [::tls::socket $server $port]
     } else {
diff --git a/tests/support/server.tcl b/tests/support/server.tcl
index ae93ad0070c..dfa9315ac9d 100644
--- a/tests/support/server.tcl
+++ b/tests/support/server.tcl
@@ -229,6 +229,7 @@ proc start_server {options {code undefined}} {
     # setup defaults
     set baseconfig "default.conf"
     set overrides {}
+    set omit {}
     set tags {}
     set keep_persistence false
 
@@ -241,6 +242,9 @@ proc start_server {options {code undefined}} {
             "overrides" {
                 set overrides $value
             }
+            "omit" {
+                set omit $value
+            }
             "tags" {
                 # If we 'tags' contain multiple tags, quoted and seperated by spaces,
                 # we want to get rid of the quotes in order to have a proper list
@@ -306,8 +310,10 @@ proc start_server {options {code undefined}} {
     set data [split [exec cat "tests/assets/$baseconfig"] "\n"]
     set config {}
     if {$::tls} {
-        dict set config "tls-cert-file" [format "%s/tests/tls/redis.crt" [pwd]]
-        dict set config "tls-key-file" [format "%s/tests/tls/redis.key" [pwd]]
+        dict set config "tls-cert-file" [format "%s/tests/tls/server.crt" [pwd]]
+        dict set config "tls-key-file" [format "%s/tests/tls/server.key" [pwd]]
+        dict set config "tls-client-cert-file" [format "%s/tests/tls/client.crt" [pwd]]
+        dict set config "tls-client-key-file" [format "%s/tests/tls/client.key" [pwd]]
         dict set config "tls-dh-params-file" [format "%s/tests/tls/redis.dh" [pwd]]
         dict set config "tls-ca-cert-file" [format "%s/tests/tls/ca.crt" [pwd]]
         dict set config "loglevel" "debug"
@@ -343,6 +349,11 @@ proc start_server {options {code undefined}} {
         dict set config $directive $arguments
     }
 
+    # remove directives that are marked to be omitted
+    foreach directive $omit {
+        dict unset config $directive
+    }
+
     # write new configuration to temporary file
     set config_file [tmpfile redis.conf]
     create_server_config_file $config_file $config
diff --git a/tests/test_helper.tcl b/tests/test_helper.tcl
index 6a38cc0018b..81c9d61a2e2 100644
--- a/tests/test_helper.tcl
+++ b/tests/test_helper.tcl
@@ -596,8 +596,8 @@ for {set j 0} {$j < [llength $argv]} {incr j} {
         set ::tls 1
         ::tls::init \
             -cafile "$::tlsdir/ca.crt" \
-            -certfile "$::tlsdir/redis.crt" \
-            -keyfile "$::tlsdir/redis.key"
+            -certfile "$::tlsdir/client.crt" \
+            -keyfile "$::tlsdir/client.key"
     } elseif {$opt eq {--host}} {
         set ::external 1
         set ::host $arg
diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl
index 2d50b417b32..8d01a21e7ad 100644
--- a/tests/unit/introspection.tcl
+++ b/tests/unit/introspection.tcl
@@ -112,6 +112,8 @@ start_server {tags {"introspection"}} {
                 tls-session-caching
                 tls-cert-file
                 tls-key-file
+                tls-client-cert-file
+                tls-client-key-file
                 tls-dh-params-file
                 tls-ca-cert-file
                 tls-ca-cert-dir
diff --git a/tests/unit/tls.tcl b/tests/unit/tls.tcl
index bb5b6d034cf..a6c2f3c2c83 100644
--- a/tests/unit/tls.tcl
+++ b/tests/unit/tls.tcl
@@ -93,5 +93,26 @@ start_server {tags {"tls"}} {
             r CONFIG SET tls-protocols ""
             r CONFIG SET tls-ciphers "DEFAULT"
         }
+
+        test {TLS: Verify tls-cert-file is also used as a client cert if none specified} {
+            set master [srv 0 client]
+            set master_host [srv 0 host]
+            set master_port [srv 0 port]
+
+            # Use a non-restricted client/server cert for the replica
+            set redis_crt [format "%s/tests/tls/redis.crt" [pwd]]
+            set redis_key [format "%s/tests/tls/redis.key" [pwd]]
+
+            start_server [list overrides [list tls-cert-file $redis_crt tls-key-file $redis_key] \
+                               omit [list tls-client-cert-file tls-client-key-file]] {
+                set replica [srv 0 client]
+                $replica replicaof $master_host $master_port
+                wait_for_condition 30 100 {
+                    [string match {*master_link_status:up*} [$replica info replication]]
+                } else {
+                    fail "Can't authenticate to master using just tls-cert-file!"
+                }
+            }
+        }
     }
 }
diff --git a/utils/gen-test-certs.sh b/utils/gen-test-certs.sh
index a46edc55af6..60814483bd9 100755
--- a/utils/gen-test-certs.sh
+++ b/utils/gen-test-certs.sh
@@ -1,23 +1,58 @@
 #!/bin/bash
+
+# Generate some test certificates which are used by the regression test suite:
+#
+#   tests/tls/ca.{crt,key}          Self signed CA certificate.
+#   tests/tls/redis.{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 fro SSL server usage.
+#   tests/tls/redis.dh              DH Params file.
+
+generate_cert() {
+    local name=$1
+    local cn="$2"
+    local opts="$3"
+
+    local keyfile=tests/tls/${name}.key
+    local certfile=tests/tls/${name}.crt
+
+    [ -f $keyfile ] || openssl genrsa -out $keyfile 2048
+    openssl req \
+        -new -sha256 \
+        -subj "/O=Redis Test/CN=$cn" \
+        -key $keyfile | \
+        openssl x509 \
+            -req -sha256 \
+            -CA tests/tls/ca.crt \
+            -CAkey tests/tls/ca.key \
+            -CAserial tests/tls/ca.txt \
+            -CAcreateserial \
+            -days 365 \
+            $opts \
+            -out $certfile
+}
+
 mkdir -p tests/tls
-openssl genrsa -out tests/tls/ca.key 4096
+[ -f tests/tls/ca.key ] || openssl genrsa -out tests/tls/ca.key 4096
 openssl req \
     -x509 -new -nodes -sha256 \
     -key tests/tls/ca.key \
     -days 3650 \
     -subj '/O=Redis Test/CN=Certificate Authority' \
     -out tests/tls/ca.crt
-openssl genrsa -out tests/tls/redis.key 2048
-openssl req \
-    -new -sha256 \
-    -key tests/tls/redis.key \
-    -subj '/O=Redis Test/CN=Server' | \
-    openssl x509 \
-        -req -sha256 \
-        -CA tests/tls/ca.crt \
-        -CAkey tests/tls/ca.key \
-        -CAserial tests/tls/ca.txt \
-        -CAcreateserial \
-        -days 365 \
-        -out tests/tls/redis.crt
-openssl dhparam -out tests/tls/redis.dh 2048
+
+cat > tests/tls/openssl.cnf <<_END_
+[ server_cert ]
+keyUsage = digitalSignature, keyEncipherment
+nsCertType = server
+
+[ client_cert ]
+keyUsage = digitalSignature, keyEncipherment
+nsCertType = client
+_END_
+
+generate_cert server "Server-only" "-extfile tests/tls/openssl.cnf -extensions server_cert"
+generate_cert client "Client-only" "-extfile tests/tls/openssl.cnf -extensions client_cert"
+generate_cert redis "Generic-cert"
+
+[ -f tests/tls/redis.dh ] || openssl dhparam -out tests/tls/redis.dh 2048