Last reviewed 14 min read

Section 08

API & Microservices Architecture

Microservices architectures introduce new security challenges: more network communication, more attack surface, and complex trust relationships. Securing APIs becomes critical.

API & Microservices Request Flow

Gateway authN, service-to-service mTLS, and tenant-aware authorization with row-level isolation.

Scroll sideways to see the whole diagram

API Gateway Security

The API Gateway is your first line of defense. It centralizes security controls before requests reach backend services.

The gateway is attractive because it is the one place an organization can enforce authentication, authorization, and traffic policy without asking every service team to reimplement them. It is also a single point of failure: a misconfigured route, an over-broad OAuth scope, or a JWT check that trusts the alg header can expose every backend at once. Treat gateway configuration as production code — reviewed, versioned, and tested — and use the OWASP API Security Top 10 (2023) to decide what each plugin must enforce rather than enabling safeguards ad hoc.

Authentication

  • • OAuth 2.0 / OpenID Connect
  • • API key validation
  • • JWT verification
  • • mTLS for B2B APIs

Traffic Management

  • • Rate limiting per client
  • • Request throttling
  • • Quota enforcement
  • • DDoS protection

Request Validation

  • • Schema validation (OpenAPI)
  • • Input sanitization
  • • Request size limits
  • • Content-Type enforcement

Observability

  • • Request/response logging
  • • Metrics and tracing
  • • Anomaly detection
  • • Audit trails

Service-to-Service Authentication

Once traffic is inside the perimeter, every call between services still needs a verifiable identity — network location is no longer an authorization signal. The pattern is a workload identity (an mTLS SPIFFE identity, a Kubernetes service account, or a cloud managed identity) exchanged for a short-lived credential the receiving service validates. The common failure is long-lived static API keys shared between services: they cannot be scoped to a single caller, do not rotate, and survive in config files and CI variables long after the service they belong to is decommissioned. Prefer credentials that expire in minutes over secrets that live for years, and verify tokens at the receiving service rather than trusting anything the caller asserts.

Mutual TLS (mTLS)

Both client and server present certificates. Provides authentication and encryption.

  • • Typically managed by service mesh
  • • Automatic certificate rotation
  • • No application code changes needed

Service Account Tokens

Short-lived tokens issued to services (Kubernetes service accounts, Azure Managed Identities).

  • • Workload identity in cloud environments
  • • No static credentials to manage
  • • Automatic token refresh

JWT Service Tokens

Services authenticate with signed JWTs containing claims about identity and permissions.

  • • Signed by trusted issuer
  • • Contains scope/permissions claims
  • • Verify signature and claims at each service

Multi-Tenancy & Tenancy Boundaries

A multi-tenant service runs one deployment for many customers, so every query, cache key, queue message, and rate-limit bucket carries an implicit "which tenant?" question. Tenancy boundaries are the enforcement of that question. Vendor guidance (AWS SaaS Lens, Azure Architecture Center) describes three isolation models, and most real systems combine them:

Silo

Per-tenant infrastructure — separate databases, namespaces, or accounts. Strongest isolation and the easiest to evidence in an audit; costs the most to operate and can hide cross-tenant trends.

Pool

Tenants share infrastructure with a tenant_id discriminator on every row and record. Lowest cost and highest density; isolation now depends entirely on correct, mandatory filtering.

Bridge

Pool by default, with the option to place a large or regulated tenant on dedicated resources. Balances cost and isolation, at the price of operating both models.

Authorization must be tenant-aware. A token should carry the tenant claim (tenant_id, org_id, or an issuer that maps to one), and every access check should be a (subject, tenant, resource) decision rather than a (subject, resource) one. Never trust a tenant identifier supplied in the request body or URL — derive it from the authenticated token and reject requests whose path or body names a different tenant. A user who is an administrator in tenant A must be a non-member in tenant B.

Data partitioning follows from the model. In a pool, enforce row-level security in the database (PostgreSQL RLS, or an equivalent policy engine) so a missing tenant_id predicate in application code is caught at the storage layer instead of silently returning another tenant's rows. Set the tenant context per connection or transaction — not per query — and make the default deny when no context is set. Cache keys, object-storage prefixes, search indexes, and background jobs need the same discriminator: a shared cache key or a queued message without a tenant is a boundary that does not exist.

