From d8a99ef674cac1fd5054127bc034beaf200382f3 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:18:53 +0200 Subject: [PATCH] Added testing with SSL authentication using CN (#1627) * Added testing with SSL authentication using CN * Update CHANGELOG.md * Codestyle changes * Fixed broken tests * Fixed hybdrid tests * Fixed SSL tests * Fixed array merging * Updated README.md * Updated spelling list * Added support for Redis 8.6 * Codestyle changes and test fixes * Fixed hybdrid tests * Updated CHANGELOG.md and composer.json * Fixed XINFO tests --- .github/docker-compose.yml | 2 + .github/wordlist.txt | 4 + CHANGELOG.md | 1 + README.md | 34 ++++++++ phpunit.xml.dist | 5 ++ src/Cluster/ClusterStrategy.php | 3 + tests/PHPUnit/PredisTestCase.php | 12 ++- tests/Predis/Cluster/PredisStrategyTest.php | 3 + tests/Predis/Cluster/RedisStrategyTest.php | 3 + tests/Predis/SSLTest.php | 89 ++++++++++++++++++++- 10 files changed, 150 insertions(+), 6 deletions(-) diff --git a/.github/docker-compose.yml b/.github/docker-compose.yml index d4090c77..ce032bf8 100644 --- a/.github/docker-compose.yml +++ b/.github/docker-compose.yml @@ -24,6 +24,7 @@ services: container_name: redis-standalone environment: - TLS_ENABLED=yes + - TLS_CLIENT_CNS=test_user - REDIS_CLUSTER=no - REDIS_PASSWORD=foobar - PORT=6379 @@ -66,6 +67,7 @@ services: - NODES=6 - REPLICAS=1 - TLS_ENABLED=yes + - TLS_CLIENT_CNS=test_user - PORT=6372 - TLS_PORT=27379 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save "" --requirepass "foobar"} diff --git a/.github/wordlist.txt b/.github/wordlist.txt index b199dd7e..a8cc67d4 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -1,11 +1,15 @@ ACLs +AUTH Autoloading +cafile CAS +CN Customizable ElastiCache FPM GC IANA +mTLS Lua PSR Packagist diff --git a/CHANGELOG.md b/CHANGELOG.md index cff31819..aebe2573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Maintenance - Added testing with SSL connection (#1624) +- Added testing with SSL authentication using CN (#1627) - Added support for Redis 8.6 (#1631) ## v3.3.0 (2025-11-24) diff --git a/README.md b/README.md index b44bfaff..61743b19 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,40 @@ The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/pro also supported, with the difference that URI strings containing these schemes are parsed following the rules described on their respective IANA provisional registration documents. +Since Redis 8.6, you can authenticate a client using the Subject CN from its TLS client certificate (mTLS). +When this is enabled on the server, the client is authenticated during the TLS handshake, so you don’t need +to send an AUTH command. + +To use this, configure: + +- a CA certificate used to verify the server certificate (cafile), +- a client certificate (local_cert) signed by a CA trusted by the Redis server for client authentication, +- the corresponding private key (local_pk). + +Make sure: + +- the Redis server certificate is signed by a CA trusted by the client, and +- the client certificate is signed by a CA trusted by the Redis server (mTLS). + +```php +// Named array of connection parameters: +$client = new Predis\Client([ + 'scheme' => 'tls', + 'ssl' => [ + 'cafile' => 'ca.pem', // CA used to verify the server certificate + 'local_cert' => 'client.crt', // client certificate (Subject CN maps to ACL user) + 'local_pk' => 'client.key', // client private key + 'verify_peer' => true, + ], +]); + +// ACL user must exist and match the certificate Subject CN (example: CN=CN_NAME). +// Enable the user and grant permissions as needed: +$client->acl->setUser('CN_NAME', 'on', '>clientpass', 'allcommands', 'allkeys') + +echo $client->acl->whoami() // CN_NAME +``` + The actual list of supported connection parameters can vary depending on each connection backend so it is recommended to refer to their specific documentation or implementation for details. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 51382a29..e54283b9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -57,8 +57,13 @@ + + + + + [$this, 'getFakeKey'], + + /* control */ + 'ACL' => [$this, 'getFakeKey'], ]; } diff --git a/tests/PHPUnit/PredisTestCase.php b/tests/PHPUnit/PredisTestCase.php index 0a1ad099..6a2e28a7 100644 --- a/tests/PHPUnit/PredisTestCase.php +++ b/tests/PHPUnit/PredisTestCase.php @@ -286,27 +286,31 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase ); if ($this->isSSLTest()) { - $options = array_merge($options, [ + $options = $options + [ 'parameters' => [ 'ssl' => [ 'cafile' => getenv('CLUSTER_CA_CERT_PATH'), + 'local_cert' => getenv('CLUSTER_LOCAL_CERT_PATH'), + 'local_pk' => getenv('CLUSTER_LOCAL_PK_PATH'), 'verify_peer' => true, 'verify_peer_name' => false, 'allow_self_signed' => false, ], ], - ]); + ]; } } else { if ($this->isSSLTest()) { - $parameters = array_merge($parameters, [ + $parameters = $parameters + [ 'ssl' => [ 'cafile' => getenv('STANDALONE_CA_CERT_PATH'), + 'local_cert' => getenv('STANDALONE_LOCAL_CERT_PATH'), + 'local_pk' => getenv('STANDALONE_LOCAL_PK_PATH'), 'verify_peer' => true, 'verify_peer_name' => false, 'allow_self_signed' => false, ], - ]); + ]; } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index d1d60a2c..6e6f9d81 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -508,6 +508,9 @@ class PredisStrategyTest extends PredisTestCase /* cluster */ 'CLUSTER' => 'keys-fake', + + /* control */ + 'ACL' => 'keys-fake', ]; if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 70035b95..be2bee8b 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -531,6 +531,9 @@ class RedisStrategyTest extends PredisTestCase /* cluster */ 'CLUSTER' => 'keys-fake', + + /* control */ + 'ACL' => 'keys-fake', ]; if (isset($type)) { diff --git a/tests/Predis/SSLTest.php b/tests/Predis/SSLTest.php index 3533eb79..bfa8f7fa 100644 --- a/tests/Predis/SSLTest.php +++ b/tests/Predis/SSLTest.php @@ -26,7 +26,13 @@ class SSLTest extends PredisTestCase */ public function testExecuteCommandOverSSLConnection() { - $redis = $this->createClient(); + $redis = $this->createClient([ + 'ssl' => [ + 'cafile' => getenv('STANDALONE_CA_CERT_PATH'), + 'verify_peer' => true, + 'verify_peer_name' => false, + ], + ]); $this->assertEquals('PONG', $redis->ping()); } @@ -76,7 +82,16 @@ class SSLTest extends PredisTestCase */ public function testClusterExecuteCommandOverSSLConnection() { - $redis = $this->createClient(); + $redis = $this->createClient(null, [ + 'cluster' => 'redis', + 'parameters' => [ + 'ssl' => [ + 'cafile' => getenv('CLUSTER_CA_CERT_PATH'), + 'verify_peer' => true, + 'verify_peer_name' => false, + ], + ], + ]); $redis->set('foo', 'bar'); $this->assertEquals('bar', $redis->get('foo')); } @@ -123,4 +138,74 @@ class SSLTest extends PredisTestCase $redis->set('foo', 'bar'); } + + /** + * @group connected + * @group ssl + * @group relay-incompatible + * @requiresRedisVersion >= 8.5.0 + * @return void + */ + public function testAuthWithSSLCertificateWithCNSpecified() + { + $redis = $this->createClient(); + + $this->assertEquals( + 'OK', + $redis->acl->setUser('test_user', 'on', '>clientpass', 'allcommands', 'allkeys') + ); + + $redis->disconnect(); + + // Remove AUTH + $redis = $this->createClient(['password' => null]); + + $this->assertEquals(getenv('CN_USER_NAME'), $redis->acl->whoami()); + $this->assertEquals(1, $redis->acl->delUser(getenv('CN_USER_NAME'))); + } + + /** + * @group connected + * @group ssl + * @group cluster + * @group relay-incompatible + * @requiresRedisVersion >= 8.5.0 + * @return void + */ + public function testClusterAuthWithSSLCertificateWithCNSpecified() + { + $redis = $this->createClient(); + + $this->assertEquals( + 'OK', + $redis->acl->setUser('test_user', 'on', '>clientpass', 'allcommands', 'allkeys') + ); + + $redis->disconnect(); + + // Remove AUTH + $defaultParameters = $this->getDefaultParametersArray(); + $trimmedParameters = array_map(function (string $parameter) { + return explode('?', $parameter)[0]; + }, $defaultParameters); + + $redis = new Client( + $trimmedParameters, + [ + 'cluster' => 'redis', + 'parameters' => [ + 'ssl' => [ + 'cafile' => getenv('CLUSTER_CA_CERT_PATH'), + 'local_cert' => getenv('CLUSTER_LOCAL_CERT_PATH'), + 'local_pk' => getenv('CLUSTER_LOCAL_PK_PATH'), + 'verify_peer' => true, + 'verify_peer_name' => false, + ], + ], + ] + ); + + $this->assertEquals(getenv('CN_USER_NAME'), $redis->acl->whoami()); + $this->assertEquals(1, $redis->acl->delUser(getenv('CN_USER_NAME'))); + } }