From 66bcdc49167b060b9755241821628c29e3669ad4 Mon Sep 17 00:00:00 2001 From: ualex73 Date: Wed, 15 Jan 2025 20:17:42 +0100 Subject: [PATCH] Fixed: SSL/TLS should be working again (experimental) --- README.md | 1 + custom_components/monitor_docker/helpers.py | 140 ++++++++++++------ .../monitor_docker/manifest.json | 2 +- 3 files changed, 95 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index edb2ba4..6bcd6a1 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ monitor_docker: | name | string (Required) | Client name of Docker daemon. Defaults to `Docker`. | | url | string (Optional) | Host URL of Docker daemon. Defaults to `unix://var/run/docker.sock`. Remote Docker daemon via TCP socket is also supported, use e.g. `http://ip:2375`. Do NOT add a slash add the end, this will invalidate the URL. For TLS support see the Q&A section. SSH is not supported. | | scan_interval | time_period (Optional) | Update interval. Defaults to 10 seconds. | +| retry | time_period (Optional) | Retry interval when a TCP error is detected. Defaults to 60 seconds. | | certpath | string (Optional) | If a TCP socket is used, you can define your Docker certificate path, forcing Monitor Docker to enable TLS. The filenames must be `cert.pem` and `key.pem`| | containers | list (Optional) | Array of containers to monitor. Defaults to all containers. | | containers_exclude | list (Optional) | Array of containers to be excluded from monitoring, when all containers are included. | diff --git a/custom_components/monitor_docker/helpers.py b/custom_components/monitor_docker/helpers.py index 2777661..f330a42 100644 --- a/custom_components/monitor_docker/helpers.py +++ b/custom_components/monitor_docker/helpers.py @@ -4,7 +4,9 @@ import concurrent import logging import os +import ssl from datetime import datetime, timezone +from pathlib import Path from typing import Any, Callable import aiodocker @@ -66,7 +68,7 @@ PRECISION, ) -VERSION = "1.19" +VERSION = "1.20b2" _LOGGER = logging.getLogger(__name__) @@ -130,54 +132,68 @@ async def init(self, startCount=0): # Check if it is a tcp connection or not tcpConnection = False - # Do some debugging logging for TCP/TLS + # Remove Docker environment variables + os.environ.pop("DOCKER_TLS_VERIFY", None) + os.environ.pop("DOCKER_CERT_PATH", None) + + # Setup Docker parameters + connector = None + session = None + ssl_context = None + if url is not None: _LOGGER.debug("%s: Docker URL is '%s'", self._instance, url) + else: + _LOGGER.debug( + "%s: Docker URL is auto-detect (most likely using 'unix://var/run/docker.socket')", + self._instance, + ) - # Check for TLS if it is not unix - if url.find("tcp:") == 0 or url.find("http:") == 0: + # If is not empty or an Unix socket, then do check TCP/SSL + if url is not None and url.find("unix:") == -1: - # Set this to true, api needs to called different - tcpConnection = True + # Check if URL is valid + if not ( + url.find("tcp:") == 0 + or url.find("http:") == 0 + or url.find("https:") == 0 + ): + raise ValueError( + f"[{self._instance}] Docker URL '{url}' does not start with tcp:, http: or https:" + ) - tlsverify = os.environ.get("DOCKER_TLS_VERIFY", None) - certpath = os.environ.get("DOCKER_CERT_PATH", None) - if tlsverify is None: - _LOGGER.debug( - "[%s]: Docker environment 'DOCKER_TLS_VERIFY' is NOT set", - self._instance, - ) - else: - _LOGGER.debug( - "[%s]: Docker environment set 'DOCKER_TLS_VERIFY=%s'", - self._instance, - tlsverify, - ) + if self._config[CONF_CERTPATH] and url.find("http:") == 0: + # fixup URL and warn + _LOGGER.warning( + "[%s] Docker URL '%s' should be https instead of http when using certificate path", + self._instance, + url, + ) + url = url.replace("http:", "https:") - if certpath is None: - _LOGGER.debug( - "[%s]: Docker environment 'DOCKER_CERT_PATH' is NOT set", - self._instance, - ) - else: - _LOGGER.debug( - "[%s]: Docker environment set 'DOCKER_CERT_PATH=%s'", - self._instance, - certpath, - ) + if self._config[CONF_CERTPATH] and url.find("tcp:") == 0: + # fixup URL and warn + _LOGGER.warning( + "[%s] Docker URL '%s' should be https instead of tcp when using certificate path", + self._instance, + url, + ) + url = url.replace("tcp:", "https:") - if self._config[CONF_CERTPATH]: - _LOGGER.debug( - "[%s]: Docker CertPath set '%s', setting environment variables DOCKER_TLS_VERIFY/DOCKER_CERT_PATH", - self._instance, - self._config[CONF_CERTPATH], - ) - os.environ["DOCKER_TLS_VERIFY"] = "1" - os.environ["DOCKER_CERT_PATH"] = self._config[CONF_CERTPATH] + if self._config[CONF_CERTPATH]: + _LOGGER.debug( + "[%s]: Docker certification path is '%s' SSL/TLS will be used", + self._instance, + self._config[CONF_CERTPATH], + ) + + # Create our SSL context object + ssl_context = await self._hass.async_add_executor_job( + self._docker_ssl_context + ) - # Create a new connector with 5 seconds timeout, otherwise it can be very long - if tcpConnection: - connector = TCPConnector() + # Setup new TCP connection, otherwise timeout takes toooo long + connector = TCPConnector(ssl=ssl_context) session = ClientSession( connector=connector, timeout=ClientTimeout( @@ -186,11 +202,11 @@ async def init(self, startCount=0): total=10, ), ) - self._api = aiodocker.Docker( - url=url, connector=connector, session=session - ) - else: - self._api = aiodocker.Docker(url=url) + + # Initiate the aiodocker instance now + self._api = aiodocker.Docker( + url=url, connector=connector, session=session, ssl_context=ssl_context + ) except Exception as err: exc_info = True if str(err) == "" else False @@ -244,6 +260,27 @@ async def init(self, startCount=0): self._config, ) + ############################################################# + def _docker_ssl_context(self) -> ssl.SSLContext | None: + """ + Create a SSLContext object + """ + + context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) + context.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS) # type: ignore + + path2 = Path(self._config[CONF_CERTPATH]) + + context.load_verify_locations(cafile=str(path2 / "ca.pem")) + context.load_cert_chain( + certfile=str(path2 / "cert.pem"), keyfile=str(path2 / "key.pem") + ) + + context.verify_flags &= ~ssl.VERIFY_X509_STRICT + context.check_hostname = False + + return context + ############################################################# def _monitor_stop(self, _service_or_event: Event) -> None: """Stop the monitor thread.""" @@ -306,10 +343,19 @@ async def _run_docker_events(self) -> None: if event is None: _LOGGER.debug("[%s] run_docker_events RAW: None", self._instance) else: + # If Type=container, give some additional information + addlog = "" + if event["Type"] == "container": + try: + addlog = f", Name={event['Actor']['Attributes']['name']}" + except: + pass + _LOGGER.debug( - "[%s] run_docker_events Type=%s, Action=%s", + "[%s] run_docker_events Type=%s%s, Action=%s", self._instance, event["Type"], + addlog, event["Action"], ) diff --git a/custom_components/monitor_docker/manifest.json b/custom_components/monitor_docker/manifest.json index 10a7cda..27e840a 100644 --- a/custom_components/monitor_docker/manifest.json +++ b/custom_components/monitor_docker/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/ualex73/monitor_docker/issues", "requirements": ["aiodocker==0.24.0", "python-dateutil==2.9.0.post0"], - "version": "1.20b1" + "version": "1.20b2" }