Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Allow client side TLS for Docker hosts #2852

Merged
merged 3 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions server/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/";
Copy link
Owner

@louislam louislam Jul 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pr.

Since data/ can be changed by the env var DATA_DIR, data/ cannot be hardcoded here.

But actually, in my opinion, these 4 env vars are not necessary, just hardcode:

  • Database.dataDir + "docker-tls/"
  • ca.pem
  • cert.pem
  • key.pem

Other sub-directories are created in this function.

Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
Database.uploadDir = Database.dataDir + "upload/";
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/";
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}

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
Expand Down Expand Up @@ -60,23 +67,21 @@ 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") {
options.socketPath = dockerHost.dockerDaemon;
} 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);

Expand Down Expand Up @@ -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 = {
Expand Down
3 changes: 3 additions & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,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`);
Expand Down