Rate limits and quotas are per tenant, not per deployment. Cap requests, storage, and concurrent jobs against the tenant claim so one noisy tenant cannot exhaust shared capacity, and apply the limit after authentication so an unauthenticated caller cannot spend a tenant's bucket. The classic failure mode is cross-tenant data exposure: an object reference such as /invoices/1042 is authorized against the caller's identity but not the invoice's tenant, so changing the id returns another customer's data — OWASP API Security API1 (Broken Object Level Authorization). Test for it explicitly: for every resource endpoint, authenticate as tenant A, request tenant B's identifiers, and confirm each returns 404 or 403.

OWASP API Security Top 10 (2023)

ID Risk Mitigation
API1 Broken Object Level Authorization Check ownership at resource level, not just endpoint
API2 Broken Authentication OAuth 2.0 + PKCE, rotate tokens, rate-limit auth endpoints
API3 Broken Object Property Level Authorization Return only allowed fields per role, never mass-assign
API4 Unrestricted Resource Consumption Rate limit, pagination, max payload size, timeout
API5 Broken Function Level Authorization Consistent authz middleware, deny by default
API6 Unrestricted Access to Sensitive Business Flows CAPTCHA, device fingerprinting, anti-automation
API7 Server Side Request Forgery URL allowlists, block internal ranges, validate schemes
API8 Security Misconfiguration Hardened defaults, automated config scanning
API9 Improper Inventory Management API catalog, deprecation policy, version sunset dates
API10 Unsafe Consumption of APIs Validate 3rd-party responses, timeout, circuit break

Rate Limiting Strategies

A rate limit protects shared capacity — it is not only an abuse control. The algorithm you pick decides whether a legitimate client can burst, how much state the limiter holds, and whether the limit holds across multiple gateway nodes. All four common algorithms are expressible with the practical that follows.

Fixed Window

100 requests per minute. Counter resets at minute boundary.

Simple but can allow burst at window edges

Sliding Window

Smooths rate over rolling time period.

Better distribution, more memory needed

Token Bucket

Tokens refill at fixed rate. Each request consumes a token.

Allows controlled bursting

Leaky Bucket

Requests queue and process at constant rate.

Smoothest output, adds latency

Practical: Redis Sliding Window Rate Limiter

rate-limiter.py
"""Sliding window rate limiter using Redis sorted sets."""
import time
import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def is_rate_limited(client_id: str, max_requests: int = 100, window_sec: int = 60) -> bool:
    """Returns True if the client has exceeded their rate limit."""
    key = f"ratelimit:{client_id}"
    now = time.time()
    window_start = now - window_sec

    pipe = r.pipeline()
    # Remove expired entries
    pipe.zremrangebyscore(key, 0, window_start)
    # Add current request
    pipe.zadd(key, {f"{now}": now})
    # Count requests in window
    pipe.zcard(key)
    # Set TTL so keys auto-expire
    pipe.expire(key, window_sec)
    results = pipe.execute()

    request_count = results[2]
    return request_count > max_requests

# Usage in Flask middleware:
@app.before_request
def check_rate_limit():
    # Key on the authenticated principal/tenant established by auth middleware.
    # Never key on a client-supplied header like X-API-Key: a caller can rotate
    # fake keys to get a fresh bucket and bypass the limit entirely.
    client_id = g.get("principal_id") or g.get("tenant_id")
    if not client_id:
        # Fall back to the client IP only when running behind a trusted reverse
        # proxy that overwrites X-Forwarded-For (otherwise the IP is spoofable too).
        client_id = request.remote_addr
    if is_rate_limited(client_id, max_requests=100, window_sec=60):
        return jsonify({"error": "rate_limit_exceeded"}), 429

Practical: Kong API Gateway Security Config

kong-security.yaml
# Kong declarative config — security plugins (Kong Gateway OSS)
_format_version: "3.0"

