From bce4835362ffa4bcd0125b0bfd4d6d9c8a7b4c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Fri, 4 Aug 2023 16:49:33 +0200 Subject: [PATCH] FEAT: Allow client side TLS for Docker hosts (#2852) * FEAT: Allow client side TLS for Docker hosts Inlcude TLS certificate in HTTPS requests when certificate files are locally available to Kuma for a host. * fix: refactor to satisfy linter requirements * fix: linter --- server/docker.js | 60 ++++++++++++++++++++++++++++++++++++++--- server/model/monitor.js | 3 +++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/server/docker.js b/server/docker.js index ff231502..11f06661 100644 --- a/server/docker.js +++ b/server/docker.js @@ -2,8 +2,15 @@ const axios = require("axios"); const { R } = require("redbean-node"); const version = require("../package.json").version; const https = require("https"); +const fs = require("fs"); class DockerHost { + + static CertificateBasePath = process.env.DOCKER_TLS_DIR_PATH || "data/docker-tls/"; + static CertificateFileNameCA = process.env.DOCKER_TLS_FILE_NAME_CA || "ca.pem"; + static CertificateFileNameCert = process.env.DOCKER_TLS_FILE_NAME_CA || "cert.pem"; + static CertificateFileNameKey = process.env.DOCKER_TLS_FILE_NAME_CA || "key.pem"; + /** * Save a docker host * @param {Object} dockerHost Docker host to save @@ -60,16 +67,13 @@ class DockerHost { * @returns {number} Total amount of containers on the host */ static async testDockerHost(dockerHost) { + const options = { url: "/containers/json?all=true", headers: { "Accept": "*/*", "User-Agent": "Uptime-Kuma/" + version }, - httpsAgent: new https.Agent({ - maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) - rejectUnauthorized: false, - }), }; if (dockerHost.dockerType === "socket") { @@ -77,6 +81,7 @@ class DockerHost { } else if (dockerHost.dockerType === "tcp") { options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon); } + options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL)); let res = await axios.request(options); @@ -111,6 +116,53 @@ class DockerHost { } return url; } + + /** + * Returns HTTPS agent options with client side TLS parameters if certificate files + * for the given host are available under a predefined directory path. + * + * The base path where certificates are looked for can be set with the + * 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'. + * + * If a directory in this path exists with a name matching the FQDN of the docker host + * (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory + * 'data/docker-tls/example.com/' would be searched for certificate files), + * then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options. + * File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'. + * + * @param {String} dockerType i.e. "tcp" or "socket" + * @param {String} url The docker host URL rewritten to https:// + * @return {Object} + * */ + static getHttpsAgentOptions(dockerType, url) { + let baseOptions = { + maxCachedSessions: 0, + rejectUnauthorized: true + }; + let certOptions = {}; + + let dirName = url.replace(/^https:\/\/([^/:]+)(\/|:).*$/, "$1"); + let dirPath = DockerHost.CertificateBasePath + dirName + "/"; + let caPath = dirPath + DockerHost.CertificateFileNameCA; + let certPath = dirPath + DockerHost.CertificateFileNameCert; + let keyPath = dirPath + DockerHost.CertificateFileNameKey; + + if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) { + let ca = fs.readFileSync(caPath); + let key = fs.readFileSync(keyPath); + let cert = fs.readFileSync(certPath); + certOptions = { + ca, + key, + cert + }; + } + + return { + ...baseOptions, + ...certOptions + }; + } } module.exports = { diff --git a/server/model/monitor.js b/server/model/monitor.js index f33dcbb7..a8f3d898 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -720,6 +720,9 @@ class Monitor extends BeanModel { options.socketPath = dockerHost._dockerDaemon; } else if (dockerHost._dockerType === "tcp") { options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon); + options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent( + DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL) + ); } log.debug("monitor", `[${this.name}] Axios Request`);