Skip to main content

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; preload on every response (edge-hardening plugin_config), on top of EAI_HSTS_ENABLED=true in 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 namespace apisix with authentication.mode: required), and upstream_host equal to EAI_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:

  1. the real-ip plugin (highest priority) derives the client address from X-Forwarded-For only when the peer is the load balancer (10.0.0.0/8);
  2. serverless-pre-function then drops every X-Dev-*, X-SPIFFE-ID, X-Forwarded-*, X-Real-IP and Forwarded header, and drops an X-Request-Id that is not a short token (^[A-Za-z0-9._:-]{8,128}$), which prevents header injection and log forging;
  3. proxy-rewrite sets X-Forwarded-For, X-Real-IP, X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Port to the gateway values;
  4. request-id generates 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​

RouteMax bodyLimit
/v1/* (OIDC)10 MiB (EAI_MAX_BODY_BYTES)600 requests per minute per token, shared Redis over TLS
/v1/* (API key)10 MiB1200 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 MiB20 requests per second per IP
portal /login, /api/auth/*64 KiB10 per minute per IP
every public route50 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_age 600.
  • 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; Server and X-Powered-By are 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.