From 63df6af4c2d00d91856e2f86f00d51daf0fae707 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 19 Oct 2023 11:36:44 +0200 Subject: [PATCH 1/3] feat: All rate limits are now configurable In addition this PR exposes the limits set to prometheus under the rate_limit{endpoint, method} gauge. --- src/lib/create-config.ts | 21 +++++++++++++ src/lib/metrics.ts | 30 +++++++++++++++++++ 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 | 3 ++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index a845d913c2e3..2787a43b528e 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 7c3963650130..b1e58e7cb14d 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,30 @@ export default class MetricsMonitor { .labels({ range: clientStat.range }) .set(clientStat.count), ); + + rateLimits.reset(); + rateLimits + .labels('/api/client/metrics', 'POST') + .set(config.metricsRateLimiting.clientMetricsMaxPerMinute); + rateLimits + .labels('/api/client/register', 'POST') + .set(config.metricsRateLimiting.clientRegisterMaxPerMinute); + rateLimits + .labels('/api/frontend/metrics', 'POST') + .set( + config.metricsRateLimiting.frontendMetricsMaxPerMinute, + ); + rateLimits + .labels('/api/frontend/register', 'POST') + .set( + config.metricsRateLimiting.frontendRegisterMaxPerMinute, + ); + rateLimits + .labels('/api/admin/user-admin', 'POST') + .set(config.rateLimiting.createUserMaxPerMinute); + rateLimits + .labels('/auth/simple', '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 cc9070163430..0db2f0bc6536 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 3bf269afdf9d..91ba4e55f752 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 ba39760e422a..eccb651d7d75 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 790a3b13e7c7..1f967e7a4f7a 100644 --- a/website/docs/reference/deploy/configuring-unleash.md +++ b/website/docs/reference/deploy/configuring-unleash.md @@ -144,6 +144,9 @@ You can also set the environment variable `ENABLED_ENVIRONMENTS` to a comma deli - `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 +- **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 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 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. From e02f6836d103096a4c02776d0ade4b7279bf230c Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 19 Oct 2023 11:43:25 +0200 Subject: [PATCH 2/3] docs: updated to include information that the rate limit is per ip --- src/lib/__snapshots__/create-config.test.ts.snap | 4 ++++ website/docs/reference/deploy/configuring-unleash.md | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index a2a9656ca223..f079d97529c4 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -195,6 +195,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/website/docs/reference/deploy/configuring-unleash.md b/website/docs/reference/deploy/configuring-unleash.md index 1f967e7a4f7a..e5d4cde6b052 100644 --- a/website/docs/reference/deploy/configuring-unleash.md +++ b/website/docs/reference/deploy/configuring-unleash.md @@ -140,13 +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 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 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 + - `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. From 8f5843a44e4530592413a5a99aa24086e6737e98 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 19 Oct 2023 11:48:10 +0200 Subject: [PATCH 3/3] fix: updated with named object for labelnames --- src/lib/metrics.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index b1e58e7cb14d..dafc40a484bd 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -268,26 +268,38 @@ export default class MetricsMonitor { rateLimits.reset(); rateLimits - .labels('/api/client/metrics', 'POST') + .labels({ endpoint: '/api/client/metrics', method: 'POST' }) .set(config.metricsRateLimiting.clientMetricsMaxPerMinute); rateLimits - .labels('/api/client/register', 'POST') + .labels({ + endpoint: '/api/client/register', + method: 'POST', + }) .set(config.metricsRateLimiting.clientRegisterMaxPerMinute); rateLimits - .labels('/api/frontend/metrics', 'POST') + .labels({ + endpoint: '/api/frontend/metrics', + method: 'POST', + }) .set( config.metricsRateLimiting.frontendMetricsMaxPerMinute, ); rateLimits - .labels('/api/frontend/register', 'POST') + .labels({ + endpoint: '/api/frontend/register', + method: 'POST', + }) .set( config.metricsRateLimiting.frontendRegisterMaxPerMinute, ); rateLimits - .labels('/api/admin/user-admin', 'POST') + .labels({ + endpoint: '/api/admin/user-admin', + method: 'POST', + }) .set(config.rateLimiting.createUserMaxPerMinute); rateLimits - .labels('/auth/simple', 'POST') + .labels({ endpoint: '/auth/simple', method: 'POST' }) .set(config.rateLimiting.simpleLoginMaxPerMinute); } catch (e) {} }