From 9bb6b19832de919025d1d9c1aa536590c2ed636f Mon Sep 17 00:00:00 2001 From: Nash Kaminski <36900518+gs-kamnas@users.noreply.github.com> Date: Sun, 7 Aug 2022 14:53:29 -0500 Subject: [PATCH] Support for SSL/TLS protected connections to MySQL databases (#14142) * Allow configuration of the SSL/TLS operating mode when connecting to a mysql database * Support SSL/TLS DB connections in the dispatcher service as well * Apply black formatting standards to Python files * Suppress pylint errors as redis module is not installed when linting * More pylint fixes * Correct typo in logging output * Refactor SSL/TLS changes into DBConfig class instead of ServiceConfig * Define DB config variables as class vars instead of instance vars * Break circular import --- LibreNMS/__init__.py | 30 ++++++++++++++++++++++++------ LibreNMS/config.py | 23 +++++++++++++++++++++++ LibreNMS/service.py | 18 +++++++++--------- LibreNMS/wrapper.py | 18 +++--------------- config/database.php | 1 + 5 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 LibreNMS/config.py diff --git a/LibreNMS/__init__.py b/LibreNMS/__init__.py index 6e1bdf01be..c01681070e 100644 --- a/LibreNMS/__init__.py +++ b/LibreNMS/__init__.py @@ -26,7 +26,7 @@ from .service import Service, ServiceConfig # Hard limit script execution time so we don't get to "hang" DEFAULT_SCRIPT_TIMEOUT = 3600 -MAX_LOGFILE_SIZE = (1024 ** 2) * 10 # 10 Megabytes max log files +MAX_LOGFILE_SIZE = (1024**2) * 10 # 10 Megabytes max log files logger = logging.getLogger(__name__) @@ -248,6 +248,24 @@ class DB: if self.config.db_socket: args["unix_socket"] = self.config.db_socket + sslmode = self.config.db_sslmode.lower() + if sslmode == "disabled": + logger.debug("Using cleartext MySQL connection") + elif sslmode == "verify_ca": + logger.info( + "Using TLS MySQL connection without CN/SAN check (CA validation only)" + ) + args["ssl"] = {"ca": self.config.db_ssl_ca, "check_hostname": False} + elif sslmode == "verify_identity": + logger.info("Using TLS MySQL connection with full validation") + args["ssl"] = {"ca": self.config.db_ssl_ca} + else: + logger.critical( + "Unsupported MySQL sslmode %s, dispatcher supports DISABLED, VERIFY_CA, and VERIFY_IDENTITY only", + self.config.db_sslmode, + ) + raise SystemExit(2) + conn = MySQLdb.connect(**args) conn.autocommit(True) conn.ping(True) @@ -403,8 +421,8 @@ class ThreadingLock(Lock): class RedisLock(Lock): def __init__(self, namespace="lock", **redis_kwargs): - import redis - from redis.sentinel import Sentinel + import redis # pylint: disable=import-error + from redis.sentinel import Sentinel # pylint: disable=import-error redis_kwargs["decode_responses"] = True if redis_kwargs.get("sentinel") and redis_kwargs.get("sentinel_service"): @@ -440,7 +458,7 @@ class RedisLock(Lock): :param owner: str a unique name for the locking node :param expiration: int in seconds, 0 expiration means forever """ - import redis + import redis # pylint: disable=import-error try: if int(expiration) < 1: @@ -485,8 +503,8 @@ class RedisLock(Lock): class RedisUniqueQueue(object): def __init__(self, name, namespace="queue", **redis_kwargs): - import redis - from redis.sentinel import Sentinel + import redis # pylint: disable=import-error + from redis.sentinel import Sentinel # pylint: disable=import-error redis_kwargs["decode_responses"] = True if redis_kwargs.get("sentinel") and redis_kwargs.get("sentinel_service"): diff --git a/LibreNMS/config.py b/LibreNMS/config.py new file mode 100644 index 0000000000..ed884cf6c6 --- /dev/null +++ b/LibreNMS/config.py @@ -0,0 +1,23 @@ +class DBConfig: + """ + Bare minimal config class for LibreNMS.DB class usage + """ + + # Start with defaults and override + db_host = "localhost" + db_port = 0 + db_socket = None + db_user = "librenms" + db_pass = "" + db_name = "librenms" + db_sslmode = "disabled" + db_ssl_ca = "/etc/ssl/certs/ca-certificates.crt" + + def populate(self, _config): + for key, val in _config.items(): + if key == "db_port": + # Special case: port number + self.db_port = int(val) + elif key.startswith("db_"): + # Prevent prototype pollution by enforcing prefix + setattr(DBConfig, key, val) diff --git a/LibreNMS/service.py b/LibreNMS/service.py index e4cee0d29b..043f7b4800 100644 --- a/LibreNMS/service.py +++ b/LibreNMS/service.py @@ -4,9 +4,10 @@ import sys import threading import time -import pymysql +import pymysql # pylint: disable=import-error import LibreNMS +from LibreNMS.config import DBConfig try: import psutil @@ -37,7 +38,7 @@ except ImportError: logger = logging.getLogger(__name__) -class ServiceConfig: +class ServiceConfig(DBConfig): def __init__(self): """ Stores all of the configuration variables for the LibreNMS service in a common object @@ -96,13 +97,6 @@ class ServiceConfig: redis_sentinel_service = None redis_timeout = 60 - db_host = "localhost" - db_port = 0 - db_socket = None - db_user = "librenms" - db_pass = "" - db_name = "librenms" - watchdog_enabled = False watchdog_logfile = "logs/librenms.log" @@ -227,6 +221,12 @@ class ServiceConfig: self.db_user = os.getenv( "DB_USERNAME", config.get("db_user", ServiceConfig.db_user) ) + self.db_sslmode = os.getenv( + "DB_SSLMODE", config.get("db_sslmode", ServiceConfig.db_sslmode) + ) + self.db_ssl_ca = os.getenv( + "MYSQL_ATTR_SSL_CA", config.get("db_ssl_ca", ServiceConfig.db_ssl_ca) + ) self.watchdog_enabled = config.get( "service_watchdog_enabled", ServiceConfig.watchdog_enabled diff --git a/LibreNMS/wrapper.py b/LibreNMS/wrapper.py index 8ca8321bda..7fae1fc0fb 100644 --- a/LibreNMS/wrapper.py +++ b/LibreNMS/wrapper.py @@ -52,6 +52,7 @@ from argparse import ArgumentParser import LibreNMS from LibreNMS.command_runner import command_runner +from LibreNMS.config import DBConfig logger = logging.getLogger(__name__) @@ -320,20 +321,6 @@ def poll_worker( poll_queue.task_done() -class DBConfig: - """ - Bare minimal config class for LibreNMS.service.DB class usage - """ - - def __init__(self, _config): - self.db_socket = _config["db_socket"] - self.db_host = _config["db_host"] - self.db_port = int(_config["db_port"]) - self.db_user = _config["db_user"] - self.db_pass = _config["db_pass"] - self.db_name = _config["db_name"] - - def wrapper( wrapper_type, # Type: str amount_of_workers, # Type: int @@ -459,7 +446,8 @@ def wrapper( logger.critical("Bogus wrapper type called") sys.exit(3) - sconfig = DBConfig(config) + sconfig = DBConfig() + sconfig.populate(config) db_connection = LibreNMS.DB(sconfig) cursor = db_connection.query(query) devices = cursor.fetchall() diff --git a/config/database.php b/config/database.php index 4e5dad43bc..1f3d00d2c6 100644 --- a/config/database.php +++ b/config/database.php @@ -66,6 +66,7 @@ return [ 'prefix_indexes' => true, 'strict' => true, 'engine' => null, + 'sslmode' => env('DB_SSLMODE', 'disabled'), 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [],