Перейти к основному содержимому

Redis 6 + SSL

· 4 мин. чтения

И так вы решили создать managed кластер Redis с поддержкой SSL в Yandex Cloud, но старые примеры кода не работают?

Например, используя PHP вы можете получать следующую ошибку:

PHP Fatal error:  Uncaught Predis\Connection\ConnectionException: Error while reading line from the server. [tcp://10.128.0.10:6380] in /usr/share/pear/Predis/Connection/AbstractConnection.php:155
Stack trace:
#0 /usr/share/pear/Predis/Connection/StreamConnection.php(314): Predis\Connection\AbstractConnection->onConnectionError()
#1 /usr/share/pear/Predis/Connection/AbstractConnection.php(120): Predis\Connection\StreamConnection->read()
#2 /usr/share/pear/Predis/Connection/AbstractConnection.php(112): Predis\Connection\AbstractConnection->readResponse()
#3 /usr/share/pear/Predis/Connection/StreamConnection.php(260): Predis\Connection\AbstractConnection->executeCommand()
#4 /usr/share/pear/Predis/Connection/AbstractConnection.php(180): Predis\Connection\StreamConnection->connect()
#5 /usr/share/pear/Predis/Connection/StreamConnection.php(288): Predis\Connection\AbstractConnection->getResource()
#6 /usr/share/pear/Predis/Connection/StreamConnection.php(394): Predis\Connection\StreamConnection->write()
#7 /usr/share/pear/Predis/C in /usr/share/pear/Predis/Connection/AbstractConnection.php on line 155

В NodeJS вы увидите что-то типа

[ioredis] Unhandled error event: Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
[ioredis] Unhandled error event: Error: read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:217:20)
[ioredis] Unhandled error event: Error: All sentinels are unreachable. Retrying from scratch after 10ms. Last error: Connection is closed.
at SentinelConnector.<anonymous> (/home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:73:31)
at Generator.next (<anonymous>)
at /home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:8:71
at new Promise (<anonymous>)
at __awaiter (/home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:4:12)
at connectToNext (/home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:59:37)
at SentinelConnector.<anonymous> (/home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:125:24)
at Generator.throw (<anonymous>)
at rejected (/home/nikthespirit/node_modules/ioredis/built/connectors/SentinelConnector/index.js:6:65)
at processTicksAndRejections (node:internal/process/task_queues:96:5)

В Python ошибка будет такой

