Skip to content

Commit

Permalink
Fixed: SSL/TLS should be working again (experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
ualex73 committed Jan 15, 2025
1 parent 79a61c2 commit 66bcdc4
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 48 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
140 changes: 93 additions & 47 deletions custom_components/monitor_docker/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,7 +68,7 @@
PRECISION,
)

VERSION = "1.19"
VERSION = "1.20b2"

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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"],
)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/monitor_docker/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 66bcdc4

Please sign in to comment.