APISIX gateway and WAF
French version: ../waf-gateway.md.
Files: deploy/apisix/config.yaml (node settings) and deploy/apisix/apisix.yaml (declarative routes, standalone mode).
TLS
ssl_protocols: TLSv1.2 TLSv1.3, ECDHE suites with AES-GCM or ChaCha20-Poly1305 only, session tickets disabled.- Global rule
redirect: {http_to_https: true}: every HTTP request gets a 301 to HTTPS. Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadon every response (edge-hardeningplugin_config), on top ofEAI_HSTS_ENABLED=truein the backend.- To the control plane: HTTP inside the cluster (
scheme: http, the control plane serves HTTP only), encrypted node to node by WireGuard and authenticated by Cilium mutual authentication (ingress from namespaceapisixwithauthentication.mode: required), andupstream_hostequal toEAI_ALLOWED_HOSTS.
Coraza WAF and OWASP CRS
The coraza-proxy-wasm module is loaded by the wasm section of config.yaml (file /usr/local/apisix/proxywasm/coraza-proxy-wasm.wasm, Harbor mirror image pinned by digest). The coraza-filter plugin loads @coraza.conf-recommended, @crs-setup.conf.example and @owasp_crs/*.conf with SecRuleEngine On: blocking mode, paranoia level 1, inbound anomaly threshold 5.
For a false positive: add a targeted exclusion (SecRuleUpdateTargetById or SecRuleRemoveById on one route), never SecRuleEngine DetectionOnly in production. Try paranoia level 2 on a staging route before tightening.
Internal headers
A client can never supply an identity or an address:
- the
real-ipplugin (highest priority) derives the client address fromX-Forwarded-Foronly when the peer is the load balancer (10.0.0.0/8); serverless-pre-functionthen drops everyX-Dev-*,X-SPIFFE-ID,X-Forwarded-*,X-Real-IPandForwardedheader, and drops anX-Request-Idthat is not a short token (^[A-Za-z0-9._:-]{8,128}$), which prevents header injection and log forging;proxy-rewritesetsX-Forwarded-For,X-Real-IP,X-Forwarded-Proto,X-Forwarded-HostandX-Forwarded-Portto the gateway values;request-idgenerates a UUID when the client did not send a valid one.
In the backend, EAI_TRUSTED_PROXIES only holds the pod CIDR of APISIX and of the ingress controller: no other peer can impose X-Forwarded-For or X-SPIFFE-ID.
Body size and rate limits
| Route | Max body | Limit |
|---|---|---|
/v1/* (OIDC) | 10 MiB (EAI_MAX_BODY_BYTES) | 600 requests per minute per token, shared Redis over TLS |
/v1/* (API key) | 10 MiB | 1200 per minute per key, 200 concurrent connections |
upload routes (_UPLOAD in security/limits.py) | 100 MiB (EAI_MAX_UPLOAD_BYTES) | 60 per minute per token or key |
/v1/gateway/*, /v1/realtime/* | 10 MiB | 20 requests per second per IP |
portal /login, /api/auth/* | 64 KiB | 10 per minute per IP |
| every public route | 50 requests per second per IP (burst 100) |
Shared counters use the Redis ACL user apisix over TLS (redis_ssl: true, redis_ssl_verify: true).
Metrics and docs never public
The deny-metrics route (priority 1000) answers 404 for /metrics, /apisix/prometheus/metrics, /openapi.json, /docs and /redoc. The APISIX Prometheus exporter listens on a private port (9091); Prometheus scrapes the pods directly, allowed by the NetworkPolicies from the observability namespace. EAI_DOCS_ENABLED=false also disables the backend docs in production.
CORS and response headers
- CORS on the OIDC route only, exact origin
https://<portal-host>, no credentials, explicit header list,max_age600. - Added headers:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer,Cross-Origin-Opener-Policy,Cross-Origin-Resource-Policy,Permissions-Policy,Cache-Control: no-store;ServerandX-Powered-Byare removed,enable_server_tokens: false.
Portal ingress
templates/portal/ingress.yaml creates two Ingresses (/ and /login) with cert-manager TLS, forced HTTPS redirect, TLS 1.2 and 1.3, HSTS, a body size cap and a dedicated limit on /login (portal.ingress.loginRateLimitPerMinute, 10 by default). Edge security headers live in the <release>-portal-edge-headers ConfigMap, applied through the custom-headers annotation; the controller must allow them in global-allowed-response-headers. Snippets stay disabled on the controller.