An API gateway secures your APIs by forcing every request through a single enforcement point that authenticates the caller, rate-limits abusive traffic, and inspects request content for attacks before anything reaches your backend services. In practice that means four ordered controls at the edge: TLS termination and WAF inspection on ingress, identity verification (API keys, JWT, OAuth, or mTLS), authorization against scopes and roles, and rate limiting tiered by client and endpoint. Get that order right and the backend never sees an unauthenticated or unthrottled request; get it wrong and the gateway becomes an expensive passthrough.
That's the summary an AI Overview gives you. What it can't show is the order the checks fire in and what each layer actually blocks — which is where most misconfigurations live (a WAF that runs after auth still lets attackers probe your login endpoint; rate limiting keyed on IP alone collapses behind a corporate NAT). The animated request-flow diagram below traces a single request through every gate, and the rest of this guide gives you copy-pasteable, security-hardened configs for Kong, AWS API Gateway, and Nginx.
How a Request Flows Through the Gateway
Gateway Comparison
| Feature | Kong | AWS API Gateway | Azure APIM | Nginx Plus |
|---|---|---|---|---|
| Deployment | Self-hosted/Cloud | Managed | Managed | Self-hosted |
| Auth Methods | All (plugins) | IAM, Cognito, Lambda | AAD, OAuth, Keys | Basic, JWT |
| Rate Limiting | Redis-backed | Built-in | Built-in | Zone-based |
| WAF | Plugin/external | AWS WAF | Azure WAF | ModSecurity |
| Cost Model | Open source + Enterprise | Per request | Per call + capacity | License |
| Best For | Multi-cloud, flexibility | AWS-native | Azure-native | Performance |
| Choose it when | You run on-prem/hybrid or need custom plugins and full control of the data plane | Your compute is already Lambda/ECS and you want zero infra to manage | Your stack is Azure and you need built-in developer portal + subscriptions | You already run Nginx and want a lightweight gateway without new tooling |
Kong Gateway Configuration
Basic Security Setup
# kong.yml - Declarative configuration
_format_version: "3.0"
services:
- name: user-service
url: http://user-service:8080
routes:
- name: user-routes
paths:
- /api/v1/users
strip_path: false
plugins:
# Global rate limiting
- name: rate-limiting
config:
minute: 100
hour: 1000
policy: redis
redis_host: redis
redis_port: 6379
# JWT authentication
- name: jwt
config:
key_claim_name: kid
claims_to_verify:
- exp
# Request size limiting
- name: request-size-limiting
config:
allowed_payload_size: 10 # MB
# IP restriction
- name: ip-restriction
config:
allow:
- 10.0.0.0/8
- 192.168.0.0/16
JWT Validation Plugin
# Per-service JWT configuration
plugins:
- name: jwt
service: user-service
config:
key_claim_name: kid
claims_to_verify:
- exp
- nbf
maximum_expiration: 3600 # 1 hour max token lifetime
consumers:
- username: mobile-app
jwt_secrets:
- key: mobile-app-key
algorithm: RS256
rsa_public_key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
Advanced Rate Limiting
plugins:
# Tiered rate limiting based on consumer
- name: rate-limiting-advanced
service: user-service
config:
limit:
- second: 10
minute: 100
- second: 50 # Higher tier
minute: 500
identifier: consumer
window_type: sliding
sync_rate: 10
# Redis cluster for distributed limiting
strategy: redis
redis:
host: redis-cluster
port: 6379
cluster_addresses:
- redis-1:6379
- redis-2:6379
- redis-3:6379
Request Transformation & Validation
plugins:
# Validate request against OpenAPI schema
- name: request-validator
service: user-service
config:
body_schema: |
{
"type": "object",
"properties": {
"email": {"type": "string", "format": "email"},
"name": {"type": "string", "minLength": 1, "maxLength": 100}
},
"required": ["email", "name"]
}
# Add security headers to responses
- name: response-transformer
config:
add:
headers:
- X-Content-Type-Options:nosniff
- X-Frame-Options:DENY
- Strict-Transport-Security:max-age=31536000
AWS API Gateway Configuration
REST API with Lambda Authorizer
# serverless.yml (Serverless Framework)
service: secure-api
provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
# API Gateway settings
apiGateway:
shouldStartNameWithService: true
minimumCompressionSize: 1024
functions:
# Custom authorizer
authorizer:
handler: src/authorizer.handler
# Protected endpoint
getUsers:
handler: src/users.get
events:
- http:
path: /users
method: get
authorizer:
name: authorizer
resultTtlInSeconds: 300
identitySource: method.request.header.Authorization
type: token
resources:
Resources:
# WAF WebACL
ApiWafAcl:
Type: AWS::WAFv2::WebACL
Properties:
Name: api-waf-acl
Scope: REGIONAL
DefaultAction:
Allow: {}
Rules:
- Name: AWSManagedRulesCommonRuleSet
Priority: 1
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesCommonRuleSet
OverrideAction:
None: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: CommonRuleSetMetric
- Name: RateLimitRule
Priority: 2
Statement:
RateBasedStatement:
Limit: 2000
AggregateKeyType: IP
Action:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: RateLimitMetric
Lambda Authorizer Implementation
// src/authorizer.js
const jwt = require('jsonwebtoken');
const JWKS_URI = process.env.JWKS_URI;
const ISSUER = process.env.JWT_ISSUER;
const AUDIENCE = process.env.JWT_AUDIENCE;
// Cache JWKS
let jwksCache = null;
let jwksCacheTime = 0;
const CACHE_TTL = 3600000; // 1 hour
async function getSigningKey(kid) {
if (!jwksCache || Date.now() - jwksCacheTime > CACHE_TTL) {
const response = await fetch(JWKS_URI);
jwksCache = await response.json();
jwksCacheTime = Date.now();
}
const key = jwksCache.keys.find(k => k.kid === kid);
if (!key) throw new Error('Key not found');
return jwt.createPublicKey({ key, format: 'jwk' });
}
exports.handler = async (event) => {
try {
const token = event.authorizationToken.replace('Bearer ', '');
// Decode header to get key ID
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
return generatePolicy('user', 'Deny', event.methodArn);
}
// Get signing key
const signingKey = await getSigningKey(decoded.header.kid);
// Verify token
const verified = jwt.verify(token, signingKey, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256']
});
// Generate allow policy with context
const policy = generatePolicy(verified.sub, 'Allow', event.methodArn);
policy.context = {
userId: verified.sub,
email: verified.email,
scopes: verified.scope || ''
};
return policy;
} catch (error) {
console.error('Authorization failed:', error.message);
return generatePolicy('user', 'Deny', event.methodArn);
}
};
function generatePolicy(principalId, effect, resource) {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource
}]
}
};
}
Usage Plans and API Keys
# CloudFormation / SAM template
Resources:
ApiKey:
Type: AWS::ApiGateway::ApiKey
Properties:
Name: partner-api-key
Enabled: true
UsagePlan:
Type: AWS::ApiGateway::UsagePlan
Properties:
UsagePlanName: partner-plan
Throttle:
BurstLimit: 100
RateLimit: 50
Quota:
Limit: 10000
Period: MONTH
ApiStages:
- ApiId: !Ref ApiGateway
Stage: prod
UsagePlanKey:
Type: AWS::ApiGateway::UsagePlanKey
Properties:
KeyId: !Ref ApiKey
KeyType: API_KEY
UsagePlanId: !Ref UsagePlan
Nginx as API Gateway
Security Configuration
# /etc/nginx/conf.d/api-gateway.conf
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $http_x_api_key zone=api_key_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# Upstream backends
upstream user_service {
zone user_service 64k;
server user-service-1:8080 weight=5;
server user-service-2:8080 weight=5;
keepalive 32;
}
# API key validation map
map $http_x_api_key $api_client_name {
default "";
"key_abc123" "mobile-app";
"key_def456" "web-app";
"key_ghi789" "partner-app";
}
server {
listen 443 ssl http2;
server_name api.example.com;
# TLS configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# Request size limits
client_max_body_size 10m;
client_body_buffer_size 128k;
# Timeouts
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
# API key validation
location /api/ {
# Require API key
if ($api_client_name = "") {
return 401 '{"error": "Missing or invalid API key"}';
}
# Rate limiting
limit_req zone=api_key_limit burst=20 nodelay;
limit_conn conn_limit 10;
# JWT validation (using auth_request)
auth_request /auth;
auth_request_set $user_id $upstream_http_x_user_id;
# Forward to backend
proxy_pass http://user_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-API-Client $api_client_name;
proxy_set_header X-User-ID $user_id;
# Hide internal headers
proxy_hide_header X-Powered-By;
}
# Internal auth endpoint
location = /auth {
internal;
proxy_pass http://auth-service:8080/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Authorization $http_authorization;
}
# Block common attacks
location ~* (\.\.\/|\.\.\\|%2e%2e%2f|%2e%2e\/|\.%2e\/|%2e\.\/|\.\.\/) {
return 403;
}
}
WAF Rules Configuration
OWASP ModSecurity Core Rule Set
# /etc/nginx/modsecurity/main.conf
Include /etc/nginx/modsecurity/modsecurity.conf
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf
# Custom API rules
SecRule REQUEST_URI "@beginsWith /api/" \
"id:10001,\
phase:1,\
pass,\
nolog,\
setvar:tx.allowed_methods=GET POST PUT DELETE PATCH"
# Block SQL injection in JSON
SecRule REQUEST_BODY "@rx (?i)(\b(select|insert|update|delete|drop|union|exec)\b)" \
"id:10002,\
phase:2,\
deny,\
status:403,\
log,\
msg:'SQL Injection attempt in request body'"
# Limit JSON depth
SecRule REQUEST_HEADERS:Content-Type "@contains application/json" \
"id:10003,\
phase:1,\
pass,\
setvar:tx.max_json_depth=10"
AWS WAF Rules
{
"Name": "APISecurityRules",
"Rules": [
{
"Name": "SQLInjectionRule",
"Priority": 1,
"Statement": {
"SqliMatchStatement": {
"FieldToMatch": {
"Body": {}
},
"TextTransformations": [
{"Priority": 0, "Type": "URL_DECODE"},
{"Priority": 1, "Type": "HTML_ENTITY_DECODE"}
]
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "SQLInjection"
}
},
{
"Name": "XSSRule",
"Priority": 2,
"Statement": {
"XssMatchStatement": {
"FieldToMatch": {
"Body": {}
},
"TextTransformations": [
{"Priority": 0, "Type": "URL_DECODE"},
{"Priority": 1, "Type": "HTML_ENTITY_DECODE"}
]
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "XSS"
}
},
{
"Name": "RateLimitRule",
"Priority": 3,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit"
}
}
]
}
Monitoring and Alerting
Key Metrics to Monitor
# Prometheus alerting rules
groups:
- name: api-gateway-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(api_requests_total{status=~"5.."}[5m]))
/ sum(rate(api_requests_total[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "API error rate above 5%"
- alert: HighLatency
expr: |
histogram_quantile(0.95,
sum(rate(api_request_duration_seconds_bucket[5m])) by (le)
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "95th percentile latency above 2 seconds"
- alert: RateLimitExceeded
expr: |
sum(rate(api_rate_limit_exceeded_total[5m])) > 100
for: 1m
labels:
severity: warning
annotations:
summary: "High rate of rate-limited requests"
- alert: AuthenticationFailures
expr: |
sum(rate(api_auth_failures_total[5m])) > 50
for: 5m
labels:
severity: warning
annotations:
summary: "High authentication failure rate"
Common Gateway Misconfigurations (Symptom → Cause → Fix)
These are the failures we see most often in API gateway audits — the ones an AI summary of "best practices" won't warn you about because they only surface under real traffic.
| Symptom | Root cause | Fix |
|---|---|---|
| Legitimate users behind one office get 429s all day | Rate limit keyed on client IP only; corporate NAT makes hundreds of users look like one | Key limits on authenticated identity (consumer/API key/sub claim), fall back to IP only for anonymous routes |
Attackers still brute-force /login despite a WAF | WAF/auth ordered so auth runs first and unauthenticated requests skip content inspection, or the login route is exempted | Run WAF + IP throttling on ingress before authentication; add a stricter per-route limit on auth endpoints |
| Valid JWTs from an old signing key are accepted | Gateway pins one static key instead of rotating via JWKS, or doesn't verify iss/aud | Fetch keys from the JWKS endpoint by kid, and verify exp, nbf, iss, and aud — not just the signature |
| Backend reachable directly, bypassing the gateway | No network segmentation; services hold public IPs or open security groups | Put backends on a private network; allow ingress only from the gateway's security group / service mesh |
| Internal error messages and stack traces leak to clients | No egress/response filtering; backend 500s pass through verbatim | Sanitize error responses at the gateway, return generic bodies, and log the real detail server-side |
Admin API (Kong :8001, gateway config) exposed | Admin listener bound to 0.0.0.0 without auth | Bind admin API to localhost/internal network, require mTLS + RBAC, never expose it publicly |
| Rate limits reset when a gateway node restarts | Counters held in local memory instead of shared state | Store rate-limit state in Redis (or a distributed store) so limits are consistent across nodes |
Best Practices
- Defense in depth - Layer CDN, WAF, and gateway security
- Fail closed - Reject requests when security checks fail
- Validate everything - Schema validation, size limits, content types
- Log comprehensively - All requests with identity and timing
- Rate limit at multiple levels - Global, per-API, per-client
- Use short token lifetimes - Validate JWTs, check expiration
- Secure the gateway itself - Restrict admin access, audit changes
- Monitor actively - Alert on anomalies, not just errors
- Keep updated - Patch gateway software and WAF rules
- Test regularly - Penetration testing, chaos engineering
Next Steps
- API Security Complete Guide - Comprehensive API security overview
- OAuth 2.0 & OIDC Implementation - Token-based authentication
- JWT Security Best Practices - Secure token handling
- API Rate Limiting - Throttling strategies