API Gateway + WAF: Rate-Limit Public Coding Agent Endpoints

0 views

Ship a public coding agent behind API Gateway and the internet will find it: credential stuffing on API keys, prompt floods that burn Bedrock tokens, WebSocket clients that open thousands of idle sessions, and tool-abuse loops that fill Redis with scratch keys. Usage plans alone are not enough. Pair API Gateway throttling with AWS WAF rate-based rules and Bot Control so abuse dies at the edge before it becomes a Lambda concurrency incident.

⚡ TL;DR: Put public agent HTTP/WebSocket APIs behind API Gateway usage plans (key + stage throttle) and WAF rate-based rules / Bot Control. Protect against prompt-flood and tool-abuse; combine with API Gateway WebSockets for multi-turn agents, AppConfig kill switches, and IAM condition keys. Edge limits first; app limits second.

Threat model for public agents

Abuse Symptom Edge control
Prompt flood Bedrock $ spike, EMF tokens skyrocket WAF rate rule + usage plan burst
Tool-abuse loop Redis/Dynamo write storm Per-key throttle + app Cedar deny
WebSocket fan-out Idle connections, fan-out cost Route-level throttle + connection caps
Scrapers / bots Noisy neighbor WAF Bot Control
Key sharing One key, many IPs Usage plan quota + anomaly alerts
// ❌ Public Function URL, no WAF, no usage plan
// Anyone who finds the URL can Invoke until account limits scream

✅ API Gateway (REST or HTTP API) + WAF WebACL + keys/JWT + app-level authz.

Usage plans and stage throttles

For REST APIs, usage plans attach API keys to throttle and quota. For HTTP APIs, use stage burst/rate limits plus JWT authorizers (keys are REST-centric).

// Infra sketch — throttle per key (CDK-ish pseudocode)
// UsagePlan: quota 10_000 requests/day, rate 20/s, burst 40
// Method /v1/agent/turn: API key required
// Method $connect (WebSocket): throttle 5/s per stage + custom authorizer

import {
  APIGatewayClient,
  CreateUsagePlanCommand,
  CreateApiKeyCommand,
  CreateUsagePlanKeyCommand,
} from "@aws-sdk/client-api-gateway";

const gw = new APIGatewayClient({});

export async function provisionTenantKey(tenantId: string) {
  const key = await gw.send(
    new CreateApiKeyCommand({
      name: `agent-${tenantId}`,
      enabled: true,
      tags: { tenant: tenantId },
    })
  );
  // attach to plan with rateLimit=20, burstLimit=40, quota=10000/DAY
  await gw.send(
    new CreateUsagePlanKeyCommand({
      usagePlanId: process.env.USAGE_PLAN_ID!,
      keyId: key.id!,
      keyType: "API_KEY",
    })
  );
  return key.value!;
}

Set stricter limits on expensive routes (POST /turn, POST /tool) than on health checks. For WebSockets, throttle $connect hard — reconnect storms are a classic outage.

WAF rate-based rules and Bot Control

Attach a regional WebACL to the API Gateway stage.

{
  "Name": "agent-public-edge",
  "Rules": [
    {
      "Name": "rate-limit-by-ip",
      "Priority": 10,
      "Action": { "Block": {} },
      "Statement": {
        "RateBasedStatement": {
          "Limit": 200,
          "AggregateKeyType": "IP",
          "ScopeDownStatement": {
            "ByteMatchStatement": {
              "SearchString": "/v1/agent",
              "FieldToMatch": { "UriPath": {} },
              "PositionalConstraint": "STARTS_WITH",
              "TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }]
            }
          }
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "agentRateByIp"
      }
    },
    {
      "Name": "bot-control-common",
      "Priority": 20,
      "OverrideAction": { "None": {} },
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesBotControlRuleSet",
          "ManagedRuleGroupConfigs": [
            { "AWSManagedRulesBotControlRuleSet": { "InspectionLevel": "COMMON" } }
          ]
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "agentBotControl"
      }
    }
  ]
}

✅ Rate-based rules catch distributed floods that still use valid API keys from many IPs. Usage plans catch a single key going vertical. You want both.

❌ Relying only on Lambda reserved concurrency: abuse still costs you the WAF/API Gateway path and fills concurrency that legitimate tenants need. Kill switches in AppConfig help mid-incident but are not a rate limiter.

WebSocket-specific protections

From API Gateway WebSockets for multi-turn coding agents:

  • Authorizer on $connect (JWT / API key mapping)
  • Cap connections per tenant in DynamoDB (conditional put)
  • Idle timeout + DynamoDB TTL session expiry hooks
  • Per-message size limits; reject huge prompts at the gateway when possible
// ✅ Connection cap per tenant on $connect
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});

export async function onConnect(event: {
  requestContext: { connectionId: string; authorizer?: { tenantId?: string } };
}) {
  const tenantId = event.requestContext.authorizer?.tenantId;
  if (!tenantId) return { statusCode: 401, body: "unauthorized" };

  try {
    await ddb.send(
      new PutItemCommand({
        TableName: "agent-connections",
        Item: {
          pk: { S: `tenant#${tenantId}` },
          sk: { S: `conn#${event.requestContext.connectionId}` },
          ttl: { N: String(Math.floor(Date.now() / 1000) + 7200) },
        },
        // optional: maintain a counter item with ConditionExpression limit
      })
    );
  } catch {
    return { statusCode: 429, body: "too_many_connections" };
  }
  return { statusCode: 200, body: "ok" };
}

App-layer backstops

Edge rate limits will not understand “this tenant already spent $50 of Bedrock today.” Add:

  • Per-tenant token budgets (EMF + Dynamo counter)
  • Cedar/IsAuthorized for tool abuse (Verified Permissions angle once live)
  • AppConfig kill of expensive tools under incident
  • IAM tags so a runaway agent cannot write outside its prefix

Prompt-flood pattern: attacker sends max-size prompts in a loop. Mitigate with body size limits, WAF size constraints, and model-side max tokens — not only request rate.

Operational checklist

  • [ ] API Gateway in front of all public agent HTTP/WebSocket endpoints (no raw Function URLs)
  • [ ] Usage plan / stage throttle on expensive routes; strict $connect limits
  • [ ] WAF WebACL: rate-based by IP (scoped to /v1/agent) + Bot Control
  • [ ] Connection caps per tenant; TTL cleanup for sockets
  • [ ] Per-tenant Bedrock/token budgets as app backstop
  • [ ] Alarms: 429 rates, WAF blocked requests, Bedrock spend anomalies
  • [ ] Runbook: AppConfig kill switches + tighten WAF limit without redeploying Lambdas

Related reading

Last updated on September 20, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.