diff --git a/test/e2e/trafficprotectionpolicy-enforce-attack/chainsaw-test.yaml b/test/e2e/trafficprotectionpolicy-enforce-attack/chainsaw-test.yaml new file mode 100644 index 00000000..9e6dca69 --- /dev/null +++ b/test/e2e/trafficprotectionpolicy-enforce-attack/chainsaw-test.yaml @@ -0,0 +1,231 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: trafficprotectionpolicy-enforce-blocks-attack +# Mechanism A enforcement-correctness coverage (network-services-operator#242). +# A valid Enforce TrafficProtectionPolicy (detection >= blocking) attached to a +# gateway must, end to end on the real Coraza edge data plane: +# 1. pass benign traffic through to the backend with the body intact, and +# 2. block a CRS-tripping attack payload with HTTP 403. +# +# Body integrity is asserted on the benign 200 (not just the status code): a WAF +# translation bug can leave the listener up yet corrupt or replace the response +# body (infra#3321). The branded local_reply_config only maps status >= 500, so a +# WAF block returns Coraza's raw 403 (unbranded) — this test asserts the 403 and +# that the attack never reached the backend, not a branded body. +# +# Precondition: the downstream (nso-infra) must have the WAF data plane wired up +# (extension-server + the extensionManager Envoy Gateway registered on the +# `datum-downstream-gateway` GatewayClass, with the Coraza filter). +# `make prepare-infra-cluster` installs this via `make downstream-waf-dataplane`. +spec: + cluster: nso-infra + # EG only reconciles namespaces carrying this label. + namespaceTemplate: + metadata: + labels: + meta.datumapis.com/upstream-cluster-name: e2e + steps: + - name: Deploy a backend + try: + - apply: + resource: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: echo + spec: + replicas: 1 + selector: + matchLabels: + app: echo + template: + metadata: + labels: + app: echo + spec: + containers: + - name: echo + image: hashicorp/http-echo:1.0 + args: ["-text=hello from backend", "-listen=:8080"] + ports: + - containerPort: 8080 + - apply: + resource: + apiVersion: v1 + kind: Service + metadata: + name: echo + spec: + selector: + app: echo + ports: + - port: 80 + targetPort: 8080 + - assert: + resource: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: echo + status: + availableReplicas: 1 + + - name: Route through the WAF gateway with a valid Enforce policy + bindings: + - name: hostname + value: (join('.', [$namespace, 'e2e.test'])) + try: + - apply: + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: waf-gw + spec: + gatewayClassName: datum-downstream-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: ($hostname) + allowedRoutes: + namespaces: + from: Same + - apply: + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: echo + spec: + parentRefs: + - name: waf-gw + hostnames: + - ($hostname) + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: echo + port: 80 + - apply: + resource: + apiVersion: networking.datumapis.com/v1alpha + kind: TrafficProtectionPolicy + metadata: + name: enforce-waf + spec: + mode: Enforce + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: waf-gw + ruleSets: + - type: OWASPCoreRuleSet + owaspCoreRuleSet: {} + - assert: + timeout: 3m + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: waf-gw + status: + (conditions[?type == 'Programmed']): + - status: "True" + catch: + - script: + timeout: 60s + content: | + set -x + kubectl get gateway -A -o yaml + kubectl describe gateway -A + kubectl get gatewayclass datum-downstream-gateway -o yaml + kubectl -n datum-downstream-gateway get pods -o wide + kubectl -n datum-downstream-gateway describe pods + kubectl -n datum-downstream-gateway logs deploy/envoy-gateway --tail=-1 + kubectl -n datum-downstream-gateway logs -l gateway.envoyproxy.io/owning-gateway-namespace --all-containers --tail=200 || true + + - name: Benign traffic reaches the backend with the body intact + description: > + Probe the downstream Envoy via the kind hostPort (30080). Assert the + benign GET returns 200 AND the backend body ("hello from backend") is + served intact — a WAF/listener translation bug can keep the listener up + while corrupting or replacing the body (infra#3321), which a status-only + check would miss. + bindings: + - name: hostname + value: (join('.', [$namespace, 'e2e.test'])) + try: + - script: + env: + - name: HOSTNAME + value: ($hostname) + content: | + set -u + for i in $(seq 1 40); do + resp=$(curl -s -w '\n%{http_code}' --max-time 5 \ + -H "Host: ${HOSTNAME}" http://localhost:30080/) || resp=$'\n000' + code=$(printf '%s' "$resp" | tail -n1) + body=$(printf '%s' "$resp" | sed '$d') + echo "attempt ${i}: HTTP ${code}" + if [ "${code}" = "200" ]; then + case "${body}" in + *"hello from backend"*) + echo "benign body intact" + exit 0 + ;; + *) + echo "benign GET returned 200 but body was corrupted: [${body}]" + exit 1 + ;; + esac + fi + sleep 3 + done + echo "benign request never returned 200" + exit 1 + + - name: A CRS-tripping attack payload is blocked with 403 + description: > + A path-traversal payload in a query parameter trips CRS rule 930110 + (severity CRITICAL, score 5) which meets the default inbound anomaly + threshold at paranoia level 1, so Coraza denies the request with 403. + Assert the 403 and that the attack never reached the backend. + bindings: + - name: hostname + value: (join('.', [$namespace, 'e2e.test'])) + try: + - script: + env: + - name: HOSTNAME + value: ($hostname) + content: | + set -u + url='http://localhost:30080/?file=../../../../etc/passwd' + for i in $(seq 1 20); do + resp=$(curl -s -w '\n%{http_code}' --max-time 5 \ + -H "Host: ${HOSTNAME}" "${url}") || resp=$'\n000' + code=$(printf '%s' "$resp" | tail -n1) + body=$(printf '%s' "$resp" | sed '$d') + echo "attempt ${i}: HTTP ${code}" + if [ "${code}" = "403" ]; then + case "${body}" in + *"hello from backend"*) + echo "attack was blocked with 403 but backend body leaked" + exit 1 + ;; + *) + echo "attack blocked with 403, backend not reached" + exit 0 + ;; + esac + fi + sleep 3 + done + echo "attack payload was not blocked with 403" + exit 1