|
|
|
|
@@ -1,9 +1,11 @@
|
|
|
|
|
"""
|
|
|
|
|
Redis client initialization and connection management
|
|
|
|
|
Provides standardized Redis connection for all services
|
|
|
|
|
|
|
|
|
|
Compatible with redis-py 6.4.0+
|
|
|
|
|
SSL/TLS support for self-signed certificates in Kubernetes environments
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import ssl
|
|
|
|
|
import redis.asyncio as redis
|
|
|
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
import structlog
|
|
|
|
|
@@ -19,6 +21,8 @@ def get_ssl_kwargs_for_url(redis_url: str) -> Dict[str, Any]:
|
|
|
|
|
Handles self-signed certificates by disabling certificate verification
|
|
|
|
|
when using rediss:// (TLS-enabled) URLs.
|
|
|
|
|
|
|
|
|
|
For redis-py 6.4.0+, ssl_cert_reqs accepts string values: "none", "optional", "required"
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
redis_url: Redis connection URL (redis:// or rediss://)
|
|
|
|
|
|
|
|
|
|
@@ -27,10 +31,8 @@ def get_ssl_kwargs_for_url(redis_url: str) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
if redis_url and redis_url.startswith("rediss://"):
|
|
|
|
|
return {
|
|
|
|
|
"ssl_cert_reqs": ssl.CERT_NONE, # Disable certificate verification
|
|
|
|
|
"ssl_ca_certs": None, # Don't require CA certificates
|
|
|
|
|
"ssl_certfile": None, # Don't require client cert
|
|
|
|
|
"ssl_keyfile": None, # Don't require client key
|
|
|
|
|
"ssl_cert_reqs": "none", # Don't verify server certificate (self-signed)
|
|
|
|
|
"ssl_check_hostname": False, # Don't verify hostname for internal services
|
|
|
|
|
}
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
@@ -133,130 +135,43 @@ class RedisConnectionManager:
|
|
|
|
|
try:
|
|
|
|
|
self._redis_url = redis_url
|
|
|
|
|
|
|
|
|
|
# Create connection pool with SSL handling for self-signed certificates
|
|
|
|
|
# For Redis 6.4.0+, we need to handle SSL parameters correctly
|
|
|
|
|
# Common connection kwargs for both TLS and non-TLS
|
|
|
|
|
connection_kwargs = {
|
|
|
|
|
'db': db,
|
|
|
|
|
'max_connections': max_connections,
|
|
|
|
|
'decode_responses': decode_responses,
|
|
|
|
|
'retry_on_timeout': retry_on_timeout,
|
|
|
|
|
'socket_keepalive': socket_keepalive,
|
|
|
|
|
'health_check_interval': health_check_interval
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# For redis-py 6.4.0+, SSL parameters must be passed to ConnectionPool.from_url()
|
|
|
|
|
# The library handles SSL internally when using rediss:// URLs
|
|
|
|
|
if redis_url.startswith("rediss://"):
|
|
|
|
|
# Extract connection parameters from URL
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
parsed_url = urlparse(redis_url)
|
|
|
|
|
|
|
|
|
|
# Build connection parameters for ConnectionPool
|
|
|
|
|
connection_params = {
|
|
|
|
|
'db': db,
|
|
|
|
|
'max_connections': max_connections,
|
|
|
|
|
'retry_on_timeout': retry_on_timeout,
|
|
|
|
|
'socket_keepalive': socket_keepalive,
|
|
|
|
|
'health_check_interval': health_check_interval
|
|
|
|
|
# SSL configuration for self-signed certificates (internal K8s traffic)
|
|
|
|
|
# Redis server has --tls-auth-clients no, so client certs are NOT required
|
|
|
|
|
# We use ssl_cert_reqs="none" to skip verifying the self-signed server cert
|
|
|
|
|
ssl_kwargs = {
|
|
|
|
|
'ssl_cert_reqs': "none", # Don't verify server certificate (self-signed)
|
|
|
|
|
'ssl_check_hostname': False, # Don't verify hostname (internal service names)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Add password if present
|
|
|
|
|
if parsed_url.password:
|
|
|
|
|
connection_params['password'] = parsed_url.password
|
|
|
|
|
connection_kwargs.update(ssl_kwargs)
|
|
|
|
|
|
|
|
|
|
# Create connection pool (without SSL parameters - they go to the client)
|
|
|
|
|
self._pool = redis.ConnectionPool(
|
|
|
|
|
host=parsed_url.hostname,
|
|
|
|
|
port=parsed_url.port or 6379,
|
|
|
|
|
**connection_params
|
|
|
|
|
self.logger.debug(
|
|
|
|
|
"redis_ssl_config",
|
|
|
|
|
ssl_enabled=True,
|
|
|
|
|
ssl_cert_reqs=ssl_kwargs.get('ssl_cert_reqs'),
|
|
|
|
|
ssl_check_hostname=ssl_kwargs.get('ssl_check_hostname')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get SSL configuration for self-signed certificates
|
|
|
|
|
ssl_kwargs = get_ssl_kwargs_for_url(redis_url)
|
|
|
|
|
# Use from_url for both TLS and non-TLS - it handles SSL automatically for rediss://
|
|
|
|
|
self._pool = redis.ConnectionPool.from_url(
|
|
|
|
|
redis_url,
|
|
|
|
|
**connection_kwargs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create Redis client with SSL parameters
|
|
|
|
|
client_params = {
|
|
|
|
|
'connection_pool': self._pool,
|
|
|
|
|
'decode_responses': decode_responses
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ssl_kwargs:
|
|
|
|
|
client_params['ssl'] = True
|
|
|
|
|
client_params['ssl_cert_reqs'] = ssl_kwargs.get('ssl_cert_reqs', ssl.CERT_NONE)
|
|
|
|
|
|
|
|
|
|
# For Kubernetes environments, try to use mounted TLS certificates
|
|
|
|
|
# These are typically mounted at /tls/redis-cert.pem, /tls/redis-key.pem, /tls/ca-cert.pem
|
|
|
|
|
import os
|
|
|
|
|
ca_certs_path = os.getenv('REDIS_CA_CERTS_PATH', '/tls/ca-cert.pem')
|
|
|
|
|
certfile_path = os.getenv('REDIS_CERTFILE_PATH', '/tls/redis-cert.pem')
|
|
|
|
|
keyfile_path = os.getenv('REDIS_KEYFILE_PATH', '/tls/redis-key.pem')
|
|
|
|
|
|
|
|
|
|
# Use environment variables or mounted files if they exist
|
|
|
|
|
if os.path.exists(ca_certs_path):
|
|
|
|
|
client_params['ssl_ca_certs'] = ca_certs_path
|
|
|
|
|
elif ssl_kwargs.get('ssl_ca_certs'):
|
|
|
|
|
client_params['ssl_ca_certs'] = ssl_kwargs.get('ssl_ca_certs')
|
|
|
|
|
|
|
|
|
|
if os.path.exists(certfile_path):
|
|
|
|
|
client_params['ssl_certfile'] = certfile_path
|
|
|
|
|
elif ssl_kwargs.get('ssl_certfile'):
|
|
|
|
|
client_params['ssl_certfile'] = ssl_kwargs.get('ssl_certfile')
|
|
|
|
|
|
|
|
|
|
if os.path.exists(keyfile_path):
|
|
|
|
|
client_params['ssl_keyfile'] = keyfile_path
|
|
|
|
|
elif ssl_kwargs.get('ssl_keyfile'):
|
|
|
|
|
client_params['ssl_keyfile'] = ssl_kwargs.get('ssl_keyfile')
|
|
|
|
|
|
|
|
|
|
# Add additional SSL context parameters for better compatibility
|
|
|
|
|
# These help with SSL handshake issues and protocol compatibility
|
|
|
|
|
client_params['ssl_check_hostname'] = False # Disable hostname verification for self-signed certs
|
|
|
|
|
|
|
|
|
|
# Add SSL context with specific protocol versions for better compatibility
|
|
|
|
|
# This helps with "wrong version number" and "unexpected eof" SSL errors
|
|
|
|
|
import ssl as ssl_module
|
|
|
|
|
ssl_context = ssl_module.create_default_context(
|
|
|
|
|
purpose=ssl_module.Purpose.SERVER_AUTH,
|
|
|
|
|
cafile=client_params.get('ssl_ca_certs')
|
|
|
|
|
)
|
|
|
|
|
ssl_context.check_hostname = False
|
|
|
|
|
ssl_context.verify_mode = client_params.get('ssl_cert_reqs', ssl_module.CERT_NONE)
|
|
|
|
|
|
|
|
|
|
# Set minimum TLS version for better security and compatibility
|
|
|
|
|
# TLS 1.2 is widely supported and secure enough for internal cluster communication
|
|
|
|
|
ssl_context.minimum_version = ssl_module.TLSVersion.TLSv1_2
|
|
|
|
|
|
|
|
|
|
# If client certificates are provided, load them
|
|
|
|
|
if client_params.get('ssl_certfile') and client_params.get('ssl_keyfile'):
|
|
|
|
|
ssl_context.load_cert_chain(
|
|
|
|
|
certfile=client_params.get('ssl_certfile'),
|
|
|
|
|
keyfile=client_params.get('ssl_keyfile')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client_params['ssl_context'] = ssl_context
|
|
|
|
|
|
|
|
|
|
# Debug: Log the SSL configuration being used
|
|
|
|
|
self.logger.debug(
|
|
|
|
|
"redis_ssl_config",
|
|
|
|
|
ssl_enabled=True,
|
|
|
|
|
ssl_cert_reqs=client_params.get('ssl_cert_reqs'),
|
|
|
|
|
ssl_ca_certs=client_params.get('ssl_ca_certs'),
|
|
|
|
|
ssl_certfile=client_params.get('ssl_certfile'),
|
|
|
|
|
ssl_keyfile=client_params.get('ssl_keyfile'),
|
|
|
|
|
ssl_check_hostname=False,
|
|
|
|
|
ssl_minimum_version="TLSv1_2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._client = redis.Redis(**client_params)
|
|
|
|
|
else:
|
|
|
|
|
# For non-TLS connections, use the original approach
|
|
|
|
|
connection_kwargs = {
|
|
|
|
|
'db': db,
|
|
|
|
|
'max_connections': max_connections,
|
|
|
|
|
'decode_responses': decode_responses,
|
|
|
|
|
'retry_on_timeout': retry_on_timeout,
|
|
|
|
|
'socket_keepalive': socket_keepalive,
|
|
|
|
|
'health_check_interval': health_check_interval
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Add SSL kwargs for self-signed certificates (using shared helper)
|
|
|
|
|
connection_kwargs.update(get_ssl_kwargs_for_url(redis_url))
|
|
|
|
|
|
|
|
|
|
self._pool = redis.ConnectionPool.from_url(
|
|
|
|
|
redis_url,
|
|
|
|
|
**connection_kwargs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._client = redis.Redis(connection_pool=self._pool)
|
|
|
|
|
self._client = redis.Redis(connection_pool=self._pool)
|
|
|
|
|
|
|
|
|
|
# Test connection
|
|
|
|
|
await self._client.ping()
|
|
|
|
|
|