---
title: "Tutorial"
description: "Deploy a test app behind Calico Ingress Gateway, set a WAF baseline, attach it to the Gateway, and confirm a SQL injection and XSS attack are both blocked."
product: "Calico Cloud"
version: "v24.0.0"
section: "Threat defense"
canonical_url: "https://docs.tigera.io/calico-cloud/threat/gateway-waf/tutorial"
---

# Tutorial

> **SECONDARY:** This feature is tech preview. Tech preview features may be subject to significant changes before they become GA.

This tutorial takes you from a running Ingress Gateway to a blocked attack in about five minutes.

## Prerequisites

- Gateway WAF is enabled. See [Enable Gateway WAF](https://docs.tigera.io/calico-cloud/threat/gateway-waf/enable.md).
- Calico Ingress Gateway is set up. See [About Calico Ingress Gateway](https://docs.tigera.io/calico-cloud/networking/ingress-gateway/about-calico-ingress-gateway.md).
- A namespace where you can deploy a test app — this tutorial creates one called `httpbin`.

## Block an attack in five minutes

1. Deploy a test application. Create an `httpbin` namespace with a deployment and service to act as the backend:

   ```yaml
   apiVersion: v1
   kind: Namespace
   metadata:
     name: httpbin
   ---
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: httpbin
     namespace: httpbin
   spec:
     replicas: 1
     selector:
       matchLabels:
         app: httpbin
     template:
       metadata:
         labels:
           app: httpbin
       spec:
         containers:
           - name: httpbin
             image: kong/httpbin:0.1.0
             ports:
               - containerPort: 80
   ---
   apiVersion: v1
   kind: Service
   metadata:
     name: httpbin
     namespace: httpbin
   spec:
     selector:
       app: httpbin
     ports:
       - protocol: TCP
         port: 80
         targetPort: 80
   ```

   Apply it, then wait for the pod to be ready:

   ```bash
   kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s
   ```

2. Create a Gateway and HTTPRoute:

   ```yaml
   apiVersion: gateway.networking.k8s.io/v1
   kind: Gateway
   metadata:
     name: httpbin-gateway
     namespace: httpbin
   spec:
     gatewayClassName: tigera-gateway-class
     listeners:
       - name: http
         protocol: HTTP
         port: 9080
   ---
   apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
   metadata:
     name: httpbin-route
     namespace: httpbin
   spec:
     parentRefs:
       - name: httpbin-gateway
     rules:
       - matches:
           - path:
               type: PathPrefix
               value: /
         backendRefs:
           - name: httpbin
             port: 80
   ```

   > **SECONDARY:** Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod.

   Apply it, then wait for the Gateway to be accepted:

   ```bash
   kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s
   ```

   At this point `Accepted` only means Gateway passed validation. Let's now wait for the Envoy proxy Deployment that Ingress Gateway creates:

   ```bash
   kubectl wait --for=condition=Available deployment \
     -l gateway.envoyproxy.io/owning-gateway-name=httpbin-gateway \
     -n httpbin --timeout=180s
   ```

3. Confirm traffic flows. Find the Envoy service the Ingress Gateway created for this Gateway (the service lands in the Gateway's own namespace, `httpbin`):

   ```bash
   export GATEWAY_SVC=$(kubectl get svc -n httpbin \
     -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}')
   ```

   Send a test request from inside the cluster:

   ```bash
   kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \
     curl -s -o /dev/null -w "%{http_code}" \
     http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get
   ```

   ```text
   200
   ```

   Traffic is flowing through the Gateway with no WAF protection yet.

4. Apply a `GlobalWAFPolicy` to set the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode:

   > **SUCCESS:** This tutorial uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](https://docs.tigera.io/calico-cloud/threat/gateway-waf/overview.md#merge-order-and-enforcement) for the tradeoffs.

   ```yaml
   apiVersion: applicationlayer.projectcalico.org/v3
   kind: GlobalWAFPolicy
   metadata:
     name: default
   spec:
     defaultAction: Block
     coreRuleSet:
       state: Enabled
       paranoiaLevel: 1
   ```

   This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`.

5. Attach WAF to the Gateway. Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway:

   ```yaml
   apiVersion: applicationlayer.projectcalico.org/v3
   kind: WAFPolicy
   metadata:
     name: httpbin-waf
     namespace: httpbin
   spec:
     targetRefs:
       - group: gateway.networking.k8s.io
         kind: Gateway
         name: httpbin-gateway
   ```

   The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Check that the policy was accepted and programmed:

   ```bash
   kubectl get wafpolicy httpbin-waf -n httpbin \
     -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
   ```

   ```text
   Licensed=True (LicenseValid)
   Accepted=True (Accepted)
   Validated=Unknown (NotEvaluated)
   Programmed=True (ConfigurationApplied)
   Ready=True (Ready)
   ```

   All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. If `Programmed=False`, check the condition message — a common cause is the WASM image not being reachable (see [Troubleshooting](https://docs.tigera.io/calico-cloud/threat/gateway-waf/troubleshooting.md)). Allow a few seconds for Envoy to load the WASM filter before running the tests below.

6. Test WAF blocking. A normal request still passes, but a SQL injection probe and an XSS probe are both blocked:

   ```bash
   # Normal request — expect 200
   kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \
     curl -s -o /dev/null -w "%{http_code}" \
     http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get

   # SQL injection probe — expect 403
   kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \
     curl -s -o /dev/null -w "%{http_code}" \
     "http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271"

   # XSS probe — expect 403
   kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \
     curl -s -o /dev/null -w "%{http_code}" \
     "http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
   ```

   The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application.

## Clean up

```bash
kubectl delete namespace httpbin
kubectl delete globalwafpolicies default
```

## Troubleshoot

If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](https://docs.tigera.io/calico-cloud/threat/gateway-waf/troubleshooting.md) for diagnosis steps and the pull-secret configuration.

## Next steps

- [Set a cluster-wide baseline for Gateway WAF](https://docs.tigera.io/calico-cloud/threat/gateway-waf/set-a-baseline.md) — start production configuration in `Detect` mode.
- [Reference](https://docs.tigera.io/calico-cloud/threat/gateway-waf/reference.md) — CRDs, Rego validation, SecRule ID ranges, and status conditions.
