diff --git a/config.js b/config.js index 6338dd7d9a..a2bf6fbfb3 100644 --- a/config.js +++ b/config.js @@ -1126,6 +1126,9 @@ config.DEFAULT_REGION = 'us-east-1'; config.VACCUM_ANALYZER_INTERVAL = 86400000; +config.NOOBAA_METRICS_AUTH_ENABLED = process.env.NOOBAA_METRICS_AUTH_ENABLED === 'true'; +config.NOOBAA_VERSION_AUTH_ENABLED = process.env.NOOBAA_VERSION_AUTH_ENABLED === 'true'; + ///////////////////// // // // OVERRIDES // diff --git a/docs/NooBaaNonContainerized/ConfigFileCustomizations.md b/docs/NooBaaNonContainerized/ConfigFileCustomizations.md index 43e709e5bc..13723dd71e 100644 --- a/docs/NooBaaNonContainerized/ConfigFileCustomizations.md +++ b/docs/NooBaaNonContainerized/ConfigFileCustomizations.md @@ -449,7 +449,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a "LOG_TO_STDERR_ENABLED": false 3. systemctl restart noobaa ``` -### 30. Notification log directory +### 31. Notification log directory * Key `NOTIFICATION_LOG_DIR` * Type String * Default empty @@ -462,7 +462,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a "NOTIFICATION_LOG_DIR": "/etc/notif" 3. systemctl restart noobaa -### 31. Prometheus HTTP enable flag - +### 32. Prometheus HTTP enable flag - * Key: `ALLOW_HTTP_METRICS` * Type: Boolean * Default: true @@ -476,7 +476,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a 3. systemctl restart noobaa ``` -### 32. Prometheus HTTPS enable flag - +### 33. Prometheus HTTPS enable flag - * Key: `ALLOW_HTTPS_METRICS` * Type: Boolean * Default: true @@ -490,7 +490,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a 3. systemctl restart noobaa ``` -### 33. Notification space monitor frequency flag - +### 34. Notification space monitor frequency flag - * Key: `NOTIFICATION_REQ_PER_SPACE_CHECK` * Type: Positive integer * Default: 0 @@ -504,7 +504,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a 3. systemctl restart noobaa ``` -### 34. Notification space monitor threshold flag - +### 35. Notification space monitor threshold flag - * Key: `NOTIFICATION_SPACE_CHECK_THRESHOLD` * Type: Number * Default: 0.1 @@ -518,7 +518,7 @@ Warning: After setting this configuration, NooBaa will skip schema validations a 3. systemctl restart noobaa ``` -### 34. Dynamic supplemental groups allocation flag - +### 36. Dynamic supplemental groups allocation flag - * Key: `NSFS_ENABLE_DYNAMIC_SUPPLEMENTAL_GROUPS` * Type: boolean * Default: true diff --git a/src/endpoint/endpoint.js b/src/endpoint/endpoint.js index 86812eac8c..2f17d5c0e6 100755 --- a/src/endpoint/endpoint.js +++ b/src/endpoint/endpoint.js @@ -349,6 +349,9 @@ function create_endpoint_handler(server_type, init_request_sdk, { virtual_hosts, * @param {import('http').ServerResponse} res */ function version_handler(req, res) { + if (config.NOOBAA_VERSION_AUTH_ENABLED) { + if (!http_utils.authorize_bearer(req, res)) return; + } const noobaa_package_version = pkg.version; res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); diff --git a/src/endpoint/endpoint_utils.js b/src/endpoint/endpoint_utils.js index 71f93254bc..9213af93ed 100644 --- a/src/endpoint/endpoint_utils.js +++ b/src/endpoint/endpoint_utils.js @@ -4,6 +4,7 @@ const querystring = require('querystring'); const http_utils = require('../util/http_utils'); const pkg = require('../../package.json'); +const config = require('../../config'); function prepare_rest_request(req) { // generate request id, this is lighter than uuid @@ -40,7 +41,9 @@ function parse_source_url(source_url) { } function set_noobaa_server_header(res) { - res.setHeader('Server', `NooBaa/${pkg.version}`); + if (!config.NOOBAA_VERSION_AUTH_ENABLED) { + res.setHeader('Server', `NooBaa/${pkg.version}`); + } } exports.prepare_rest_request = prepare_rest_request; diff --git a/src/server/analytic_services/prometheus_reporting.js b/src/server/analytic_services/prometheus_reporting.js index 7d2e5a7e24..a3a6bced00 100644 --- a/src/server/analytic_services/prometheus_reporting.js +++ b/src/server/analytic_services/prometheus_reporting.js @@ -69,6 +69,12 @@ async function start_server( return; } const metrics_request_handler = async (req, res) => { + if (config.NOOBAA_METRICS_AUTH_ENABLED) { + // Authorize bearer token metrics endpoint + // Role 'metrics' is used in operator RPC call, + // Update operator RPC call first before changing role + if (!http_utils.authorize_bearer(req, res, [ "metrics", "admin" ])) return; + } // Serve all metrics on the root path for system that do have one or more fork running. if (fork_enabled) { // we would like this part to be first as clusterMetrics might fail. diff --git a/src/server/web_server.js b/src/server/web_server.js index c0efc683a7..5b751ed210 100755 --- a/src/server/web_server.js +++ b/src/server/web_server.js @@ -217,6 +217,10 @@ async function get_log_level_handler(req, res) { } async function get_version_handler(req, res) { + // Authorize bearer token version endpoint + if (config.NOOBAA_VERSION_AUTH_ENABLED) { + if (!http_utils.authorize_bearer(req, res)) return; + } const { status, version } = await getVersion(req.url); if (version) res.send(version); if (status !== 200) res.status(status); diff --git a/src/util/http_utils.js b/src/util/http_utils.js index 733eaf46ba..55906cd555 100644 --- a/src/util/http_utils.js +++ b/src/util/http_utils.js @@ -946,6 +946,55 @@ function set_response_headers_from_request(req, res) { if (req.query['response-expires']) res.setHeader('Expires', req.query['response-expires']); } +/** + * Authenticate JWT bearer token for metrics / version endpoints. + * Returns `true` on success, `false` after the function already sent an HTTP + * response (401/403) and the caller should terminate the handler early. + * + * @param {import('http').IncomingMessage} req + * @param {import('http').ServerResponse} res + * @param {string[]} roles + */ +function authorize_bearer(req, res, roles = undefined) { + const { authorization, ...rest_headers } = req.headers; + if (!authorization) { + dbg.error('Authentication required:', req.method, req.url, rest_headers); + // request lacks authentication, let the client know it's required with 401 Unauthorized + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Bearer'); + res.setHeader('Content-Type', 'text/plain'); + res.end('Unauthorized'); + return false; + } + if (!authorization.startsWith('Bearer ')) { + dbg.error('Authentication scheme must be Bearer:', req.method, req.url, rest_headers); + // authentication was provided but is invalid, return 403 Forbidden. + res.statusCode = 403; + res.setHeader('Content-Type', 'text/plain'); + res.end('Forbidden'); + return false; + } + const token = authorization.slice('Bearer '.length); + let auth; + try { + auth = jwt_utils.authorize_jwt_token(token); + } catch (err) { + dbg.error('Authentication failed to verify JWT token:', req.method, req.url, rest_headers, err); + res.statusCode = 403; + res.setHeader('Content-Type', 'text/plain'); + res.end('Forbidden'); + return false; + } + if (roles && !roles.includes(auth.role)) { + dbg.error('Authentication role is not allowed:', auth, roles, req.method, req.url, rest_headers); + res.statusCode = 403; + res.setHeader('Content-Type', 'text/plain'); + res.end('Forbidden'); + return false; + } + return true; +} + exports.parse_url_query = parse_url_query; exports.parse_client_ip = parse_client_ip; exports.get_md_conditions = get_md_conditions; @@ -984,3 +1033,4 @@ exports.CONTENT_TYPE_APP_JSON = CONTENT_TYPE_APP_JSON; exports.CONTENT_TYPE_APP_XML = CONTENT_TYPE_APP_XML; exports.CONTENT_TYPE_APP_FORM_URLENCODED = CONTENT_TYPE_APP_FORM_URLENCODED; exports.set_response_headers_from_request = set_response_headers_from_request; +exports.authorize_bearer = authorize_bearer; diff --git a/src/util/jwt_utils.js b/src/util/jwt_utils.js index 676fb16e53..854df2ae28 100644 --- a/src/util/jwt_utils.js +++ b/src/util/jwt_utils.js @@ -27,6 +27,10 @@ function make_internal_auth_token(object = {}, jwt_options = {}) { return make_auth_token(object, jwt_options); } +/** + * authorize jwt token by verifying it against the jwt secret + * @param {string} token + */ function authorize_jwt_token(token) { try { return jwt.verify(token, get_jwt_secret());