diff --git a/Config/database.php b/Config/database.php index f992a4c..83cf82b 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 -%} @@ -19,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 7a1ebce..d573efc 100644 --- a/README.md +++ b/README.md @@ -87,6 +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 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 eecebb2..a8c1295 100644 --- a/bin/misp_create_configs.py +++ b/bin/misp_create_configs.py @@ -152,6 +152,9 @@ 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"), + "MYSQL_TLS_VERIFY": Option(typ=bool, default=True), # Redis "REDIS_HOST": Option(required=True), "REDIS_PORT": Option(typ=int, default=6379, validation=check_uint), @@ -594,6 +597,12 @@ 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["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 eab4681..54d9d69 100644 --- a/bin/misp_create_database.py +++ b/bin/misp_create_database.py @@ -10,19 +10,49 @@ 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(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 '{variable_name}' must be boolean (`true`, `1`, `yes`, `false`, `0` or `no`), `{value}` given") + + +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]) -> Connection: - logging.info(f"Connecting to MySQL server {host}:{port}") +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) + return connect(host, port, user, password, ssl) except Exception as e: last_exception = e logging.info("Waiting for database connection...") @@ -60,7 +90,7 @@ 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) + 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.")