Path · Exact
Matches the path exactly. Anything longer or shorter does not match.
- matches: - path: { type: Exact, value: /features/exact }
HTTP Method
First rule matches only POST; a fallback rule catches every other method on the same path.
- matches: - path: { type: PathPrefix, value: /features/post } method: POST
Header matching
When x-canary: yes is present, routes to echo-v2; otherwise to echo.
- matches: - path: { type: PathPrefix, value: /features/header } headers: - { name: x-canary, value: "yes" }
Query param matching
?canary=true routes to echo-v2; without it, default backend.
- matches: - path: { type: PathPrefix, value: /features/query } queryParams: - { name: canary, value: "true" }
Weighted traffic split
Splits 80% to echo, 20% to echo-v2. Click to fire 20 requests and see distribution.
- backendRefs: - { name: echo, port: 80, weight: 80 } - { name: echo-v2, port: 80, weight: 20 }
URLRewrite filter
Gateway strips /features/rewrite and forwards as /rewritten/... to the backend.
- filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: /rewritten
RequestHeaderModifier
Gateway injects two extra headers into the request before forwarding. The echo backend shows them in its JSON.
- filters: - type: RequestHeaderModifier requestHeaderModifier: add: - { name: x-injected-by-gateway, value: envoy-gateway-eg } - { name: x-trace-id, value: demo-trace-42 }
RequestRedirect filter
Gateway answers with 302 and a Location header — never even contacts a backend.
- filters: - type: RequestRedirect requestRedirect: path: type: ReplaceFullPath replaceFullPath: /features/exact statusCode: 302
Path · RegularExpression
Matches when the path matches the regex. Anything else falls through. Requires the experimental Gateway-API CRDs (installed on this cluster).
- matches: - path: type: RegularExpression value: "^/features/users/[0-9]+$"
Hostname matching
Two HTTPRoutes attach to the same Gateway with different hostnames:. We probe with an explicit Host: header — no DNS needed.
hostnames: [ "v1.features.wolfslight.cc" ] # second HTTPRoute hostnames: [ "v2.features.wolfslight.cc" ]
ResponseHeaderModifier
Adds x-added-header, sets content-language: en-CH, removes etag — all three sub-modes in one filter.
- filters: - type: ResponseHeaderModifier responseHeaderModifier: add: - { name: x-added-header, value: "I was added by the Gateway" } set: - { name: content-language, value: en-CH } remove: [ "etag" ]
RequestMirror filter
Primary backend echo answers; echo-v2 receives a fire-and-forget copy. The primary's response is what you see; the mirror runs in the background.
- filters: - type: RequestMirror requestMirror: backendRef: kind: Service name: echo-v2 port: 80
Cross-namespace backendRef
No longer cross-namespace — corrected 2026-08-26: this route was originally built as an HTTPRoute in a demo namespace pointing at a Service in a separate lb-demo namespace, guarded by a ReferenceGrant. On this cluster both moved into the single gateway-demo namespace, so the backendRef is now local — no ReferenceGrant needed here anymore. The route still answers; the live x-gw-matched header below says so itself ("now local: see deviation note"). What follows is the original cross-namespace shape for reference, kept because ReferenceGrant is still exactly how you'd wire this up if the Service lived in another namespace.
- backendRefs: - name: hello namespace: lb-demo # cross-NS (historical shape — now local) port: 80 # + ReferenceGrant in lb-demo allowing routes from demo
Backend timeouts
Slow backend (httpbin /delay/5) sleeps 5 s. HTTPRoute has timeouts.request: 2s. Gateway returns 504 after ~2 s without waiting for the backend.
- matches: - path: { type: PathPrefix, value: /features/timeout } timeouts: request: "2s" backendRequest: "2s" # backend would take 5s
BackendTrafficPolicy · Local Rate Limit (60 req/min, per route)
Envoy Gateway's local rate-limit: a fixed bucket of 60 requests per minute. Real behaviour, corrected 2026-08-26 (again — see below): the policy targets the Gateway, not this one route specifically, but Envoy Gateway applies a Gateway-targeted Local limit per HTTPRoute — each of the four routes under this Gateway gets its own independent 60/min bucket. It also has no per-source-IP Distinct selector, so within a route's bucket it's still first-come-first-served, not a per-visitor allowance. Burst past 60 on this route in the current minute and the surplus comes back as 429 Too Many Requests, from any IP — but other routes on this Gateway are unaffected.
apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: name: gateway-demo-ratelimit spec: targetRefs: - kind: Gateway name: gateway-demo rateLimit: type: Local local: rules: - limit: requests: 60 unit: Minute
Corrected 2026-08-26: a Gateway-targeted `Local` rate limit is applied PER ROUTE by Envoy Gateway —
each HTTPRoute under gateway-demo gets its own independent 60/min bucket, not one bucket shared
across the whole Gateway. (This exact mistake was already fixed once in this repo, in
apps/gateway-demo/policies.yaml, commit 5025d9b, 2026-07-15 — it crept back in here and had to be
fixed a second time.)
Why the button above doesn't prove that on its own: firing 80 requests at ONE route and seeing
60x200/20x429 is consistent with BOTH "per route" and "gateway-wide" — a single-route burst can't
tell them apart. The distinguishing test needs a SECOND route: burst /features/exact to exhaustion,
then immediately hit /secret and / — if the limit were gateway-wide, those would 429 too. They
don't (401 and 200, live-checked 2026-08-26), so the bucket is per-route.
Why there's still no per-IP fairness: Local rate limiting is scoped per route/gateway target AND
per Envoy replica (gateway-demo runs 1 replica) — never per source IP. One flooding client can
still exhaust a route's bucket for every other visitor of THAT route. Real per-IP fairness needs
Global Rate Limiting (rate-limit-service + Redis) — see apps/gateway-demo/policies.yaml for the
full reasoning.
Security Headers via ResponseHeaderModifier
Six browser-side hardening headers set globally on the UI + JSON + Secret routes: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Content-Security-Policy.
filters: - type: ResponseHeaderModifier responseHeaderModifier: add: - Strict-Transport-Security: "max-age=63072000; includeSubDomains; preload" - X-Content-Type-Options: "nosniff" - X-Frame-Options: "DENY" - Referrer-Policy: "strict-origin-when-cross-origin" - Permissions-Policy: "geolocation=(), camera=(), microphone=()" - Content-Security-Policy: "default-src 'self'; …"
Coraza WAF · OWASP-style rules via EnvoyExtensionPolicy
Coraza is the OWASP-endorsed ModSecurity successor (Go-based, runs as a WASM filter inside Envoy), running here as EnvoyExtensionPolicy/gateway-demo-waf against the OWASP Core Rule Set. It targets three HTTPRoutes on this Gateway — features, v1-features, v2-features — with a pinned WASM image digest (never :latest) and failOpen: false: if the WASM module crashes, requests are rejected rather than let through unfiltered — a deliberate demo choice, carried over from the same trade-off made in secpol-demo. Probes hit /features/exact with malicious payloads — the WAF returns 403 before the request reaches the backend.
apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyExtensionPolicy metadata: name: gateway-demo-waf spec: targetRefs: - kind: HTTPRoute name: features - kind: HTTPRoute name: v1-features - kind: HTTPRoute name: v2-features wasm: - name: coraza-waf code: type: Image image.url: ghcr.io/corazawaf/coraza-proxy-wasm@sha256:65d6009b… # pinned, not :latest failOpen: false config: directives_map: default: - "Include @owasp_crs/*.conf" - "SecRuleEngine On"
TCPRoute · raw L4 routing to a stateless echo backend
No HTTP, no hostname-matching — just port-based L4 forwarding. Gateway listener tcp on port 9007, TCPRoute tcp-echo hands traffic straight to a registry.k8s.io/e2e-test-images/agnhost porter backend. Corrected 2026-08-26: this was originally advertised as Redis on 6379 with password auth. That was deliberately not rebuilt — an open, writable datastore on the same public ELB as this documentation site is a risk a demo shouldn't carry, and swapping in some other service on 6379 would still have signalled "Redis lives here" to anyone port-scanning. The current backend is stateless: it stores nothing and has nothing to authenticate. Browser can't speak raw TCP; test from your laptop:
apiVersion: gateway.networking.k8s.io/v1alpha2 kind: TCPRoute metadata: name: tcp-echo namespace: gateway-demo spec: parentRefs: - name: gateway-demo sectionName: tcp # Gateway listener :9007 rules: - backendRefs: - name: tcp-echo port: 9007 # TCPRoute has NO hostnames field — TCP carries no host identifier. # Routing is purely by listener-port. A new Gateway listener means the CCM # opens a matching port on the ELB — see /howto for that event trail.
GRPCRoute · gRPC routing through the Gateway
Separate Route kind for gRPC (HTTP/2 + protobuf). Attaches to a Gateway listener that has allowedRoutes.kinds: [GRPCRoute]. Live on grpc.wolfslight.cc with its own listener + cert. Corrected 2026-08-26: the backend is not fortio — that image is not pullable on this cluster (403 from the registry). It's grpc/java-example-hostname, the official example from the Envoy Gateway docs, exposing helloworld.Greeter, grpc.health.v1.Health and grpc.reflection.v1.ServerReflection. There is also no cors-grpc SecurityPolicy — it was deliberately not carried over (see apps/gateway-demo/policies.yaml), so a cross-origin browser call from gateway.wolfslight.cc fails CORS preflight before it reaches the Route. The button below is disabled for that reason; this is a CLI-only demo — grpcurl from your laptop:
apiVersion: gateway.networking.k8s.io/v1 kind: GRPCRoute metadata: name: grpc-echo namespace: gateway-demo spec: parentRefs: - name: gateway-demo sectionName: grpc hostnames: [ "grpc.wolfslight.cc" ] rules: - backendRefs: - name: grpc-echo port: 50051 # Service port has appProtocol: grpc so Envoy uses HTTP/2 upstream
SecurityPolicy · Basic Auth gate to /secret
Envoy Gateway's SecurityPolicy CR attaches Basic Auth to an HTTPRoute. Demo credentials: test:test. Wrong/missing creds → 401; correct creds → a properly classified Resident-Evil-themed leak from inside the Raccoon Cluster.
apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: name: secret-basic-auth namespace: gateway-demo spec: targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: secret # the route to protect basicAuth: users: name: secret-basic-auth # htpasswd Secret