From 07d283619536c3e2de48eb006a1fdaa4a9f2fdde Mon Sep 17 00:00:00 2001 From: diaznet Date: Fri, 30 Oct 2026 14:59:14 +0100 Subject: [PATCH 1/2] Add optional TLS for MySQL/MariaDB connections --- Config/database.php | 3 +++ README.md | 2 ++ bin/misp_create_configs.py | 5 +++++ bin/misp_create_database.py | 34 ++++++++++++++++++++++++++++------ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Config/database.php b/Config/database.php index f992a4c..2aa3fe8 100644 --- a/Config/database.php +++ b/Config/database.php @@ -11,6 +11,9 @@ class DATABASE_CONFIG { 'database' => '{{ MYSQL_DATABASE }}', 'prefix' => '', 'encoding' => 'utf8mb4 COLLATE utf8mb4_unicode_ci', +{% if MYSQL_USE_TLS %} + 'ssl_ca' => '{{ MYSQL_TLS_CA }}', +{% endif %} 'settings' => [ 'time_zone' => '"+00:00"', {% if MYSQL_SETTINGS -%} diff --git a/README.md b/README.md index 7a1ebce..d73485b 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ MISP requires MySQL or MariaDB database. * `MYSQL_DATABASE` (required, string) - database name * `MYSQL_SETTINGS` (optional, string) - database settings, which should be set for each db connection (JSON dict, or semicolon separated key value pairs) * `MYSQL_FLAGS` (required, string) - PDO flags which should be set for each db connection (JSON dict, or semicolon separated key value pairs) +* `MYSQL_USE_TLS` (optional, bool, default `false`) - connect to the database over TLS; the server certificate and hostname are always verified +* `MYSQL_TLS_CA` (optional, string, default `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem`) - path to the CA certificate (PEM) used to verify the database server certificate, for example a mounted file with your internal CA ### Redis diff --git a/bin/misp_create_configs.py b/bin/misp_create_configs.py index eecebb2..5d6ccaa 100644 --- a/bin/misp_create_configs.py +++ b/bin/misp_create_configs.py @@ -152,6 +152,8 @@ def parse_mysql_settings(variable_name: str, value: str) -> dict: "MYSQL_DATABASE": Option(required=True), "MYSQL_SETTINGS": Option(required=False, parser=parse_mysql_settings), "MYSQL_FLAGS": Option(required=False, parser=parse_mysql_settings), + "MYSQL_USE_TLS": Option(typ=bool, default=False), + "MYSQL_TLS_CA": Option(default="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"), # Redis "REDIS_HOST": Option(required=True), "REDIS_PORT": Option(typ=int, default=6379, validation=check_uint), @@ -594,6 +596,9 @@ def create(): if not variables[var]: variables[var] = variables[var.replace("_INNER", "")] + if variables["MYSQL_USE_TLS"] and not os.access(variables["MYSQL_TLS_CA"], os.R_OK): + error(f"TLS for MySQL is enabled, but CA file '{variables['MYSQL_TLS_CA']}' from 'MYSQL_TLS_CA' does not exist or is not readable") + if variables["OIDC_LOGIN"]: for var in ("OIDC_PROVIDER", "OIDC_CLIENT_CRYPTO_PASS", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET"): if not variables[var]: diff --git a/bin/misp_create_database.py b/bin/misp_create_database.py index eab4681..243218a 100644 --- a/bin/misp_create_database.py +++ b/bin/misp_create_database.py @@ -10,19 +10,38 @@ from pymysql.connections import Connection from typing import Optional, TextIO +DEFAULT_TLS_CA = "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" -def connect(host: str, port: int, user: str, password: Optional[str]) -> pymysql.connections.Connection: - connection = pymysql.connect(host=host, port=port, user=user, password=password, connect_timeout=1, client_flag=CLIENT.MULTI_STATEMENTS) + +def error(message: str): + print(f"ERROR: {message}", file=sys.stderr) + sys.exit(1) + + +def convert_bool(value: str) -> bool: + value = value.lower() + if value in ("true", "1", "yes", "on"): + return True + if value in ("false", "0", "no", "off", ""): + return False + + error(f"Environment variable 'MYSQL_USE_TLS' must be boolean (`true`, `1`, `yes`, `false`, `0` or `no`), `{value}` given") + + +def connect(host: str, port: int, user: str, password: Optional[str], tls_ca: Optional[str] = None) -> pymysql.connections.Connection: + # With a CA file, PyMySQL requires TLS and verifies both the certificate and the hostname + ssl = {"ca": tls_ca} if tls_ca else None + connection = pymysql.connect(host=host, port=port, user=user, password=password, connect_timeout=1, client_flag=CLIENT.MULTI_STATEMENTS, ssl=ssl) connection.ping() return connection -def wait_for_connection(host: str, port: int, user: str, password: Optional[str]) -> Connection: - logging.info(f"Connecting to MySQL server {host}:{port}") +def wait_for_connection(host: str, port: int, user: str, password: Optional[str], tls_ca: Optional[str] = None) -> Connection: + logging.info(f"Connecting to MySQL server {host}:{port}" + (" using TLS" if tls_ca else "")) last_exception = None for i in range(1, 10): try: - return connect(host, port, user, password) + return connect(host, port, user, password, tls_ca) except Exception as e: last_exception = e logging.info("Waiting for database connection...") @@ -60,7 +79,10 @@ def main(): port = os.environ.get("MYSQL_PORT") port = 3306 if port is None else int(port) - connection = wait_for_connection(args.host, port, args.user, password) + use_tls = convert_bool(os.environ.get("MYSQL_USE_TLS", "false")) + tls_ca = os.environ.get("MYSQL_TLS_CA") or DEFAULT_TLS_CA if use_tls else None + + connection = wait_for_connection(args.host, port, args.user, password, tls_ca) if is_schema_created(connection, args.database): logging.info("Database schema is already created.") From 58c991375488043b1bfe2a6ceedbf392356331ef Mon Sep 17 00:00:00 2001 From: diaznet Date: Fri, 30 Oct 2026 16:38:01 +0100 Subject: [PATCH 2/2] Added MYSQL_TLS_VERIFY boolean to give the ability to disable the database server certificate and hostname verification. --- Config/database.php | 7 +++++-- README.md | 3 ++- bin/misp_create_configs.py | 4 ++++ bin/misp_create_database.py | 32 ++++++++++++++++++++------------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Config/database.php b/Config/database.php index 2aa3fe8..83cf82b 100644 --- a/Config/database.php +++ b/Config/database.php @@ -22,9 +22,12 @@ class DATABASE_CONFIG { {% endfor %} {%- endif %} ], -{% if MYSQL_FLAGS %} +{% if MYSQL_FLAGS or (MYSQL_USE_TLS and not MYSQL_TLS_VERIFY) %} 'flags' => [ - {% for flag, value in MYSQL_FLAGS.items() %} +{% if MYSQL_USE_TLS and not MYSQL_TLS_VERIFY %} + PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, +{% endif %} + {% for flag, value in (MYSQL_FLAGS or {}).items() %} '{{ flag }}' => '{{ value }}', {% endfor %} ] diff --git a/README.md b/README.md index d73485b..d573efc 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,9 @@ MISP requires MySQL or MariaDB database. * `MYSQL_DATABASE` (required, string) - database name * `MYSQL_SETTINGS` (optional, string) - database settings, which should be set for each db connection (JSON dict, or semicolon separated key value pairs) * `MYSQL_FLAGS` (required, string) - PDO flags which should be set for each db connection (JSON dict, or semicolon separated key value pairs) -* `MYSQL_USE_TLS` (optional, bool, default `false`) - connect to the database over TLS; the server certificate and hostname are always verified +* `MYSQL_USE_TLS` (optional, bool, default `false`) - connect to the database over TLS; the server certificate and hostname are verified unless `MYSQL_TLS_VERIFY` is disabled * `MYSQL_TLS_CA` (optional, string, default `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem`) - path to the CA certificate (PEM) used to verify the database server certificate, for example a mounted file with your internal CA +* `MYSQL_TLS_VERIFY` (optional, bool, default `true`) - verify the database server certificate and hostname; set to `false` only if the server certificate cannot be verified (for example the self-signed certificate MariaDB generates automatically), the connection is then still encrypted, but not protected against man-in-the-middle attacks ### Redis diff --git a/bin/misp_create_configs.py b/bin/misp_create_configs.py index 5d6ccaa..a8c1295 100644 --- a/bin/misp_create_configs.py +++ b/bin/misp_create_configs.py @@ -154,6 +154,7 @@ def parse_mysql_settings(variable_name: str, value: str) -> dict: "MYSQL_FLAGS": Option(required=False, parser=parse_mysql_settings), "MYSQL_USE_TLS": Option(typ=bool, default=False), "MYSQL_TLS_CA": Option(default="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"), + "MYSQL_TLS_VERIFY": Option(typ=bool, default=True), # Redis "REDIS_HOST": Option(required=True), "REDIS_PORT": Option(typ=int, default=6379, validation=check_uint), @@ -599,6 +600,9 @@ def create(): if variables["MYSQL_USE_TLS"] and not os.access(variables["MYSQL_TLS_CA"], os.R_OK): error(f"TLS for MySQL is enabled, but CA file '{variables['MYSQL_TLS_CA']}' from 'MYSQL_TLS_CA' does not exist or is not readable") + if variables["MYSQL_USE_TLS"] and not variables["MYSQL_TLS_VERIFY"]: + warning("TLS certificate verification for MySQL is disabled ('MYSQL_TLS_VERIFY' is false). The connection is encrypted, but not protected against man-in-the-middle attacks.") + if variables["OIDC_LOGIN"]: for var in ("OIDC_PROVIDER", "OIDC_CLIENT_CRYPTO_PASS", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET"): if not variables[var]: diff --git a/bin/misp_create_database.py b/bin/misp_create_database.py index 243218a..54d9d69 100644 --- a/bin/misp_create_database.py +++ b/bin/misp_create_database.py @@ -18,30 +18,41 @@ def error(message: str): sys.exit(1) -def convert_bool(value: str) -> bool: +def convert_bool(variable_name: str, value: str) -> bool: value = value.lower() if value in ("true", "1", "yes", "on"): return True if value in ("false", "0", "no", "off", ""): return False - error(f"Environment variable 'MYSQL_USE_TLS' must be boolean (`true`, `1`, `yes`, `false`, `0` or `no`), `{value}` given") + error(f"Environment variable '{variable_name}' must be boolean (`true`, `1`, `yes`, `false`, `0` or `no`), `{value}` given") -def connect(host: str, port: int, user: str, password: Optional[str], tls_ca: Optional[str] = None) -> pymysql.connections.Connection: - # With a CA file, PyMySQL requires TLS and verifies both the certificate and the hostname - ssl = {"ca": tls_ca} if tls_ca else None +def tls_options() -> Optional[dict]: + if not convert_bool("MYSQL_USE_TLS", os.environ.get("MYSQL_USE_TLS", "false")): + return None + if convert_bool("MYSQL_TLS_VERIFY", os.environ.get("MYSQL_TLS_VERIFY", "true")): + # With a CA file, PyMySQL requires TLS and verifies both the certificate and the hostname + return {"ca": os.environ.get("MYSQL_TLS_CA") or DEFAULT_TLS_CA} + # Connection is still encrypted, but the server certificate is not verified + return {"verify_mode": False, "check_hostname": False} + + +def connect(host: str, port: int, user: str, password: Optional[str], ssl: Optional[dict] = None) -> pymysql.connections.Connection: connection = pymysql.connect(host=host, port=port, user=user, password=password, connect_timeout=1, client_flag=CLIENT.MULTI_STATEMENTS, ssl=ssl) connection.ping() return connection -def wait_for_connection(host: str, port: int, user: str, password: Optional[str], tls_ca: Optional[str] = None) -> Connection: - logging.info(f"Connecting to MySQL server {host}:{port}" + (" using TLS" if tls_ca else "")) +def wait_for_connection(host: str, port: int, user: str, password: Optional[str], ssl: Optional[dict] = None) -> Connection: + tls_info = "" + if ssl: + tls_info = " using TLS" if "ca" in ssl else " using TLS without certificate verification" + logging.info(f"Connecting to MySQL server {host}:{port}{tls_info}") last_exception = None for i in range(1, 10): try: - return connect(host, port, user, password, tls_ca) + return connect(host, port, user, password, ssl) except Exception as e: last_exception = e logging.info("Waiting for database connection...") @@ -79,10 +90,7 @@ def main(): port = os.environ.get("MYSQL_PORT") port = 3306 if port is None else int(port) - use_tls = convert_bool(os.environ.get("MYSQL_USE_TLS", "false")) - tls_ca = os.environ.get("MYSQL_TLS_CA") or DEFAULT_TLS_CA if use_tls else None - - connection = wait_for_connection(args.host, port, args.user, password, tls_ca) + connection = wait_for_connection(args.host, port, args.user, password, tls_options()) if is_schema_created(connection, args.database): logging.info("Database schema is already created.")