From 55b167562e614754bf8a0bbb187fe081f76ec676 Mon Sep 17 00:00:00 2001 From: AdamB Date: Fri, 14 Apr 2023 13:11:44 +0100 Subject: [PATCH] Implement authentication for Redis/Sentinel (#14805) * Implement ACL support for redis (and sentinel) Currently, sentinel only works with anonymous connections. Some parameters are passed when using sentinel, however these are dropped on the floor. This encapsulates them as py-redis expects, and passes them correctly. * Pass username * Differentiate duplicate error messages * Actually pass var * Docs and requirement bump * Lint * Consistency * More lint * Lint harder * Doc Updates --- LibreNMS/__init__.py | 18 +++++++++----- LibreNMS/queuemanager.py | 13 +++++++++- LibreNMS/service.py | 27 ++++++++++++++++++++- doc/Extensions/Dispatcher-Service.md | 36 +++++++++++++++++++++------- requirements.txt | 2 +- 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/LibreNMS/__init__.py b/LibreNMS/__init__.py index 53fe8d19a5..293b20554a 100644 --- a/LibreNMS/__init__.py +++ b/LibreNMS/__init__.py @@ -420,7 +420,7 @@ class ThreadingLock(Lock): class RedisLock(Lock): - def __init__(self, namespace="lock", **redis_kwargs): + def __init__(self, namespace="lock", sentinel_kwargs=None, **redis_kwargs): import redis # pylint: disable=import-error from redis.sentinel import Sentinel # pylint: disable=import-error @@ -433,9 +433,12 @@ class RedisLock(Lock): kwargs = { k: v for k, v in redis_kwargs.items() - if k in ["decode_responses", "password", "db", "socket_timeout"] + if k + in ["decode_responses", "username", "password", "db", "socket_timeout"] } - self._redis = Sentinel(sentinels, **kwargs).master_for(sentinel_service) + self._redis = Sentinel( + sentinels, sentinel_kwargs=sentinel_kwargs, **kwargs + ).master_for(sentinel_service) else: kwargs = {k: v for k, v in redis_kwargs.items() if "sentinel" not in k} self._redis = redis.Redis(**kwargs) @@ -527,7 +530,7 @@ class RedisLock(Lock): class RedisUniqueQueue(object): - def __init__(self, name, namespace="queue", **redis_kwargs): + def __init__(self, name, namespace="queue", sentinel_kwargs=None, **redis_kwargs): import redis # pylint: disable=import-error from redis.sentinel import Sentinel # pylint: disable=import-error @@ -540,9 +543,12 @@ class RedisUniqueQueue(object): kwargs = { k: v for k, v in redis_kwargs.items() - if k in ["decode_responses", "password", "db", "socket_timeout"] + if k + in ["decode_responses", "username", "password", "db", "socket_timeout"] } - self._redis = Sentinel(sentinels, **kwargs).master_for(sentinel_service) + self._redis = Sentinel( + sentinels, sentinel_kwargs=sentinel_kwargs, **kwargs + ).master_for(sentinel_service) else: kwargs = {k: v for k, v in redis_kwargs.items() if "sentinel" not in k} self._redis = redis.Redis(**kwargs) diff --git a/LibreNMS/queuemanager.py b/LibreNMS/queuemanager.py index 98e80ce3db..f7ead52bd7 100644 --- a/LibreNMS/queuemanager.py +++ b/LibreNMS/queuemanager.py @@ -203,10 +203,17 @@ class QueueManager: try: return LibreNMS.RedisUniqueQueue( self.queue_name(queue_type, group), + sentinel_kwargs={ + "username": self.config.redis_sentinel_user, + "password": self.config.redis_sentinel_pass, + "socket_timeout": self.config.redis_timeout, + "unix_socket_path": self.config.redis_socket, + }, namespace="librenms.queue", host=self.config.redis_host, port=self.config.redis_port, db=self.config.redis_db, + username=self.config.redis_user, password=self.config.redis_pass, unix_socket_path=self.config.redis_socket, sentinel=self.config.redis_sentinel, @@ -228,7 +235,11 @@ class QueueManager: logger.critical( "ERROR: Redis connection required for distributed polling" ) - logger.critical("Could not connect to Redis. {}".format(e)) + logger.critical( + "Queue manager could not connect to Redis. {}: {}".format( + type(e).__name__, e + ) + ) exit(2) return LibreNMS.UniqueQueue() diff --git a/LibreNMS/service.py b/LibreNMS/service.py index 468ebafa46..dcb306f49f 100644 --- a/LibreNMS/service.py +++ b/LibreNMS/service.py @@ -91,9 +91,12 @@ class ServiceConfig(DBConfig): redis_host = "localhost" redis_port = 6379 redis_db = 0 + redis_user = None redis_pass = None redis_socket = None redis_sentinel = None + redis_sentinel_user = None + redis_sentinel_pass = None redis_sentinel_service = None redis_timeout = 60 @@ -178,6 +181,9 @@ class ServiceConfig(DBConfig): self.redis_db = os.getenv( "REDIS_DB", config.get("redis_db", ServiceConfig.redis_db) ) + self.redis_user = os.getenv( + "REDIS_USERNAME", config.get("redis_user", ServiceConfig.redis_user) + ) self.redis_pass = os.getenv( "REDIS_PASSWORD", config.get("redis_pass", ServiceConfig.redis_pass) ) @@ -190,6 +196,14 @@ class ServiceConfig(DBConfig): self.redis_sentinel = os.getenv( "REDIS_SENTINEL", config.get("redis_sentinel", ServiceConfig.redis_sentinel) ) + self.redis_sentinel_user = os.getenv( + "REDIS_SENTINEL_USERNAME", + config.get("redis_sentinel_user", ServiceConfig.redis_sentinel_user), + ) + self.redis_sentinel_pass = os.getenv( + "REDIS_SENTINEL_PASSWORD", + config.get("redis_sentinel_pass", ServiceConfig.redis_sentinel_pass), + ) self.redis_sentinel_service = os.getenv( "REDIS_SENTINEL_SERVICE", config.get("redis_sentinel_service", ServiceConfig.redis_sentinel_service), @@ -644,10 +658,17 @@ class Service: """ try: return LibreNMS.RedisLock( + sentinel_kwargs={ + "username": self.config.redis_sentinel_user, + "password": self.config.redis_sentinel_pass, + "socket_timeout": self.config.redis_timeout, + "unix_socket_path": self.config.redis_socket, + }, namespace="librenms.lock", host=self.config.redis_host, port=self.config.redis_port, db=self.config.redis_db, + username=self.config.redis_user, password=self.config.redis_pass, unix_socket_path=self.config.redis_socket, sentinel=self.config.redis_sentinel, @@ -668,7 +689,11 @@ class Service: logger.critical( "ERROR: Redis connection required for distributed polling" ) - logger.critical("Could not connect to Redis. {}".format(e)) + logger.critical( + "Lock manager could not connect to Redis. {}: {}".format( + type(e).__name__, e + ) + ) self.exit(2) return LibreNMS.ThreadingLock() diff --git a/doc/Extensions/Dispatcher-Service.md b/doc/Extensions/Dispatcher-Service.md index b571daf469..d8394ba509 100644 --- a/doc/Extensions/Dispatcher-Service.md +++ b/doc/Extensions/Dispatcher-Service.md @@ -18,7 +18,7 @@ behaviour only found in Python3.4+. - PyMySQL is recommended as it requires no C compiler to install. MySQLclient can also be used, but does require compilation. - python-dotenv .env loader -- redis-py 3.0+ and Redis 5.0+ server (if using distributed polling) +- redis-py 4.0+ and Redis 5.0+ server (if using distributed polling) - psutil These can be obtained from your OS package manager, or from PyPI with the below commands. @@ -76,20 +76,40 @@ DB_PASSWORD= Once you have your Redis database set up, configure it in the .env file on each node. Configure the redis cache driver for distributed locking. +There are a number of options - most of them are optional if your redis instance is standalone and unauthenticated (neither recommended). + ```dotenv +## +## Standalone +## REDIS_HOST=127.0.0.1 REDIS_PORT=6379 -# OR -REDIS_SENTINEL=192.0.2.1:26379 -REDIS_SENTINEL_SERVICE=myservice - REDIS_DB=0 -#REDIS_PASSWORD= -#REDIS_TIMEOUT=60 +REDIS_TIMEOUT=60 -CACHE_DRIVER=redis +# If requirepass is set in redis set everything above as well as: (recommended) +REDIS_PASSWORD=PasswordGoesHere + +# If ACL's are in use, set everything above as well as: (highly recommended) +REDIS_USERNAME=UsernameGoesHere + +## +## Sentinel +## +REDIS_SENTINEL=redis-001.example.org:26379,redis-002.example.org:26379,redis-003.example.org:26379 +REDIS_SENTINEL_SERVICE=mymaster + +# If requirepass is set in sentinel, set everything above as well as: (recommended) +REDIS_SENTINEL_PASSWORD=SentinelPasswordGoesHere + +# If ACL's are in use, set everything above as well as: (highly recommended) +REDIS_SENTINEL_USERNAME=SentinelUsernameGoesHere ``` +For more information on ACL's, see + +Note that if you use Sentinel, you may still need `REDIS_PASSWORD`, `REDIS_USERNAME`, `REDIS_DB` and `REDIS_TIMEOUT` - Sentinel just provides the address of the instance currently accepting writes and manages failover. It's possible (and recommended) to have authentication both on Sentinel and the managed Redis instances. + ### Basic Configuration Additional configuration settings can be set in `config.php` or diff --git a/requirements.txt b/requirements.txt index c7eaec7af1..f9efb0847d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PyMySQL!=1.0.0 python-dotenv -redis>=3.0 +redis>=4.0 setuptools psutil>=5.6.0 command_runner>=1.3.0