Aller au contenu principal

API Gateway Hardening: 10-Step Production Checklist

· 13 minutes de lecture
STOA Team
The STOA Platform Team

Running an API gateway in production requires more than deploying with default settings. An insecure gateway exposes every backend service to attack, leaks sensitive data, and creates compliance nightmares. This 10-step security hardening checklist covers the critical controls you need before production deployment. Each step includes concrete configuration examples and verification commands.

Why Gateway Security Matters

Your API gateway is the single entry point for all external traffic. A misconfigured gateway can expose:

  • Internal services to unauthorized access
  • Customer data to cross-origin leaks
  • Backend systems to denial-of-service attacks
  • Metadata about your infrastructure architecture

The difference between a secure gateway and an exploitable one often comes down to 10 configuration decisions. Let's walk through each one.

Step 1: Enforce TLS Everywhere

Minimum TLS 1.2, prefer TLS 1.3. TLS 1.0 and 1.1 have known vulnerabilities and are deprecated by major browsers.

Configuration Example (nginx)

server {
listen 443 ssl http2;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;

# HSTS: force HTTPS for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Redirect HTTP to HTTPS
error_page 497 https://$host$request_uri;
}

For Kubernetes Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway
annotations:
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
nginx.ingress.kubernetes.io/hsts: "true"
nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
spec:
tls:
- hosts:
- api.example.com
secretName: api-tls-secret

Verification

# Test TLS version enforcement
openssl s_client -connect api.example.com:443 -tls1_1 2>&1 | grep -i "handshake failure"

# Should show: handshake failure (TLS 1.1 rejected)

# Check HSTS header
curl -I https://api.example.com | grep -i "strict-transport-security"

# Should show: Strict-Transport-Security: max-age=31536000

Step 2: Configure CORS Properly

Never use Access-Control-Allow-Origin: * in production. Wildcard CORS allows any website to make authenticated requests to your API, bypassing the browser's same-origin policy.

Bad (insecure)

cors:
allowOrigins: ["*"] # Allows ANY website to call your API
allowCredentials: true # Combined with *, this is a critical vulnerability

Good (explicit allowlist)

cors:
allowOrigins:
- https://app.example.com
- https://admin.example.com
allowMethods: ["GET", "POST", "PUT", "DELETE"]
allowHeaders: ["Authorization", "Content-Type"]
exposeHeaders: ["X-Request-ID"]
allowCredentials: true
maxAge: 3600

STOA Example

apiVersion: gostoa.dev/v1alpha1
kind: Tool
metadata:
name: customer-api
spec:
endpoint: https://backend.example.com/api/customers
cors:
allowOrigins:
- ${PORTAL_URL}
- ${CONSOLE_URL}
allowCredentials: true

Verification

# Test CORS enforcement
curl -H "Origin: https://malicious.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS https://api.example.com/v1/users

# Should NOT include: Access-Control-Allow-Origin: https://malicious.com

# Test allowed origin
curl -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS https://api.example.com/v1/users

# Should include: Access-Control-Allow-Origin: https://app.example.com

Learn more about secure authentication patterns in our Authentication Guide.

Step 3: Set Rate Limits Per Consumer

Rate limiting prevents abuse and DDoS attacks. Always apply limits at both the global and per-consumer level.

Three-Tier Rate Limiting Strategy

# Global: protect infrastructure from total overload
global:
requestsPerSecond: 1000

# Per-endpoint: protect expensive operations
endpoints:
- path: /v1/search
requestsPerMinute: 100
- path: /v1/export
requestsPerHour: 10

# Per-consumer: fair usage
consumers:
- id: free-tier
requestsPerMinute: 60
- id: pro-tier
requestsPerMinute: 600
- id: enterprise-tier
requestsPerMinute: 6000

Kong Example

plugins:
- name: rate-limiting
config:
minute: 60
policy: local
fault_tolerant: true
hide_client_headers: false

STOA Example

apiVersion: gostoa.dev/v1alpha1
kind: Policy
metadata:
name: customer-api-rate-limit
spec:
type: rate_limit
config:
maxRequests: 100
windowSeconds: 60
scope: consumer # Per API key

Verification

