Skip to content

Commit

Permalink
Implement authentication for Redis/Sentinel (librenms#14805)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TheMysteriousX authored Apr 14, 2023
1 parent 3362e0e commit 55b1675
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 17 deletions.
18 changes: 12 additions & 6 deletions LibreNMS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def print_locks(self):


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

Expand All @@ -433,9 +433,12 @@ def __init__(self, namespace="lock", **redis_kwargs):
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)
Expand Down Expand Up @@ -527,7 +530,7 @@ def print_locks(self):


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

Expand All @@ -540,9 +543,12 @@ def __init__(self, name, namespace="queue", **redis_kwargs):
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)
Expand Down
13 changes: 12 additions & 1 deletion LibreNMS/queuemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,17 @@ def _create_queue(self, queue_type, group):
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,
Expand All @@ -228,7 +235,11 @@ def _create_queue(self, queue_type, group):
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()
Expand Down
27 changes: 26 additions & 1 deletion LibreNMS/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ def __init__(self, workers, frequency, calculate=None):
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

Expand Down Expand Up @@ -178,6 +181,9 @@ def populate(self):
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)
)
Expand All @@ -190,6 +196,14 @@ def populate(self):
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),
Expand Down Expand Up @@ -644,10 +658,17 @@ def create_lock_manager(self):
"""
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,
Expand All @@ -668,7 +689,11 @@ def create_lock_manager(self):
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()
Expand Down
36 changes: 28 additions & 8 deletions doc/Extensions/Dispatcher-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
# 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
CACHE_DRIVER=redis
##
## 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 <https://redis.io/docs/management/security/acl/>

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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 55b1675

Please sign in to comment.