services:
  - name: backend-api
    url: http://api-server:8080
    routes:
      - name: api-route
        paths: ["/api/v1"]
        strip_path: false
    plugins:
      # JWT authentication. Kong's JWT plugin does NOT resolve JWKS by "kid":
      # it matches a Consumer by key_claim_name and verifies with that consumer's
      # stored secret/key. Key on a claim that identifies the issuer/consumer
      # (e.g. "iss"), not "kid". For JWKS/kid-aware verification, use a plugin
      # that fetches the JWKS (or validate the token upstream).
      - name: jwt
        config:
          claims_to_verify: ["exp"]
          key_claim_name: "iss"
          secret_is_base64: false

      # Rate limiting - the OSS rate-limiting plugin runs a fixed window per node.
      # Sliding-window limiting shared across nodes needs rate-limiting-advanced,
      # which ships with Kong Enterprise / Konnect (plus a Redis datastore).
      - name: rate-limiting
        config:
          minute: 100
          policy: local

      # Request size limiting (prevent DoS)
      - name: request-size-limiting
        config:
          allowed_payload_size: 1  # 1 MB max

      # CORS
      - name: cors
        config:
          origins: ["https://app.example.com"]
          methods: ["GET", "POST", "PUT", "DELETE"]
          headers: ["Authorization", "Content-Type"]
          max_age: 3600

  - name: admin-api
    url: http://admin-server:8081
    routes:
      - name: admin-route
        paths: ["/admin"]
    plugins:
      # IP restriction for the admin route
      - name: ip-restriction
        config:
          allow: ["10.0.0.0/8"]
          deny: []

API Security Tools

Category Tool Purpose
Gateway Kong, Envoy, APISIX Centralized auth, rate limiting, routing
Testing 42Crunch, OWASP ZAP, Burp API security scanning, fuzzing
Governance Spectral, Optic, Redocly OpenAPI linting, breaking change detection
Discovery Treblle, Akto, Salt Security Shadow API detection, inventory

Circuit Breaker Pattern

Circuit breakers prevent cascade failures by stopping calls to failing services.

A breaker is a state machine wrapped around a remote call, and it needs a failure threshold, a timeout, and a recovery probe to be useful. Without them it either never trips — and the cascade continues — or it trips on a single transient error and stays open. The subtle failure is choosing the wrong granularity: one breaker shared by every operation on a dependency will open for all of them when a single endpoint degrades, while a breaker per call site multiplies the state you must reason about. Pair breakers with timeouts, bounded retries with jitter, and a fallback that degrades gracefully instead of failing the whole request.

CLOSED

Normal operation. Requests pass through. Failures counted.

OPEN

Failure threshold exceeded. Requests fail fast. No calls to service.

HALF-OPEN

Testing recovery. Limited requests allowed. Success = close, failure = open.

API Security Checklist

  • ☐ All APIs require authentication
  • ☐ Authorization checked at resource level
  • ☐ Input validated against schema
  • ☐ Rate limiting configured per client tier
  • ☐ Response doesn't leak internal details
  • ☐ HTTPS only, TLS 1.2+
  • ☐ Security headers set (CORS, CSP for browser APIs)
  • ☐ API versioning strategy defined
  • ☐ Deprecation policy for old versions
  • ☐ Documentation doesn't expose vulnerabilities

Framework Alignment

OWASP: API Security Top 10 (2023), ASVS 5.0 (API and web-service controls)
NIST CSF 2.0: PR.AA (Identity & Access Control), PR.DS (Data Security)
ISO 27002:2022: A.8.26 (Application Security Requirements), A.8.28 (Secure Coding)
CIS Controls v8.1: 16 (Application Software Security), 13 (Network Monitoring & Defense)
Related: Security Frameworks →

API & Microservices Labs

Secure the gateway, isolate tenants, and stop cascading failures in a distributed system.

