diff --git a/.env.example b/.env.example index 8a805f5a..ff19a71e 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ JWT_SECRET=your-secure-jwt-secret-here # Directory containing docker-compose files COMPOSE_DIR=/path/to/your/compose/files +# Global API rate limit (requests per minute per IP, production only) +API_RATE_LIMIT=100 + # ─── SSO / LDAP Configuration (Admiral) ─────────────────────────── # LDAP / Active Directory diff --git a/CHANGELOG.md b/CHANGELOG.md index 323dd63c..55af1752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* **api:** global rate limiter on all `/api/` routes — 100 requests/min per IP in production (configurable via `API_RATE_LIMIT` env var). Auth endpoints retain their existing stricter limits (5 req/15min) which stack independently. Returns `429 Too Many Requests` when exceeded. + ### Fixed * **security:** enforce `isValidStackName()` on all routes that accept a `stackName` parameter — 11 routes had no validation and 2 used a weaker manual check; all now use the canonical `^[a-zA-Z0-9_-]+$` regex guard, returning `400 Invalid stack name` on rejection diff --git a/backend/src/index.ts b/backend/src/index.ts index 1a87e6d0..d9778fa4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -133,6 +133,21 @@ app.use(cors({ credentials: true, })); +// Global API rate limiter — caps total requests per IP across all /api/ routes. +// Auth-specific limiters (authRateLimiter, ssoRateLimiter) apply additional, +// stricter limits on their respective routes and stack independently. +const globalApiLimiter = rateLimit({ + windowMs: 60 * 1000, + max: process.env.NODE_ENV === 'production' + ? parseInt(process.env.API_RATE_LIMIT || '100', 10) + : 1000, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Too many requests. Please try again shortly.' }, +}); + +app.use('/api/', globalApiLimiter); + // Conditionally parse JSON bodies. Remote proxy requests must NOT have their body // consumed here: express.json() drains the IncomingMessage stream into req.body // and http-proxy then pipes an already-ended stream to the remote server. diff --git a/docs/api-reference/overview.mdx b/docs/api-reference/overview.mdx index 8e44fa1c..d0eae5c5 100644 --- a/docs/api-reference/overview.mdx +++ b/docs/api-reference/overview.mdx @@ -88,7 +88,19 @@ This applies to all stack endpoints: read, write, deploy, down, restart, stop, s ## Rate limiting -Authentication endpoints (`/api/auth/*`) are rate-limited to **5 requests per 15 minutes** in production. API token-authenticated requests are not rate-limited. +All `/api/` endpoints are subject to a global rate limit of **100 requests per minute** per IP address. When exceeded, the server responds with `429 Too Many Requests`: + +```json +{ + "error": "Too many requests. Please try again shortly." +} +``` + +Authentication endpoints (`/api/auth/*`) have an additional, stricter limit of **5 requests per 15 minutes** to prevent brute-force attacks. Both limits apply independently — a login request counts against both the global limit and the auth limit. + +The global rate limit is configurable via the `API_RATE_LIMIT` environment variable (requests per minute, default `100`). + +Standard rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`) are included in all API responses. ## License tier requirements