From 1bba76413f858003e4bd7f323a003295e18ecff5 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 26 Oct 2023 09:20:29 +0200 Subject: [PATCH] feat: make all internal rate limits configurable (#5095) ### What This PR makes the rate limit for user creation and simple login (our password based login) configurable in the same way you can do metricsRateLimiting. ### Worth noting In addition this PR adds a `rate_limit{endpoint, method}` prometheus gauge, which gets the data from the UnleashConfig. --- .../__snapshots__/create-config.test.ts.snap | 4 ++ src/lib/create-config.ts | 21 ++++++++++ src/lib/metrics.ts | 42 +++++++++++++++++++ src/lib/routes/admin-api/user-admin.ts | 2 +- src/lib/routes/index.ts | 2 +- src/lib/types/option.ts | 7 ++++ .../reference/deploy/configuring-unleash.md | 11 +++-- 7 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 24ab989aa4..c80b022101 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -145,6 +145,10 @@ exports[`should create default config 1`] = ` "preRouterHook": undefined, "prometheusApi": undefined, "publicFolder": undefined, + "rateLimiting": { + "createUserMaxPerMinute": 20, + "simpleLoginMaxPerMinute": 10, + }, "secureHeaders": false, "segmentValuesLimit": 1000, "server": { diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index a845d913c2..2787a43b52 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -19,6 +19,7 @@ import { ICspDomainOptions, IClientCachingOption, IMetricsRateLimiting, + IRateLimiting, } from './types/option'; import { getDefaultLogProvider, LogLevel, validateLogProvider } from './logger'; import { defaultCustomAuthDenyAll } from './default-custom-auth-deny-all'; @@ -132,6 +133,23 @@ function loadMetricsRateLimitingConfig( ]); } +function loadRateLimitingConfig(options: IUnleashOptions): IRateLimiting { + const createUserMaxPerMinute = parseEnvVarNumber( + process.env.CREATE_USER_RATE_LIMIT_PER_MINUTE, + 20, + ); + const simpleLoginMaxPerMinute = parseEnvVarNumber( + process.env.SIMPLE_LOGIN_LIMIT_PER_MINUTE, + 10, + ); + + const defaultRateLimitOptions: IRateLimiting = { + createUserMaxPerMinute, + simpleLoginMaxPerMinute, + }; + return mergeAll([defaultRateLimitOptions, options.rateLimiting || {}]); +} + function loadUI(options: IUnleashOptions): IUIConfig { const uiO = options.ui || {}; const ui: IUIConfig = { @@ -525,6 +543,8 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { const metricsRateLimiting = loadMetricsRateLimitingConfig(options); + const rateLimiting = loadRateLimitingConfig(options); + return { db, session, @@ -559,6 +579,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { disableScheduler: options.disableScheduler, isEnterprise: isEnterprise, metricsRateLimiting, + rateLimiting, }; } diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 7c39636501..dafc40a484 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -190,6 +190,12 @@ export default class MetricsMonitor { labelNames: ['environment'], }); + const rateLimits = new client.Gauge({ + name: 'rate_limits', + help: 'Rate limits (per minute) for METHOD/ENDPOINT pairs', + labelNames: ['endpoint', 'method'], + }); + async function collectStaticCounters() { try { const stats = await instanceStatsService.getStats(); @@ -259,6 +265,42 @@ export default class MetricsMonitor { .labels({ range: clientStat.range }) .set(clientStat.count), ); + + rateLimits.reset(); + rateLimits + .labels({ endpoint: '/api/client/metrics', method: 'POST' }) + .set(config.metricsRateLimiting.clientMetricsMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/client/register', + method: 'POST', + }) + .set(config.metricsRateLimiting.clientRegisterMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/frontend/metrics', + method: 'POST', + }) + .set( + config.metricsRateLimiting.frontendMetricsMaxPerMinute, + ); + rateLimits + .labels({ + endpoint: '/api/frontend/register', + method: 'POST', + }) + .set( + config.metricsRateLimiting.frontendRegisterMaxPerMinute, + ); + rateLimits + .labels({ + endpoint: '/api/admin/user-admin', + method: 'POST', + }) + .set(config.rateLimiting.createUserMaxPerMinute); + rateLimits + .labels({ endpoint: '/auth/simple', method: 'POST' }) + .set(config.rateLimiting.simpleLoginMaxPerMinute); } catch (e) {} } diff --git a/src/lib/routes/admin-api/user-admin.ts b/src/lib/routes/admin-api/user-admin.ts index cc90701634..0db2f0bc65 100644 --- a/src/lib/routes/admin-api/user-admin.ts +++ b/src/lib/routes/admin-api/user-admin.ts @@ -288,7 +288,7 @@ export default class UserAdminController extends Controller { }), rateLimit({ windowMs: minutesToMilliseconds(1), - max: 20, + max: config.rateLimiting.createUserMaxPerMinute, validate: false, standardHeaders: true, legacyHeaders: false, diff --git a/src/lib/routes/index.ts b/src/lib/routes/index.ts index 3bf269afdf..91ba4e55f7 100644 --- a/src/lib/routes/index.ts +++ b/src/lib/routes/index.ts @@ -31,7 +31,7 @@ class IndexRouter extends Controller { new SimplePasswordProvider(config, services).router, rateLimit({ windowMs: minutesToMilliseconds(1), - max: 10, + max: config.rateLimiting.simpleLoginMaxPerMinute, validate: false, standardHeaders: true, legacyHeaders: false, diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index ba39760e42..eccb651d7d 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -124,6 +124,7 @@ export interface IUnleashOptions { publicFolder?: string; disableScheduler?: boolean; metricsRateLimiting?: Partial; + rateLimiting?: Partial; } export interface IEmailOption { @@ -193,6 +194,11 @@ export interface IMetricsRateLimiting { frontendRegisterMaxPerMinute: number; } +export interface IRateLimiting { + createUserMaxPerMinute: number; + simpleLoginMaxPerMinute: number; +} + export interface IUnleashConfig { db: IDBOption; session: ISessionOption; @@ -227,4 +233,5 @@ export interface IUnleashConfig { publicFolder?: string; disableScheduler?: boolean; isEnterprise: boolean; + rateLimiting: IRateLimiting; } diff --git a/website/docs/reference/deploy/configuring-unleash.md b/website/docs/reference/deploy/configuring-unleash.md index 790a3b13e7..e5d4cde6b0 100644 --- a/website/docs/reference/deploy/configuring-unleash.md +++ b/website/docs/reference/deploy/configuring-unleash.md @@ -140,10 +140,13 @@ unleash.start(unleashOptions); - **keepAliveTimeout** - Use this to tweak connection keepalive timeout in seconds. Useful for hosted situations where you need to make sure your connections are closed before terminating the instance. Defaults to `15`. Overridable with the `SERVER_KEEPALIVE_TIMEOUT` environment variable. You can also set the environment variable `ENABLED_ENVIRONMENTS` to a comma delimited string of environment names to override environments. - **metricsRateLimiting** - Use the following to tweak the rate limits for `/api/client/register`, `/api/client/metrics`, `/api/frontend/register` and `/api/frontend/metrics` POST endpoints - - `clientMetricsMaxPerMinute` - How many requests per minute is allowed against POST `/api/client/metrics` before returning 429. Set to 6000 by default (100rps) - Overridable with `REGISTER_CLIENT_RATE_LIMIT_PER_MINUTE` environment variable - - `clientRegisterMaxPerMinute` - How many requests per minute is allowed against POST `/api/client/register` before returning 429. Set to 6000 by default (100rps) - Overridable with `CLIENT_METRICS_RATE_LIMIT_PER_MINUTE` environment variable - - `frontendMetricsMaxPerMinute` - How many requests per minute is allowed against POST `/api/frontend/metrics` before returning 429. Set to 6000 by default (100rps) - Overridable with `FRONTEND_METRICS_RATE_LIMIT_PER_MINUTE` environment variable - - `frontendRegisterMaxPerMinute` - How many requests per minute is allowed against POST `/api/frontend/register` before returning 429. Set to 6000 by default (100rps) - Overridable with `REGISTER_FRONTEND_RATE_LIMIT_PER_MINUTE` environment variable + - `clientMetricsMaxPerMinute` - How many requests per minute per IP is allowed against POST `/api/client/metrics` before returning 429. Set to 6000 by default (100rps) - Overridable with `REGISTER_CLIENT_RATE_LIMIT_PER_MINUTE` environment variable + - `clientRegisterMaxPerMinute` - How many requests per minute per IP is allowed against POST `/api/client/register` before returning 429. Set to 6000 by default (100rps) - Overridable with `CLIENT_METRICS_RATE_LIMIT_PER_MINUTE` environment variable + - `frontendMetricsMaxPerMinute` - How many requests per minute per IP is allowed against POST `/api/frontend/metrics` before returning 429. Set to 6000 by default (100rps) - Overridable with `FRONTEND_METRICS_RATE_LIMIT_PER_MINUTE` environment variable + - `frontendRegisterMaxPerMinute` - How many requests per minute per IP is allowed against POST `/api/frontend/register` before returning 429. Set to 6000 by default (100rps) - Overridable with `REGISTER_FRONTEND_RATE_LIMIT_PER_MINUTE` environment variable +- **rateLimiting** - Use the following to tweak the rate limits for `POST /auth/simple` (Password-based login) and `POST /api/admin/user-admin` (Creating users) + - `simpleLoginMaxPerMinute` - How many requests per minute per IP is allowed against POST `/auth/simple` before returning 429. Set to 10 by default - Overridable with `SIMPLE_LOGIN_LIMIT_PER_MINUTE` environment variable + - `createUserMaxPerMinute` - How many requests per minute per IP is allowed against POST `/api/admin/user-admin` before returning 429. Set to 20 by default - Overridable with `CREATE_USER_RATE_LIMIT_PER_MINUTE` environment variable ### Disabling Auto-Start {#disabling-auto-start} If you're using Unleash as part of a larger express app, you can disable the automatic server start by calling `server.create`. It takes the same options as `server.start`, but will not begin listening for connections.