# Test rate limit enforcement
for i in {1..65}; do
curl -H "X-API-Key: test-key" https://api.example.com/v1/users
done

# Request 61+ should return: 429 Too Many Requests

Step 4: Block SSRF Vectors

Server-Side Request Forgery (SSRF) allows attackers to probe internal networks. Block private IP ranges in proxy targets.

Blocked IP Ranges (RFC 1918 + special-use)

10.0.0.0/8          # Private network
172.16.0.0/12 # Private network
192.168.0.0/16 # Private network
127.0.0.0/8 # Loopback
169.254.0.0/16 # Link-local
::1/128 # IPv6 loopback
fc00::/7 # IPv6 unique local addresses
fe80::/10 # IPv6 link-local

Implementation (Rust example from STOA Gateway)

fn is_blocked_url(url: &str) -> bool {
let parsed = Url::parse(url).ok()?;
let host = parsed.host_str()?;

// Block IP addresses
if let Ok(ip) = host.parse::<IpAddr>() {
return match ip {
IpAddr::V4(ipv4) => {
ipv4.is_private() ||
ipv4.is_loopback() ||
ipv4.is_link_local() ||
ipv4.octets()[0] == 169 && ipv4.octets()[1] == 254
}
IpAddr::V6(ipv6) => {
ipv6.is_loopback() ||
ipv6.is_unicast_link_local() ||
(ipv6.segments()[0] & 0xfe00) == 0xfc00 // ULA
}
};
}

// Block localhost variations
matches!(host, "localhost" | "127.0.0.1" | "::1")
}

Nginx Example

# Use a resolver that doesn't resolve private IPs
resolver 1.1.1.1 8.8.8.8 valid=300s;

# Deny proxy to private ranges
geo $blocked_backend {
default 0;
10.0.0.0/8 1;
172.16.0.0/12 1;
192.168.0.0/16 1;
127.0.0.0/8 1;
}

if ($blocked_backend) {
return 403 "Blocked: internal IP range";
}

Verification

# Test SSRF protection
curl -X POST https://api.example.com/v1/proxy \
-H "Content-Type: application/json" \
-d '{"target": "http://169.254.169.254/latest/meta-data/"}'

# Should return: 403 Forbidden or 400 Bad Request

Read more about SSRF and API security in API Security Checklist for Solo Developers.

Step 5: Add Security Headers

Security headers protect against XSS, clickjacking, and MIME sniffing attacks. Apply these headers to every response.

Required Headers

# Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;

# Prevent clickjacking
add_header X-Frame-Options "DENY" always;

# Content Security Policy (adjust for your needs)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'" always;

# Referrer policy (hide sensitive URLs)
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions policy (disable unnecessary features)
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

STOA Gateway (Rust middleware)

pub fn security_headers_layer() -> ServiceBuilder<Stack<MapResponseLayer<impl Fn(Response) -> Response + Clone>>> {
ServiceBuilder::new().map_response(|mut response: Response| {
let headers = response.headers_mut();
headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff"));
headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
headers.insert("Referrer-Policy", HeaderValue::from_static("strict-origin-when-cross-origin"));
response
})
}

Verification

curl -I https://api.example.com | grep -E "X-Content-Type-Options|X-Frame-Options|Content-Security-Policy"

# Should show all three headers

Step 6: Enable mTLS for Service-to-Service

Mutual TLS (mTLS) ensures both client and server authenticate each other. Critical for zero-trust architectures.

Certificate Setup

# Generate CA certificate (one-time)
openssl req -x509 -newkey rsa:4096 -keyout ca-key.pem -out ca-cert.pem -days 3650 -nodes

# Generate client certificate
openssl req -newkey rsa:4096 -keyout client-key.pem -out client-csr.pem -nodes
openssl x509 -req -in client-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365

Nginx mTLS Configuration

server {
listen 443 ssl;

ssl_certificate /etc/nginx/certs/server-cert.pem;
ssl_certificate_key /etc/nginx/certs/server-key.pem;

# Require client certificate
ssl_client_certificate /etc/nginx/certs/ca-cert.pem;
ssl_verify_client on;
ssl_verify_depth 2;

location /internal/ {
# Only reachable with valid client cert
proxy_pass http://backend;
}
}

