Last reviewed 11 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.

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