From 38076e5a0d364fb3a488c7c58755de4247ee4d4e Mon Sep 17 00:00:00 2001 From: Srdjan Date: Wed, 15 Mar 2023 15:41:48 +1000 Subject: [PATCH] MDL-72622 core: Support TLS for redis cache connections --- cache/stores/redis/addinstanceform.php | 8 +++ .../stores/redis/lang/en/cachestore_redis.php | 4 ++ cache/stores/redis/lib.php | 66 +++++++++++++++---- cache/stores/redis/settings.php | 15 +++++ 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/cache/stores/redis/addinstanceform.php b/cache/stores/redis/addinstanceform.php index 1a3faa16c37..cdd9ed653a3 100644 --- a/cache/stores/redis/addinstanceform.php +++ b/cache/stores/redis/addinstanceform.php @@ -44,6 +44,14 @@ class cachestore_redis_addinstance_form extends cachestore_addinstance_form { $form->addHelpButton('server', 'server', 'cachestore_redis'); $form->addRule('server', get_string('required'), 'required'); + $form->addElement('advcheckbox', 'encryption', get_string('encrypt_connection', 'cachestore_redis')); + $form->setType('encryption', PARAM_BOOL); + $form->addHelpButton('encryption', 'encrypt_connection', 'cachestore_redis'); + + $form->addElement('text', 'cafile', get_string('ca_file', 'cachestore_redis')); + $form->setType('cafile', PARAM_TEXT); + $form->addHelpButton('cafile', 'ca_file', 'cachestore_redis'); + $form->addElement('passwordunmask', 'password', get_string('password', 'cachestore_redis')); $form->setType('password', PARAM_RAW); $form->addHelpButton('password', 'password', 'cachestore_redis'); diff --git a/cache/stores/redis/lang/en/cachestore_redis.php b/cache/stores/redis/lang/en/cachestore_redis.php index f323d312a5f..7b6cddb30c9 100644 --- a/cache/stores/redis/lang/en/cachestore_redis.php +++ b/cache/stores/redis/lang/en/cachestore_redis.php @@ -27,6 +27,10 @@ defined('MOODLE_INTERNAL') || die(); $string['compressor_none'] = 'No compression.'; $string['compressor_php_gzip'] = 'Use gzip compression.'; $string['compressor_php_zstd'] = 'Use Zstandard compression.'; +$string['encrypt_connection'] = 'Use TLS encryption.'; +$string['encrypt_connection_help'] = 'Use TLS to connect to Redis. Do not use \'tls://\' in the hostname for Redis, use this option instead.'; +$string['ca_file'] = 'CA file path'; +$string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem'; $string['pluginname'] = 'Redis'; $string['prefix'] = 'Key prefix'; $string['prefix_help'] = 'This prefix is used for all key names on the Redis server. diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index c28120020a0..3c72c7d7e0e 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -189,42 +189,60 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ if (array_key_exists('compressor', $configuration)) { $this->compressor = (int)$configuration['compressor']; } - $password = !empty($configuration['password']) ? $configuration['password'] : ''; - $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : ''; if (array_key_exists('lockwait', $configuration)) { $this->lockwait = (int)$configuration['lockwait']; } if (array_key_exists('locktimeout', $configuration)) { $this->locktimeout = (int)$configuration['locktimeout']; } - $this->redis = $this->new_redis($configuration['server'], $prefix, $password); + $this->redis = $this->new_redis($configuration); } /** * Create a new Redis instance and * connect to the server. * - * @param string $server The server connection string - * @param string $prefix The key prefix - * @param string $password The server connection password + * @param array $configuration The server configuration * @return Redis */ - protected function new_redis($server, $prefix = '', $password = '') { + protected function new_redis(array $configuration): \Redis { + global $CFG; + $redis = new Redis(); - // Check for Unix socket. + + $server = $configuration['server']; + $encrypt = (bool) ($configuration['encryption'] ?? false); + $password = !empty($configuration['password']) ? $configuration['password'] : ''; + $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : ''; + // Check if it isn't a Unix socket to set default port. + $port = null; + $opts = []; if ($server[0] === '/') { $port = 0; } else { $port = 6379; // No Unix socket so set default port. if (strpos($server, ':')) { // Check for custom port. - $serverconf = explode(':', $server); - $server = $serverconf[0]; - $port = $serverconf[1]; + list($server, $port) = explode(':', $server); + } + + // We can encrypt if we aren't unix socket. + if ($encrypt) { + $server = 'tls://' . $server; + if (empty($configuration['cafile'])) { + $sslopts = [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]; + } else { + $sslopts = ['cafile' => $configuration['cafile']]; + } + $opts['stream'] = $sslopts; } } try { - if ($redis->connect($server, $port)) { + if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) { + if (!empty($password)) { $redis->auth($password); } @@ -235,11 +253,20 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ if (!empty($prefix)) { $redis->setOption(Redis::OPT_PREFIX, $prefix); } + if ($encrypt && !$redis->ping()) { + /* + * In case of a TLS connection, if phpredis client does not + * communicate immediately with the server the connection hangs. + * See https://github.com/phpredis/phpredis/issues/2332 . + */ + throw new \RedisException("Ping failed"); + } $this->isready = true; } else { $this->isready = false; } } catch (\RedisException $e) { + debugging("redis $server: $e", DEBUG_NORMAL); $this->isready = false; } @@ -759,6 +786,8 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ 'password' => $data->password, 'serializer' => $data->serializer, 'compressor' => $data->compressor, + 'encryption' => $data->encryption, + 'cafile' => $data->cafile, ); } @@ -780,6 +809,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ if (!empty($config['compressor'])) { $data['compressor'] = $config['compressor']; } + if (!empty($config['encryption'])) { + $data['encryption'] = $config['encryption']; + } + if (!empty($config['cafile'])) { + $data['cafile'] = $config['cafile']; + } $editform->set_data($data); } @@ -805,6 +840,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ if (!empty($config->test_password)) { $configuration['password'] = $config->test_password; } + if (!empty($config->test_encryption)) { + $configuration['encryption'] = $config->test_encryption; + } + if (!empty($config->test_cafile)) { + $configuration['cafile'] = $config->test_cafile; + } // Make it possible to test TTL performance by hacking a copy of the cache definition. if (!empty($config->test_ttl)) { $definition = clone $definition; @@ -832,6 +873,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS, 'prefix' => $DB->get_prefix(), + 'encryption' => defined('TEST_CACHESTORE_REDIS_ENCRYPT') && TEST_CACHESTORE_REDIS_ENCRYPT, ]; } diff --git a/cache/stores/redis/settings.php b/cache/stores/redis/settings.php index 71781a82e47..d7e7dfc7c3f 100644 --- a/cache/stores/redis/settings.php +++ b/cache/stores/redis/settings.php @@ -34,6 +34,21 @@ $settings->add( 16 ) ); +$settings->add(new admin_setting_configcheckbox( + 'cachestore_redis/test_encryption', + get_string('encrypt_connection', 'cachestore_redis'), + get_string('encrypt_connection', 'cachestore_redis'), + false)); +$settings->add( + new admin_setting_configtext( + 'cachestore_redis/test_cafile', + get_string('ca_file', 'cachestore_redis'), + get_string('ca_file', 'cachestore_redis'), + '', + PARAM_TEXT, + 16 + ) +); $settings->add( new admin_setting_configpasswordunmask( 'cachestore_redis/test_password',