fix(#338): Add rate limiting to orchestrator API

- Add @nestjs/throttler for rate limiting support
- Configure multiple throttle profiles: default (100/min), strict (10/min for spawn/kill), status (200/min for polling)
- Apply strict rate limits to spawn and kill endpoints to prevent DoS
- Apply higher rate limits to status/health endpoints for monitoring
- Add OrchestratorThrottlerGuard with X-Forwarded-For support for proxy setups
- Add unit tests for throttler guard

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 18:26:50 -06:00
parent 3f16bbeca1
commit ce7fb27c46
8 changed files with 244 additions and 4 deletions

View File

@@ -12,21 +12,28 @@ import {
HttpCode,
UseGuards,
} from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import { QueueService } from "../../queue/queue.service";
import { AgentSpawnerService } from "../../spawner/agent-spawner.service";
import { AgentLifecycleService } from "../../spawner/agent-lifecycle.service";
import { KillswitchService } from "../../killswitch/killswitch.service";
import { SpawnAgentDto, SpawnAgentResponseDto } from "./dto/spawn-agent.dto";
import { OrchestratorApiKeyGuard } from "../../common/guards/api-key.guard";
import { OrchestratorThrottlerGuard } from "../../common/guards/throttler.guard";
/**
* Controller for agent management endpoints
*
* All endpoints require API key authentication via X-API-Key header.
* Set ORCHESTRATOR_API_KEY environment variable to configure the expected key.
*
* Rate limits:
* - Status endpoints: 200 requests/minute
* - Spawn/kill endpoints: 10 requests/minute (strict)
* - Default: 100 requests/minute
*/
@Controller("agents")
@UseGuards(OrchestratorApiKeyGuard)
@UseGuards(OrchestratorApiKeyGuard, OrchestratorThrottlerGuard)
export class AgentsController {
private readonly logger = new Logger(AgentsController.name);
@@ -43,6 +50,7 @@ export class AgentsController {
* @returns Agent spawn response with agentId and status
*/
@Post("spawn")
@Throttle({ strict: { limit: 10, ttl: 60000 } })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async spawn(@Body() dto: SpawnAgentDto): Promise<SpawnAgentResponseDto> {
this.logger.log(`Received spawn request for task: ${dto.taskId}`);
@@ -81,6 +89,7 @@ export class AgentsController {
* @returns Array of all agent sessions with their status
*/
@Get()
@Throttle({ status: { limit: 200, ttl: 60000 } })
listAgents(): {
agentId: string;
taskId: string;
@@ -123,6 +132,7 @@ export class AgentsController {
* @returns Agent status details
*/
@Get(":agentId/status")
@Throttle({ status: { limit: 200, ttl: 60000 } })
async getAgentStatus(@Param("agentId") agentId: string): Promise<{
agentId: string;
taskId: string;
@@ -181,6 +191,7 @@ export class AgentsController {
* @returns Success message
*/
@Post(":agentId/kill")
@Throttle({ strict: { limit: 10, ttl: 60000 } })
@HttpCode(200)
async killAgent(@Param("agentId") agentId: string): Promise<{ message: string }> {
this.logger.warn(`Received kill request for agent: ${agentId}`);
@@ -204,6 +215,7 @@ export class AgentsController {
* @returns Summary of kill operation
*/
@Post("kill-all")
@Throttle({ strict: { limit: 10, ttl: 60000 } })
@HttpCode(200)
async killAllAgents(): Promise<{
message: string;

View File

@@ -1,12 +1,22 @@
import { Controller, Get } from "@nestjs/common";
import { Controller, Get, UseGuards } from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import { HealthService } from "./health.service";
import { OrchestratorThrottlerGuard } from "../../common/guards/throttler.guard";
/**
* Health check controller for orchestrator service
*
* Rate limits:
* - Health endpoints: 200 requests/minute (higher for monitoring)
*/
@Controller("health")
@UseGuards(OrchestratorThrottlerGuard)
export class HealthController {
constructor(private readonly healthService: HealthService) {}
@Get()
check() {
@Throttle({ status: { limit: 200, ttl: 60000 } })
check(): { status: string; uptime: number; timestamp: string } {
return {
status: "healthy",
uptime: this.healthService.getUptime(),
@@ -15,7 +25,8 @@ export class HealthController {
}
@Get("ready")
ready() {
@Throttle({ status: { limit: 200, ttl: 60000 } })
ready(): { ready: boolean } {
// NOTE: Check Valkey connection, Docker daemon (see issue #TBD)
return { ready: true };
}

View File

@@ -1,7 +1,9 @@
import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller";
import { HealthService } from "./health.service";
@Module({
controllers: [HealthController],
providers: [HealthService],
})
export class HealthModule {}