diff --git a/README.md b/README.md index 60114c3f..6e215d1f 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ These options can be configured by setting environment variables using `-e KEY=" | `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. | | `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name | | `ENABLE_PROMETHEUS_METRICS` | `true` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`| - +| `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. | > If you change `WG_PORT`, make sure to also change the exposed port. diff --git a/docker-compose.yml b/docker-compose.yml index aa138e29..095557bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: # - UI_ENABLE_SORT_CLIENTS=true # - WG_ENABLE_EXPIRES_TIME=true # - ENABLE_PROMETHEUS_METRICS=false + # - PROMETHEUS_METRICS_PASSWORD=$$2a$$12$$vkvKpeEAHD78gasyawIod.1leBMKg8sBwKW.pQyNsq78bXV3INf2G # (needs double $$, hash of 'prometheus_password'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash) image: ghcr.io/wg-easy/wg-easy container_name: wg-easy diff --git a/src/config.js b/src/config.js index 30bfbc05..ae9da29d 100644 --- a/src/config.js +++ b/src/config.js @@ -42,3 +42,4 @@ module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS | module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false'; module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false'; module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'true'; +module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD; diff --git a/src/lib/Server.js b/src/lib/Server.js index c50770da..f673eb50 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -2,6 +2,7 @@ const bcrypt = require('bcryptjs'); const crypto = require('node:crypto'); +const basicAuth = require('basic-auth'); const { createServer } = require('node:http'); const { stat, readFile } = require('node:fs/promises'); const { resolve, sep } = require('node:path'); @@ -37,9 +38,11 @@ const { UI_ENABLE_SORT_CLIENTS, WG_ENABLE_EXPIRES_TIME, ENABLE_PROMETHEUS_METRICS, + PROMETHEUS_METRICS_PASSWORD, } = require('../config'); const requiresPassword = !!PASSWORD_HASH; +const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD; /** * Checks if `password` matches the PASSWORD_HASH. @@ -49,13 +52,12 @@ const requiresPassword = !!PASSWORD_HASH; * @param {string} password String to test * @returns {boolean} true if matching environment, otherwise false */ -const isPasswordValid = (password) => { +const isPasswordValid = (password, hash) => { if (typeof password !== 'string') { return false; } - - if (PASSWORD_HASH) { - return bcrypt.compareSync(password, PASSWORD_HASH); + if (hash) { + return bcrypt.compareSync(password, hash); } return false; @@ -163,7 +165,7 @@ module.exports = class Server { }); } - if (!isPasswordValid(password)) { + if (!isPasswordValid(password, PASSWORD_HASH)) { throw createError({ status: 401, message: 'Incorrect Password', @@ -193,7 +195,7 @@ module.exports = class Server { } if (req.url.startsWith('/api/') && req.headers['authorization']) { - if (isPasswordValid(req.headers['authorization'])) { + if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) { return next(); } return res.status(401).json({ @@ -308,20 +310,6 @@ module.exports = class Server { const { expireDate } = await readBody(event); await WireGuard.updateClientExpireDate({ clientId, expireDate }); return { success: true }; - })) - .get('/metrics', defineEventHandler(async (event) => { - setHeader(event, 'Content-Type', 'text/plain'); - if (ENABLE_PROMETHEUS_METRICS === 'true') { - return WireGuard.getMetrics(); - } - return ''; - })) - .get('/metrics/json', defineEventHandler(async (event) => { - setHeader(event, 'Content-Type', 'application/json'); - if (ENABLE_PROMETHEUS_METRICS === 'true') { - return WireGuard.getMetricsJSON(); - } - return ''; })); const safePathJoin = (base, target) => { @@ -347,6 +335,51 @@ module.exports = class Server { }); }; + // Prometheus Metrics API + const routerPrometheusMetrics = createRouter(); + app.use(routerPrometheusMetrics); + + // Check Prometheus credentials + app.use( + fromNodeMiddleware((req, res, next) => { + if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) { + return next(); + } + const user = basicAuth(req); + if (requiresPrometheusPassword && !user) { + res.statusCode = 401; + return { error: 'Not Logged In' }; + } + + if (req.url.startsWith('/metrics') && user.pass) { + if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) { + return next(); + } + res.statusCode = 401; + return { error: 'Incorrect Password' }; + } + res.statusCode = 401; + return { error: 'Not Logged In' }; + }), + ); + + // Prometheus Routes + routerPrometheusMetrics + .get('/metrics', defineEventHandler(async (event) => { + setHeader(event, 'Content-Type', 'text/plain'); + if (ENABLE_PROMETHEUS_METRICS === 'true') { + return WireGuard.getMetrics(); + } + return ''; + })) + .get('/metrics/json', defineEventHandler(async (event) => { + setHeader(event, 'Content-Type', 'application/json'); + if (ENABLE_PROMETHEUS_METRICS === 'true') { + return WireGuard.getMetricsJSON(); + } + return ''; + })); + // backup_restore const router3 = createRouter(); app.use(router3); diff --git a/src/package-lock.json b/src/package-lock.json index 09668cd6..a48758ee 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.1", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6", @@ -992,6 +993,24 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", diff --git a/src/package.json b/src/package.json index b0032c2a..007f08c2 100644 --- a/src/package.json +++ b/src/package.json @@ -15,6 +15,7 @@ "author": "Emile Nijssen", "license": "CC BY-NC-SA 4.0", "dependencies": { + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "crc-32": "^1.2.2", "debug": "^4.3.6",