Using Attack Trees to Prioritize API Rate‑Limiting Controls After a Recent DDoS Event

Attack‑Tree‑Driven Rate‑Limiting After a DDoS

We just survived a 2026 DDoS that hammered our public REST gateway. A botnet slammed a handful of endpoints with insane request rates, chewing through CPU and memory until real users hit time‑outs. The fallout was a frantic review of our throttling logic. Attack trees turned out to be the perfect tool to decide which endpoints need the tightest controls and how to enforce them without turning our API into a brick wall.

Why Attack Trees?

An attack tree is a diagram that starts with a top‑level goal—say, “Prevent API outage due to traffic overload”—and branches into tactics, techniques, and procedures. Each leaf is a concrete attack step. By scoring nodes for likelihood, impact, and cost, you get a clear, data‑driven way to prioritize defenses.

For API defenses this is handy because:

  • Granularity – You can drill down to specific endpoints, HTTP verbs, or even query strings.
  • Quantification – Scores give you a risk‑adjusted budget for rate limits.
  • Traceability – Every mitigation can be tied back to a threat, easing audits and compliance.

NIST’s CSF even recommends threat modeling as part of the “Identify” function, and attack trees fit that perfectly.

Building the Tree

  1. Root Goal – “Prevent API outage due to traffic overload.”
  2. Major Vectors –
    • Botnet‑driven request floods
    • Credential‑stressed brute‑force
    • Denial‑of‑service via resource‑heavy endpoints
  3. Sub‑Goals – For the flood vector, examples:
    • “Send >10 k req/s to /orders/create”
    • “Send >5 k req/s to /auth/login”
  4. Scores –
    • Likelihood: from logs & threat intel
    • Impact: SLA breaches & revenue loss
    • Cost: dev effort, infra, user friction
  5. Prioritize – Risk = likelihood × impact; tackle the highest first.

A compact tree for our case looks like this:

Root: API Availability
├─ Flood Botnet
│   ├─ /orders/create >10k req/s  (Risk 9)
│   ├─ /auth/login >5k req/s      (Risk 7)
│   └─ /search?q=* >8k req/s      (Risk 5)
├─ Credential Stuffing
│   ├─ /auth/login >1k req/s      (Risk 6)
│   └─ /reset-password >500 req/s (Risk 4)
└─ Resource Exhaustion
    ├─ /reports/generate >200 req/s (Risk 8)
    └─ /export/data >150 req/s     (Risk 6)

Translating Scores into Rate Limits

Endpoint Current New Target Rationale
/orders/create 100/min 50/min Highest risk; 10 k req/s seen.
/auth/login 200/min 120/min Brute‑force + flood.
/search 500/min 300/min High volume, lower risk.
/reports/generate 50/min 30/min Resource‑heavy, moderate risk.
/export/data 30/min 20/min Similar to reports.

Limits are per IP by default, but we also layered per API key and per user throttles where it made sense. The trade‑off? A modest latency bump for real users during peaks, but the chance of a full outage is gone.

Implementation Snippets

NGINX Rate Limiting

# Shared memory zone
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/m;

# Specific location
location /orders/create {
    limit_req zone=api burst=10 nodelay;
    proxy_pass http://backend;
}

Envoy Filters

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: api-rate-limit
spec:
  workloadSelector:
    labels:
      app: api-gateway
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.filters.http.ratelimit
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
          domain: api
          failure_mode_deny: true
          rate_limit_service:
            grpc_service:
              envoy_grpc:
                cluster_name: ratelimit_cluster

Redis‑Backed Token Bucket (Node.js)

const Redis = require('ioredis');
const redis = new Redis();

async function allowRequest(key, limit, windowMs) {
  const now = Date.now();
  const windowStart = now - windowMs;
  const count = await redis.zcount(key, windowStart, now);
  if (count >= limit) return false;
  await redis.zadd(key, now, now);
  await redis.expire(key, Math.ceil(windowMs / 1000));
  return true;
}

Practical Caveats

Issue Mitigation
False positives – Legit high‑volume clients hit limits. Whitelists per API key/IP and a dynamic admin UI to tweak thresholds.
Distributed attacks – Botnet spans many IPs. Combine IP limits with user‑agent, API key checks, or client‑cert throttling for internal services.
Latency spikes – Burst handling can cause jitter. Tune burst parameters; only enable nodelay on critical endpoints.
Monitoring blind spots – Logs may miss abuse patterns. Instrument with Prometheus; alert on sudden 429 spikes.
Compliance – Need audit trails of throttling decisions. Log decisions to a tamper‑evident store (e.g., write‑once storage).

Post‑Implementation Review

After rollout:

  • /orders/create saw a 30 % drop in peak traffic.
  • 429 responses jumped from 0.1 % to 2 %, but no 5xx errors appeared.
  • Support tickets fell 15 % and no complaints surfaced.

We also ran a staged re‑attack with a controlled botnet; the new limits held and the backend stayed responsive.

Lessons Learned

  1. Attack trees give you numbers – No more guessing which endpoint to throttle; you base it on measurable risk scores.
  2. Rate limits aren’t one‑size‑fits‑all – A blanket cap hurts usability; tailor controls per endpoint.
  3. Dynamic adjustment is essential – Threats shift; re‑score the tree weekly and adjust limits.
  4. Documentation matters – Keep the attack tree and mitigation matrix in a shared repo so new folks can see the rationale.

Further Reading

See also