From 4c4b8f30c4ddb290ac36f825247cf363f67e5eda Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Mon, 2 Feb 2026 15:11:25 +0200 Subject: [PATCH 1/5] Added testing with TLS-based authentication --- docker-compose.yml | 4 ++- dockers/sentinel.conf | 8 ------ tests/ssl_utils.py | 11 +++++++- tests/test_asyncio/test_ssl.py | 41 +++++++++++++++++++++++++++++ tests/test_ssl.py | 47 ++++++++++++++++++++++++++++++---- 5 files changed, 96 insertions(+), 15 deletions(-) delete mode 100644 dockers/sentinel.conf diff --git a/docker-compose.yml b/docker-compose.yml index 26bc5965a1..3de7e8eeba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ x-client-libs-stack-image: &client-libs-stack-image image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-8.4.0}" x-client-libs-image: &client-libs-image - image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.4.0}" + image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.6-rc1-21356658603-debian-amd64}" services: @@ -13,6 +13,7 @@ services: container_name: redis-standalone environment: - TLS_ENABLED=yes + - TLS_CLIENT_CNS=test_user - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 @@ -56,6 +57,7 @@ services: - NODES=6 - REPLICAS=1 - TLS_ENABLED=yes + - TLS_CLIENT_CNS=test_user - PORT=16379 - TLS_PORT=27379 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save "" --tls-cluster yes} diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf deleted file mode 100644 index 817a528765..0000000000 --- a/dockers/sentinel.conf +++ /dev/null @@ -1,8 +0,0 @@ -sentinel resolve-hostnames yes -sentinel monitor redis-py-test redis 6379 2 -# Be much more tolerant to transient stalls (index builds, GC, I/O) -sentinel down-after-milliseconds redis-py-test 60000 -# Avoid rapid repeated failover attempts -sentinel failover-timeout redis-py-test 180000 -# Keep it conservative: sync one replica at a time -sentinel parallel-syncs redis-py-test 1 \ No newline at end of file diff --git a/tests/ssl_utils.py b/tests/ssl_utils.py index 612c0d5aca..8d7b11530e 100644 --- a/tests/ssl_utils.py +++ b/tests/ssl_utils.py @@ -2,8 +2,11 @@ import os from collections import namedtuple +CN_USERNAME = "test_user" CLIENT_CERT_NAME = "client.crt" +CLIENT_CN_CERT_NAME = f"{CN_USERNAME}.crt" CLIENT_KEY_NAME = "client.key" +CLIENT_CN_KEY_NAME = f"{CN_USERNAME}.key" SERVER_CERT_NAME = "redis.crt" SERVER_KEY_NAME = "redis.key" CA_CERT_NAME = "ca.crt" @@ -12,7 +15,7 @@ class CertificateType(str, enum.Enum): client = "client" server = "server" - + client_cn = "client-cn" TLSFiles = namedtuple("TLSFiles", ["certfile", "keyfile", "ca_certfile"]) @@ -41,3 +44,9 @@ def get_tls_certificates( os.path.join(cert_dir, SERVER_KEY_NAME), os.path.join(cert_dir, CA_CERT_NAME), ) + elif cert_type == CertificateType.client_cn: + return TLSFiles( + os.path.join(cert_dir, CLIENT_CN_CERT_NAME), + os.path.join(cert_dir, CLIENT_CN_KEY_NAME), + os.path.join(cert_dir, CA_CERT_NAME), + ) diff --git a/tests/test_asyncio/test_ssl.py b/tests/test_asyncio/test_ssl.py index c5cbb90cfe..c3f8b16f7b 100644 --- a/tests/test_asyncio/test_ssl.py +++ b/tests/test_asyncio/test_ssl.py @@ -4,6 +4,8 @@ import pytest import pytest_asyncio import redis.asyncio as redis +from tests.conftest import skip_if_server_version_lt +from tests.ssl_utils import get_tls_certificates, CertificateType, CN_USERNAME # Skip test or not based on cryptography installation try: @@ -193,3 +195,42 @@ async def test_ssl_password_parameter(self, request): assert conn.ssl_context.password == test_password finally: await r.aclose() + + @skip_if_server_version_lt("8.5.0") + async def test_ssl_authenticate_with_client_cert(self, request, r): + """Test that when client certificate is used for authentication, + the connection is created successfully""" + + try: + # Non SSL client, to setup ACL + assert await r.acl_setuser( + "test_user", + enabled=True, + reset=True, + passwords=["+clientpass"], + keys=['*'], + commands=[ + "+acl" + ], + ) + finally: + await r.close() + + ssl_url = request.config.option.redis_ssl_url + p = urlparse(ssl_url)[1].split(":") + client_cn_cert, client_cn_key, ca_cert = get_tls_certificates( + request.session.config.REDIS_INFO["tls_cert_subdir"], CertificateType.client_cn + ) + r = redis.Redis( + host=p[0], + port=p[1], + ssl=True, + ssl_certfile=client_cn_cert, + ssl_keyfile=client_cn_key, + ssl_cert_reqs="required", + ssl_ca_certs=ca_cert, + ) + try: + assert await r.acl_whoami() == CN_USERNAME + finally: + await r.close() diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 96175d681f..d890b9ee35 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -6,8 +6,8 @@ import redis from redis.exceptions import ConnectionError, RedisError -from .conftest import skip_if_cryptography, skip_if_nocryptography -from .ssl_utils import CertificateType, get_tls_certificates +from .conftest import skip_if_cryptography, skip_if_nocryptography, skip_if_server_version_lt +from .ssl_utils import CertificateType, get_tls_certificates, CN_USERNAME @pytest.mark.ssl @@ -20,10 +20,10 @@ class TestSSL: @pytest.fixture(autouse=True) def _set_ssl_certs(self, request): - tls_cert_subdir = request.session.config.REDIS_INFO["tls_cert_subdir"] - self.client_certs = get_tls_certificates(tls_cert_subdir) + self.tls_cert_subdir = request.session.config.REDIS_INFO["tls_cert_subdir"] + self.client_certs = get_tls_certificates(self.tls_cert_subdir) self.server_certs = get_tls_certificates( - tls_cert_subdir, cert_type=CertificateType.server + self.tls_cert_subdir, cert_type=CertificateType.server ) def test_ssl_with_invalid_cert(self, request): @@ -425,3 +425,40 @@ def capture_context_wrap_socket(context_self, sock, **_kwargs): finally: r.close() + + @skip_if_server_version_lt("8.5.0") + def test_ssl_authenticate_with_client_cert(self, request, r): + """Test that when client certificate is used for authentication, + the connection is created successfully""" + + try: + # Non SSL client, to setup ACL + assert r.acl_setuser( + "test_user", + enabled=True, + reset=True, + passwords=["+clientpass"], + keys=['*'], + commands=[ + "+acl" + ], + ) + finally: + r.close() + + ssl_url = request.config.option.redis_ssl_url + p = urlparse(ssl_url)[1].split(":") + client_cn_cert, client_cn_key, ca_cert = get_tls_certificates(self.tls_cert_subdir, CertificateType.client_cn) + r = redis.Redis( + host=p[0], + port=p[1], + ssl=True, + ssl_certfile=client_cn_cert, + ssl_keyfile=client_cn_key, + ssl_cert_reqs="required", + ssl_ca_certs=ca_cert, + ) + try: + assert r.acl_whoami() == CN_USERNAME + finally: + r.close() From 38cb3e1ef0dfef1183581d01285458d896584c60 Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Mon, 2 Feb 2026 15:15:08 +0200 Subject: [PATCH 2/5] Revert changes --- dockers/sentinel.conf | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 dockers/sentinel.conf diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf new file mode 100644 index 0000000000..817a528765 --- /dev/null +++ b/dockers/sentinel.conf @@ -0,0 +1,8 @@ +sentinel resolve-hostnames yes +sentinel monitor redis-py-test redis 6379 2 +# Be much more tolerant to transient stalls (index builds, GC, I/O) +sentinel down-after-milliseconds redis-py-test 60000 +# Avoid rapid repeated failover attempts +sentinel failover-timeout redis-py-test 180000 +# Keep it conservative: sync one replica at a time +sentinel parallel-syncs redis-py-test 1 \ No newline at end of file From 175245212b9998262b9bba5bc7115bdc2543653d Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Mon, 2 Feb 2026 15:16:30 +0200 Subject: [PATCH 3/5] Revert changes --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3de7e8eeba..7d7b646d8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ x-client-libs-stack-image: &client-libs-stack-image image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-8.4.0}" x-client-libs-image: &client-libs-image - image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.6-rc1-21356658603-debian-amd64}" + image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.4.0}" services: From 2e1111b40fd1a29b9227d5b71eb66aa30f688985 Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Mon, 2 Feb 2026 15:18:21 +0200 Subject: [PATCH 4/5] Codestyle changes --- tests/ssl_utils.py | 1 + tests/test_asyncio/test_ssl.py | 9 ++++----- tests/test_ssl.py | 16 ++++++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/ssl_utils.py b/tests/ssl_utils.py index 8d7b11530e..3fe54a0d88 100644 --- a/tests/ssl_utils.py +++ b/tests/ssl_utils.py @@ -17,6 +17,7 @@ class CertificateType(str, enum.Enum): server = "server" client_cn = "client-cn" + TLSFiles = namedtuple("TLSFiles", ["certfile", "keyfile", "ca_certfile"]) diff --git a/tests/test_asyncio/test_ssl.py b/tests/test_asyncio/test_ssl.py index c3f8b16f7b..602f2b5951 100644 --- a/tests/test_asyncio/test_ssl.py +++ b/tests/test_asyncio/test_ssl.py @@ -208,10 +208,8 @@ async def test_ssl_authenticate_with_client_cert(self, request, r): enabled=True, reset=True, passwords=["+clientpass"], - keys=['*'], - commands=[ - "+acl" - ], + keys=["*"], + commands=["+acl"], ) finally: await r.close() @@ -219,7 +217,8 @@ async def test_ssl_authenticate_with_client_cert(self, request, r): ssl_url = request.config.option.redis_ssl_url p = urlparse(ssl_url)[1].split(":") client_cn_cert, client_cn_key, ca_cert = get_tls_certificates( - request.session.config.REDIS_INFO["tls_cert_subdir"], CertificateType.client_cn + request.session.config.REDIS_INFO["tls_cert_subdir"], + CertificateType.client_cn, ) r = redis.Redis( host=p[0], diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d890b9ee35..dd0b6059b9 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -6,7 +6,11 @@ import redis from redis.exceptions import ConnectionError, RedisError -from .conftest import skip_if_cryptography, skip_if_nocryptography, skip_if_server_version_lt +from .conftest import ( + skip_if_cryptography, + skip_if_nocryptography, + skip_if_server_version_lt, +) from .ssl_utils import CertificateType, get_tls_certificates, CN_USERNAME @@ -438,17 +442,17 @@ def test_ssl_authenticate_with_client_cert(self, request, r): enabled=True, reset=True, passwords=["+clientpass"], - keys=['*'], - commands=[ - "+acl" - ], + keys=["*"], + commands=["+acl"], ) finally: r.close() ssl_url = request.config.option.redis_ssl_url p = urlparse(ssl_url)[1].split(":") - client_cn_cert, client_cn_key, ca_cert = get_tls_certificates(self.tls_cert_subdir, CertificateType.client_cn) + client_cn_cert, client_cn_key, ca_cert = get_tls_certificates( + self.tls_cert_subdir, CertificateType.client_cn + ) r = redis.Redis( host=p[0], port=p[1], From 98bb9ec225dbde283438e2e821a2c36baaa933d8 Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Mon, 2 Feb 2026 15:25:10 +0200 Subject: [PATCH 5/5] Change hardcoded value to constant --- tests/test_asyncio/test_ssl.py | 2 +- tests/test_ssl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asyncio/test_ssl.py b/tests/test_asyncio/test_ssl.py index 602f2b5951..3673d19310 100644 --- a/tests/test_asyncio/test_ssl.py +++ b/tests/test_asyncio/test_ssl.py @@ -204,7 +204,7 @@ async def test_ssl_authenticate_with_client_cert(self, request, r): try: # Non SSL client, to setup ACL assert await r.acl_setuser( - "test_user", + CN_USERNAME, enabled=True, reset=True, passwords=["+clientpass"], diff --git a/tests/test_ssl.py b/tests/test_ssl.py index dd0b6059b9..fd193d06fc 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -438,7 +438,7 @@ def test_ssl_authenticate_with_client_cert(self, request, r): try: # Non SSL client, to setup ACL assert r.acl_setuser( - "test_user", + CN_USERNAME, enabled=True, reset=True, passwords=["+clientpass"],