Traceback (most recent call last):
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 814, in read_response
response = self._parser.read_response(disable_decoding=disable_decoding)
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 320, in read_response
raw = self._buffer.readline()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 251, in readline
self._read_from_socket()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 194, in _read_from_socket
data = self._sock.recv(socket_read_size)
ConnectionResetError: [Errno 104] Connection reset by peer
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test.py", line 8, in <module>
master.set('foo', 'bar')
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/commands/core.py", line 1673, in set
return self.execute_command("SET", *pieces, **options)
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/client.py", line 1173, in execute_command
conn = self.connection or pool.get_connection(command_name, **options)
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 1370, in get_connection
connection.connect()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/sentinel.py", line 54, in connect
return self.retry.call_with_retry(
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/retry.py", line 50, in call_with_retry
raise error
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/retry.py", line 45, in call_with_retry
return do()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/sentinel.py", line 44, in _connect_retry
self.connect_to(self.connection_pool.get_master_address())
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/sentinel.py", line 34, in connect_to
super().connect()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 619, in connect
self.on_connect()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 709, in on_connect
auth_response = self.read_response()
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/sentinel.py", line 61, in read_response
return super().read_response(disable_decoding=disable_decoding)
File "/home/nikthespirit/.local/lib/python3.8/site-packages/redis/connection.py", line 820, in read_response
raise ConnectionError(f"Error while reading from {hosterr}" f" : {e.args}")
redis.exceptions.ConnectionError: Error while reading from 10.128.0.22:6380 : (104, 'Connection reset by peer')

Отлично, теперь когда я перечислил все тексты ошибок в надежде, что поисковики их проиндексируют и приведут отчаявшихся в этот пост можно приступать к объяснению как же сделать чтобы все заработало.

PHP

<?php
require 'Predis/Autoloader.php';
Predis\Autoloader::register();

$sentinels = ['tcp://rc1a-dt766ydjtxyvhwvl.mdb.yandexcloud.net:26379','tcp://rc1c-8pqc9ac2q4vu7rel.mdb.yandexcloud.net:26379'];
$options = [
'replication' => 'sentinel',
'service' => 'redis959',
'parameters' => [
'password' => 'password',
# Нужно добавить следующие параметры
'scheme' => 'tls',
'ssl' => [
'cafile' => '/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt',
'allow_self_signed' => true,
'verify_peer_name' => false,
],
]
];
$client = new Predis\Client($sentinels, $options);

$client->set('foo', 'bar');
print_r($client->get('foo'));

NodeJS

const fs = require('fs');
const Redis = require('ioredis');
const redis = new Redis({
sentinels: [
{host: 'rc1a-z9glsnfzpomnp7l5.mdb.yandexcloud.net', port: 26379},{host: 'rc1c-iv4ttmsv9ndtoi8j.mdb.yandexcloud.net', port: 26379}
],
name: 'redis783',
password: 'password',
role: 'master',
// Нужно добавить следующие параметры
enableTLSForSentinelMode: true,
tls: {
ca: fs.readFileSync("/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt"),
checkServerIdentity: () => undefined
}
});

redis.set('foo', 'bar');
redis.get('foo').then( x => console.log(x))

Python

from redis.sentinel import Sentinel

hosts = ['rc1a-pzeodufqw7rj7ubz.mdb.yandexcloud.net','rc1b-mvr04pp3z854www2.mdb.yandexcloud.net']
sentinel = Sentinel(
[(h, 26379) for h in hosts],
socket_timeout=0.1,
# Нужно добавить следующие параметры
ssl=True,
ssl_ca_certs='/usr/local/share/ca-certificates/Yandex/YandexInternalRootCA.crt'
)
master = sentinel.master_for('redis757', password='password')
slave = sentinel.slave_for('redis757', password='password')

master.set('foo', 'bar')
print(slave.get('foo'))

Про отключение TLS-проверок

Как вы могли заметить в примерах на PHP и Node нам пришлось передать дополнительные параметры отключающие проверку hostname из сертификата. Если проверку не отключить, то вы будете получать примерно такие ошибки в Node

[ioredis] Unhandled error event: Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: IP: 10.128.0.22 is not in the cert's list:
at new NodeError (node:internal/errors:371:5)
at Object.checkServerIdentity (node:tls:346:12)
at TLSSocket.onConnectSecure (node:_tls_wrap:1540:27)
at TLSSocket.emit (node:events:520:28)
at TLSSocket._finishInit (node:_tls_wrap:944:8)
at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:725:12)

Или в PHP.

PHP Warning:  stream_socket_enable_crypto(): Peer certificate CN=`rc1a-dt766ydjtxyvhwvl.mdb.yandexcloud.net' did not match expected CN=`10.128.0.16' in /usr/share/pear/Predis/Connection/StreamConnection.php on line 246

Узлы кластера (redis-server и redis-sentinel) и клиенты оперируют ip-адресами, а в сертификатах указаны FQDN-записи. При попытке подключения с настройками по умолчанию TLS-библиотеки проверяют, соответствует ли хост, указанный в сертификате, хосту, к которому происходит подключение. Получается, клиент подключается к ip-адресу (например, 10.128.0.16), а в сертификате прописано доменное имя хоста (например, rc1a-dt766ydjtxyvhwvl.mdb.yandexcloud.net).

Проблема известная https://github.com/redis/redis/issues/7928, для ее решения в Redis некоторое время назад добавили поддержку работы не с адресами, а с хостнеймами. Эта возможность пока что экспериментальная, приводит к другим проблемам, и вряд ли будет бэкпортирована в предыдущие версии Redis.

Разработчики Redis предлагают отключить проверку соответствия хостнейма, оставив валидацию цепочки сертификатов, что мы и сделали в примерах кода выше.