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
This commit is contained in:
Nash Kaminski 2022-08-07 14:53:29 -05:00 committed by GitHub
parent 1be8de0b24
commit 9bb6b19832
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 30 deletions

View File

@ -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"):

23
LibreNMS/config.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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'),
]) : [],