Kubernetes Service Mesh (Istio example)

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: mtls-strict
spec:
mtls:
mode: STRICT # Reject non-mTLS traffic

Learn more about mTLS configuration in our mTLS Configuration Guide.

Step 7: Implement API Key Rotation

API keys eventually leak. Scheduled rotation with grace periods prevents service disruption.

Rotation Strategy

# Dual-key grace period pattern
apiKeys:
- key: key_v2_abc123...
status: active
createdAt: 2026-02-01
expiresAt: 2026-03-01
- key: key_v1_def456...
status: deprecated # Still works but marked for removal
createdAt: 2026-01-01
expiresAt: 2026-02-15 # Grace period

Automated Rotation Script

#!/bin/bash
# rotate-api-keys.sh — run monthly via cron

# Generate new key
NEW_KEY=$(openssl rand -hex 32)

# Store in secrets manager
kubectl create secret generic api-keys \
--from-literal=current="$NEW_KEY" \
--from-literal=previous="$OLD_KEY" \
--dry-run=client -o yaml | kubectl apply -f -

# Notify consumers (14-day grace period)
send-rotation-notice "$NEW_KEY" --grace-period=14d

Consumer Migration

# Test new key before old one expires
curl -H "X-API-Key: key_v2_abc123..." https://api.example.com/v1/health

# Update application config
kubectl set env deployment/app API_KEY=key_v2_abc123...

Read our API Key Rotation Guide for detailed procedures.

Step 8: Set Up Audit Logging

Every request must be logged with an immutable audit trail. Essential for incident response and compliance.

Minimum Log Fields

{
"timestamp": "2026-02-15T14:23:45Z",
"request_id": "req_abc123",
"method": "POST",
"path": "/v1/users",
"consumer_id": "app_xyz789",
"source_ip": "203.0.113.42",
"status_code": 201,
"response_time_ms": 145,
"user_agent": "MyApp/1.2.3",
"bytes_sent": 1024,
"bytes_received": 256
}

Fluent Bit Pipeline (Kubernetes)

apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
data:
fluent-bit.conf: |
[INPUT]
Name tail
Path /var/log/gateway/*.log
Parser json
Tag gateway.access

[FILTER]
Name kubernetes
Match gateway.*
Merge_Log On

[OUTPUT]
Name opensearch
Match gateway.*
Host opensearch.logging.svc
Port 9200
Index gateway-logs
Type _doc

STOA Gateway Audit Middleware

pub async fn audit_middleware(
State(audit_svc): State<Arc<AuditService>>,
request: Request,
next: Next,
) -> Response {
let start = Instant::now();
let method = request.method().clone();
let path = request.uri().path().to_string();

let response = next.run(request).await;

audit_svc.log(AuditEvent {
timestamp: Utc::now(),
method: method.to_string(),
path,
status: response.status().as_u16(),
duration_ms: start.elapsed().as_millis() as u64,
}).await;

response
}

Verification

# Trigger a request
curl -H "X-API-Key: test" https://api.example.com/v1/users

# Check logs (OpenSearch example)
curl -u admin:password https://opensearch.example.com/gateway-logs/_search?q=path:/v1/users

# Should return the logged request

Learn more about observability setup in our Observability Guide.

Step 9: Run Container Security

Non-root users, read-only filesystems, and dropped capabilities reduce attack surface.

Secure Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: gateway
image: ghcr.io/example/api-gateway:v1.2.3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}

Dockerfile Best Practices

# Multi-stage build
FROM rust:1.85 AS builder
WORKDIR /build
COPY . .
RUN cargo build --release

FROM gcr.io/distroless/cc-debian12
COPY --from=builder /build/target/release/gateway /gateway

# Non-root user (distroless default UID 65532)
USER nonroot:nonroot

ENTRYPOINT ["/gateway"]

Verification

# Check pod security context
kubectl get pod api-gateway-xyz -o jsonpath='{.spec.securityContext}'

# Should show: runAsNonRoot: true, runAsUser: 1000

# Check container capabilities
kubectl get pod api-gateway-xyz -o jsonpath='{.spec.containers[0].securityContext.capabilities}'

# Should show: drop: [ALL]

Step 10: Automate Security Scanning

Security is continuous, not one-time. Automate SAST, dependency audits, and container scanning in CI.

GitHub Actions Security Pipeline

name: Security Scan

on: [push, pull_request]

jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Secret scanning
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2

# SAST for Python
- name: Bandit
run: |
pip install bandit
bandit -r src/ -ll -f json -o bandit-report.json

# Dependency audit
- name: pip-audit
run: |
pip install pip-audit
pip-audit --require-hashes --desc

container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Build image
- name: Build
run: docker build -t gateway:test .

# Scan for vulnerabilities
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: gateway:test
severity: CRITICAL,HIGH
exit-code: 1 # Fail CI on findings

Pre-Commit Hooks

# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks

- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ['-ll']

Weekly Dependency Audit (Dependabot alternative)

# .github/workflows/dependency-audit.yml
name: Weekly Dependency Audit

on:
schedule:
- cron: '0 2 * * 1' # Monday 2 AM

jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Rust audit
run: cargo audit

- name: Python audit
run: |
pip install pip-audit safety
pip-audit
safety check

- name: NPM audit
run: npm audit --audit-level=moderate

Verification Checklist

Run this checklist before production deployment:

StepVerification CommandExpected Result
1. TLSopenssl s_client -connect api.example.com:443 -tls1_1Handshake failure
2. CORScurl -H "Origin: https://evil.com" -I https://api.example.comNo CORS headers for evil.com
3. Rate Limit61 requests in 60s with same key429 Too Many Requests
4. SSRFcurl -d '{"target":"http://169.254.169.254"}' https://api.example.com/proxy403 Forbidden
5. Headerscurl -I https://api.example.comX-Content-Type-Options, X-Frame-Options present
6. mTLScurl https://api.example.com/internal/ without client certConnection rejected
7. Key RotationUse deprecated key after expiry401 Unauthorized
8. Audit LogsTrigger request, check logsRequest logged with all fields
9. Containerkubectl get pod -o yamlrunAsNonRoot: true, capabilities dropped
10. ScanningPush to mainCI security checks pass

Next Steps

After completing this checklist:

  1. Document your threat model: What assets are you protecting? What are the likely attack vectors?
  2. Set up monitoring: Alert on 429 responses (rate limit hits), 403s (SSRF blocks), and missing audit logs
  3. Schedule penetration testing: Hire external security researchers or run bug bounty programs
  4. Review access logs weekly: Look for anomalies (geographic, timing, API usage patterns)
  5. Automate compliance checks: Use tools like Open Policy Agent to enforce security policies

For European regulatory requirements (DORA, NIS2), see our DORA & NIS2 API Gateway Compliance Guide.

For multi-tenant security patterns, read Multi-Tenant API Gateway on Kubernetes.

For detailed security configuration in STOA Platform, consult the Security Configuration Reference and Admin Security Hardening Guide.

FAQ

How often should I rotate API keys?

Every 90 days for production systems. Critical services (payments, healthcare) should rotate monthly. Always use a 14-day grace period where both old and new keys work to allow consumers to migrate.

Can I use self-signed certificates for mTLS?

Yes, for internal service-to-service traffic. Self-signed certs are fine as long as you control the CA and all services trust it. For external-facing APIs, use certificates from a trusted public CA (Let's Encrypt, DigiCert).

What's the difference between global and per-consumer rate limits?

Global limits protect infrastructure; per-consumer limits ensure fair usage. Set global limits based on your server capacity (e.g., 10,000 req/s). Set per-consumer limits based on your pricing tiers (e.g., 60 req/min for free tier, 6,000 req/min for enterprise).

Conclusion

Security hardening is not a one-time task. Threats evolve, dependencies get vulnerabilities, and configurations drift. This 10-step checklist provides a strong foundation, but you must continuously monitor, audit, and update your gateway.

The most secure gateway is one that follows the principle of least privilege: only the features you need, only the access you require, and everything else denied by default.

For a comprehensive overview of open-source API gateway options and their security features, read our Open Source API Gateway Guide 2026.

Want to see these security controls in action? STOA Platform implements all 10 steps out of the box. Learn more in our Security Compliance documentation or explore the Gateway Concepts.