Secure an API Gateway Configuration Custom Lab medium
Inventory every route on a gateway and mark which ones require authenticationEnforce authentication, per-client rate limits, and request size limitsConfirm authorization is enforced at the service, not only at the gatewayAdd schema validation for request bodiesTest one broken object level authorization case: request another tenant's ID and confirm 404 or 403
Tenant Isolation Test Custom Lab hard
Deploy a multi-tenant endpoint backed by a shared datastoreAdd tenant_id row-level security and set the tenant context per transactionAuthenticate as tenant A and request tenant B's records for each endpointConfirm every request returns 404 or 403 and that the attempt is loggedAdd a per-tenant rate limit applied after authentication
Add a Circuit Breaker Custom Lab medium
Wrap one remote call in a breaker with a failure threshold, timeout, and recovery probeSimulate the dependency failing and confirm requests fail fastConfirm the breaker half-opens after the cool-down and closes on successAdd a fallback that degrades gracefully instead of failing the whole request

Evidence to Capture

In a microservice review the finding lives in a route table, an identity binding, or a scoping policy. Quote the entry.

Route inventory: Every gateway route with its authentication plugin, upstream service, and whether it is publicly reachable. The route that has no plugin is the finding, and the config export is the artefact.

Service identity binding: The workload identity, the audience it is issued for, and the policy that maps identity to allowed upstream. A shared client credential removes the ability to attribute a call.

Tenancy scoping: Where the tenant identifier comes from (token claim or request parameter) and the data-layer constraint that enforces it. A filter in application code alone is one missed code path from a cross-tenant read.

Limit configuration: The limiting key, window, burst allowance, and the response code returned on exceed. A per-IP limit in front of an authenticated API limits the attacker who holds one credential not at all.

Endpoint to Top-10 mapping: The affected endpoint, the request and response pair, and the code location that handles it, mapped to the API Top 10 entry it violates.

Likely False Positives

Missing-looking controls that sit at another layer of the request path.

  • No rate limit in the service: Edge limiting at the CDN or gateway is a legitimate placement. Read the route policy before recording the service as unprotected.
  • Tenant id sent in the body: Acceptable where the data layer enforces row-level security against the token claim. Look for the policy, not the parameter.
  • Object authorization in the gateway: A gateway policy on the path identifier is real enforcement. Check it keys on the token subject rather than the path alone.
  • Internal API without per-endpoint limits: Consumer-level quotas on an authenticated internal mesh can be sufficient. The quota and the caller allowlist are the evidence.
  • Circuit breaker returning cached data: A degraded response is by design when the cache holds only public data and the fallback is recorded. Check what the cache can contain.

Review Checklist

Route surface

  • ☐ Export the route table and confirm each route has an authentication plugin and a named upstream.
  • ☐ Check for routes that reach an upstream directly, bypassing the gateway.
  • ☐ Confirm deprecated and internal versions are withdrawn, not only undocumented.
  • ☐ Check the gateway rejects unknown routes rather than forwarding to a default upstream.

Identity and tenancy

  • ☐ Confirm each service presents its own identity and that audiences are per-callee.
  • ☐ Read the data-layer policy for the tenant key and verify it cannot be supplied by the caller.
  • ☐ Check the token carries the tenant claim and that services reject a mismatch between claim and request.
  • ☐ Confirm administrative endpoints require a distinct scope from user-facing ones.

Resilience

  • ☐ Confirm limits key on the authenticated subject or tenant, not only the client address.
  • ☐ Check the exceed path returns a clear status and does not degrade into an unprotected queue.
  • ☐ Read the circuit breaker fallback for what data it can serve and to whom.
  • ☐ Confirm retries and timeouts are bounded so a slow upstream cannot exhaust the pool.

Remediation

One chokepoint, one identity, one scope

Structural

  • • Make the gateway the only ingress to the services, with no route that lacks an authentication plugin, and default-deny unknown paths.
  • • Issue per-service identities with per-callee audiences, so a stolen token is usable against one upstream only.
  • • Push tenant scoping into the data layer as a policy bound to the token claim, and delete the application-side filter as the only defence.

Compensating

  • • Where a shared credential cannot be split yet, add per-caller quotas and log the caller identifier so attribution is possible.
  • • For endpoints that must remain unauthenticated, add a strict per-route limit and a request schema check at the edge.

CWE References: CWE-639