Ship, Securely, Ch. 5: Kubernetes Runtime Security
Part of Ship, Securely, a hands-on DevSecOps book.
Chapter 5. Kubernetes Configuration Testing
Why this step
Catching issues in infra/k8s/*.yaml before kubectl apply is cheap and fast. These tools complement each other:
- kube-linter: opinionated rules → SARIF (great for PR review).
- kube-score: human-readable “why” explanations.
- Checkov: policy-as-code across K8s/Terraform/etc. (already in CI).
Run it
# from repo root
make k8s-audit
What you’ll see
kube-linter.sarifwith rule IDs & resource paths.kube-score.txtwith “critical/objective” hints.checkov.jsonfrom earlier CI runs.
Fix-it focus (quick wins)
- Add
securityContextwithrunAsNonRoot: true,readOnlyRootFilesystem: true,allowPrivilegeEscalation: false. - Set
resources.requests/limitsfor each container. - Avoid
:latestimages; pin a tag or digest. - Ensure
livenessProbeandreadinessProbeare present.
Checklist
-
make k8s-auditproduced all three reports - You fixed at least 2 misconfigs and re-ran the audit
- SARIF is viewable in your editor (e.g., VS Code SARIF viewer)
Chapter 5. Kyverno
Run the test
make kyverno-install
make kyverno-apply
make kyverno-policy-test
This attempts to apply infra/k8s/bad/deploy-bad.yaml and saves the exact
kubectl error output to artifacts/policy-tests.txt.
How to read the output
A typical rejection looks like:
Error from server: error when creating "deploy-bad.yaml":
admission webhook "validate.kyverno.svc" denied the request:
policy disallow-latest-tags, rule no-latest:
validation error: Do not use :latest image tags.
resource: Deployment/ship/bad-app, field: spec.template.spec.containers[0].image
policy enforce-pod-security-basics, rule require-seccontext:
validation error: Harden containers: non-root, no privilege escalation, read-only root FS.
resource: Deployment/ship/bad-app, field: spec.template.spec.containers[0].securityContext
Map the pieces:
- policy → which file in
policies/matched - rule → rule name in that file
- validation error → the message you wrote in the policy
- resource → kind/namespace/name that failed
- field → the exact path to fix
Evidence tip
Attach artifacts/policy-tests.txt to your PR (or include it in the evidence zip). Reviewers see which policy blocked which field and why.
Checklist
- Bad manifest rejected
- Evidence saved to
artifacts/policy-tests.txt - You can point to the exact field to change
Chapter 5. Admission Policies with Kyverno
Why this step
Linters warn, admission enforces. Policies prevent accidental drift (“works on my laptop”) from landing in the cluster.
Install Kyverno
make kyverno-install
Uses Helm in the Makefile. If you prefer official manifests, apply them instead.
Apply policies
Ensure the following are present under policies/:
disallow-latest.yamlenforce-pod-security-basics.yamlrequire-resources.yamlrequire-probes.yaml- (optional)
verify-image-signatures.yaml(paste yourcosign.pub)
Then:
make kyverno-apply
Prove it works (intentional failure)
A deliberately non-compliant manifest lives at infra/k8s/bad/deploy-bad.yaml.
Run:
make kyverno-bad-apply
Expected: kubectl shows one or more validation errors. The output is copied to artifacts/policy-tests.txt as evidence.
(Optional) Verify image signatures
If you sign locally:
cosign generate-key-pair # once
make sbom sign_blob verify_blob # proves sbom signature
# If you also sign the IMAGE later:
# cosign sign --key cosign.key $(GOOD)
# Configure verify-image-signatures.yaml with your public key and re-apply.
Checklist
- Kyverno installed and
policies/*.yamlcommitted - A bad manifest was rejected; evidence saved to
artifacts/policy-tests.txt - (Optional) verify-image-signatures policy references your cosign public key
Chapter 5. RBAC for Least Privileges
Where should “RBAC Least Privilege” live?
Keep RBAC as its own short chapter (Ops hardening) with hands-on Role/RoleBinding/SA steps. It’s a foundational Kubernetes control and applies whether or not Kyverno is present.
Reference Kyverno as an optional guardrail at the end of that chapter (e.g., “disallow wildcard RBAC”), since you already use Kyverno earlier. That keeps the learning path incremental:
- grant least privilege (RBAC files)
- verify with
kubectl auth can-i - optionally enforce via Kyverno policy
Minimal Makefile additions (fit your style)
These targets don’t change any existing flow; they’re opt-in helpers.
# ---- RBAC (namespace-scoped least privilege) ----
k8s-rbac:
@echo "== Apply app ServiceAccount + Role + RoleBinding =="
@kubectl apply -f $(K8S_DIR)/rbac/sa.yaml
@kubectl apply -f $(K8S_DIR)/rbac/role.yaml
@kubectl apply -f $(K8S_DIR)/rbac/binding.yaml
rbac-verify:
@echo "== RBAC can-i checks (evidence) =="
@mkdir -p artifacts
@{ \
date; \
echo "# RBAC verify (serviceaccount $(NAMESPACE):app-sa)"; \
kubectl -n $(NAMESPACE) auth can-i get pods --as=system:serviceaccount:$(NAMESPACE):app-sa; \
kubectl -n $(NAMESPACE) auth can-i list pods --as=system:serviceaccount:$(NAMESPACE):app-sa; \
kubectl -n $(NAMESPACE) auth can-i watch pods --as=system:serviceaccount:$(NAMESPACE):app-sa; \
kubectl -n $(NAMESPACE) auth can-i create pods --as=system:serviceaccount:$(NAMESPACE):app-sa; \
kubectl -n $(NAMESPACE) auth can-i get secrets --as=system:serviceaccount:$(NAMESPACE):app-sa; \
kubectl -n kube-system auth can-i list pods --as=system:serviceaccount:$(NAMESPACE):app-sa; \
} | tee artifacts/rbac-verify.txt
@echo "wrote artifacts/rbac-verify.txt"
RBAC files (namespace $(NAMESPACE))
infra/k8s/rbac/sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: ship
automountServiceAccountToken: true
infra/k8s/rbac/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-read
namespace: ship
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
infra/k8s/rbac/binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-read-binding
namespace: ship
subjects:
- kind: ServiceAccount
name: app-sa
namespace: ship
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: app-read
And add to your Deployment (already present file):
spec:
template:
spec:
serviceAccountName: app-sa
Optional: Kyverno guardrail for RBAC (put in your existing policies/)
Since you already install/apply Kyverno, add a simple policy to block wildcard RBAC. This belongs in the Kyverno section and you can link it from the RBAC chapter as “turn on this safety net”.
policies/disallow-rbac-wildcards.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-rbac-wildcards
spec:
validationFailureAction: enforce
background: true
rules:
- name: no-wildcard-role
match:
any:
- resources:
kinds: ["Role","ClusterRole"]
validate:
message: "Avoid wildcard RBAC; specify explicit verbs/resources."
pattern:
spec:
rules:
- verbs: "!*=*"
resources: "!*=*"
Wire it with your existing target (no new targets needed):
# just run:
make kyverno-apply
Chapter 5. Kubernetes Network Policies
— Default-Deny (with Tests)
Prereqs
- Your app is deployed in namespace ship, Service name app (adjust if different).
- Your Minikube profile has a CNI that enforces NetworkPolicies (e.g., Cilium or Calico).
If you haven’t enabled one yet, run a CNI addon that supports NetworkPolicy. (Minikube supports popular CNIs; pick one available on your setup.)
1) Capture service info
export NS=ship
export SVC=app
# Replace with your service port if not 8080:
export PORT=8080
kubectl -n $NS get svc $SVC -o wide
2) Apply a namespace default-deny
Create infra/k8s/netpol-default-deny.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: ship
spec:
podSelector: {} # all Pods in namespace
policyTypes: ["Ingress","Egress"] # deny everything by default
mkdir -p artifacts/netpol-manifests
kubectl apply -f infra/k8s/netpol-default-deny.yaml | tee artifacts/netpol-manifests/apply.txt
3) Launch a test client and try to curl (should be blocked)
# A temporary debug pod without any special labels
kubectl -n $NS run netshoot --restart=Never --image=nicolaka/netshoot -it --rm -- \
/bin/sh -lc "apk add --no-cache curl >/dev/null 2>&1 || true; \
echo 'curling cluster service…'; \
curl -sS --max-time 5 http://$SVC.$NS.svc.cluster.local:$PORT/healthz || echo 'BLOCKED'"
Save output:
kubectl -n $NS run netshoot --restart=Never --image=nicolaka/netshoot --command -- sleep 5 2>/dev/null || true
kubectl -n $NS delete pod netshoot 2>/dev/null || true
Append your curl attempt (stdout/stderr) to ./artifacts/netpol-test.txt:
echo -e "\n--- BLOCK attempt $(date) ---" >> artifacts/netpol-test.txt
# (paste the curl output from the interactive run here)
Expected: timeout or connection refused → blocked.
4) Allow traffic only from labeled clients to the app Pods
Label your app Pods (if not already):
kubectl -n $NS label deploy/$SVC app=$SVC --overwrite
Create infra/k8s/netpol-allow-app-from-labeled.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-from-labeled
namespace: ship
spec:
podSelector:
matchLabels:
app: app # target: your app Pods (adjust label if needed)
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
access: allowed
ports:
- protocol: TCP
port: 8080 # adjust to your app containerPort/Service port
Apply it:
kubectl apply -f infra/k8s/netpol-allow-app-from-labeled.yaml \
| tee -a artifacts/netpol-manifests/apply.txt
5) Test again (first blocked, then allowed)
Create an unlabeled client (still blocked):
kubectl -n $NS run np-client --restart=Never --image=nicolaka/netshoot -- \
/bin/sh -lc "apk add --no-cache curl >/dev/null 2>&1 || true; \
curl -sS --max-time 5 http://$SVC.$NS.svc.cluster.local:$PORT/healthz || echo BLOCKED" \
| tee -a artifacts/netpol-test.txt
kubectl -n $NS delete pod np-client --now
Create an allowed client and label it:
kubectl -n $NS run np-allowed --restart=Never --image=nicolaka/netshoot -- /bin/sh -lc "sleep 3600" >/dev/null 2>&1 &
sleep 2
kubectl -n $NS label pod np-allowed access=allowed --overwrite
kubectl -n $NS exec np-allowed -- /bin/sh -lc \
"apk add --no-cache curl >/dev/null 2>&1 || true; \
curl -sS --max-time 5 http://$SVC.$NS.svc.cluster.local:$PORT/healthz" \
| tee -a artifacts/netpol-test.txt
kubectl -n $NS delete pod np-allowed --now
Expected: first attempt BLOCKED, second attempt 200 OK.
Makefile helpers (optional)
netpol-apply:
kubectl apply -f infra/k8s/netpol-default-deny.yaml
kubectl apply -f infra/k8s/netpol-allow-app-from-labeled.yaml
netpol-test:
@NS=ship SVC=app PORT=8080 bash -lc '\
kubectl -n $$NS run np-client --restart=Never --image=nicolaka/netshoot -- /bin/sh -lc "apk add --no-cache curl >/dev/null 2>&1 || true; curl -sS --max-time 5 http://$$SVC.$$NS.svc.cluster.local:$$PORT/healthz || echo BLOCKED" | tee -a artifacts/netpol-test.txt; \
kubectl -n $$NS delete pod np-client --now; \
kubectl -n $$NS run np-allowed --restart=Never --image=nicolaka/netshoot -- /bin/sh -lc "sleep 3600" >/dev/null 2>&1 & sleep 2; \
kubectl -n $$NS label pod np-allowed access=allowed --overwrite; \
kubectl -n $$NS exec np-allowed -- /bin/sh -lc "apk add --no-cache curl >/dev/null 2>&1 || true; curl -sS --max-time 5 http://$$SVC.$$NS.svc.cluster.local:$$PORT/healthz" | tee -a artifacts/netpol-test.txt; \
kubectl -n $$NS delete pod np-allowed --now; \
echo "Wrote artifacts/netpol-test.txt"'
Checklist
- Your Minikube profile has a CNI that enforces NetworkPolicy
-
default-denyapplied for Ingress+Egress - Labeled allow-rule works (blocked → allowed)
-
artifacts/netpol-test.txtsaved with both outcomes
Chapter 5. Checklist
Decide how you’ll enforce at admission and constrain at runtime.
Namespaces & Multitenancy
- Namespace model: Per team/app/tenant? Is
pod-security.kubernetes.io/enforce=restrictedapplied across namespaces? - Separation: Do build/test namespaces differ from runtime namespaces?
RBAC & Service Accounts
- Least privilege: Do workloads run under dedicated SAs with namespace-scoped Roles only?
- can-i evidence: Do you record
kubectl auth can-ichecks in artifacts?
Network Policies
- Default deny: Is there a cluster-wide default deny for ingress/egress?
- Allow rules: Have you explicitly allowed app ingress and DNS egress only where needed?
Admission Controls (Kyverno)
- Deny bad: Which rules are enforced? (no privileged, drop ALL caps, runAsNonRoot, probes, seccomp)
- Verify good: Are image signatures verified? Is there an org-registry allow-list?
Image Integrity
- Cosign policy: Which public keys are trusted? How are they rotated?
- Untrusted images: What’s the response when an image is denied (runbook, alerting)?
Secrets & Config
- SOPS/age: Are Kubernetes Secrets encrypted in Git? Who can decrypt?
- External secrets: Any plan to fetch secrets from KMS/secret manager?
Runtime Signals & WAF
- Falco: Which rules are enabled? Who receives alerts, and where?
- WAF: Where does WAF sit (ingress controller vs edge)? What attack classes do you block first?
In-Cluster Scanning
- Trivy Operator: Will you enable it? Who triages CRD reports and how often?
Chapter 5. Encrypt Kubernetes Secrets in Git
Why this
You don’t need a cloud KMS to stop plaintext secrets in Git. SOPS + age lets you encrypt fields in YAML, commit safely, and decrypt only when applying on your machine.
1) Install SOPS and age (one-time)
- macOS:
brew install sops age - Windows (PowerShell):
winget install -e --id Mozilla.SOPSandwinget install -e --id FiloSottile.OpenSSH(age may be in package managers you use) - Linux (Debian/Ubuntu):
sudo apt install -y sops age
If your platform uses different packages, install SOPS and age via your package manager of choice.
2) Generate an age key (keep local)
age-keygen -o .sops.agekey
echo 'export SOPS_AGE_KEY_FILE=.sops.agekey' >> .env.sops
source .env.sops
Do not commit
.sops.agekey. Add it to.gitignore.
3) Define SOPS rules for Kubernetes Secrets
Create .sops.yaml at repo root:
# Encrypt Kubernetes Secret data/stringData by default
creation_rules:
- path_regex: infra/k8s/.*\.secret\.ya?ml$
encrypted_regex: '^(data|stringData)$'
age: []
# age recipient(s) will be added automatically from your key when you first encrypt
4) Create a sample Secret (unencrypted working copy)
Create infra/k8s/secret.secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: demo-secret
namespace: ship
type: Opaque
stringData:
API_TOKEN: "replace-me"
5) Encrypt it in-place
sops -e -i infra/k8s/secret.secret.yaml
git add infra/k8s/secret.secret.yaml .sops.yaml
git commit -m "chore(secrets): add encrypted Secret via SOPS"
Open the file—you should see ciphertext, plus a sops: metadata block.
6) Apply to the cluster (decrypt on the fly)
sops -d infra/k8s/secret.secret.yaml | kubectl apply -f -
kubectl -n ship get secret demo-secret -o yaml | grep -A3 data:
You’ll see base64 fields at runtime (normal for K8s), but Git still holds ciphertext.
7) Audit evidence (prove it’s encrypted)
grep -n "sops:" -n infra/k8s/secret.secret.yaml > artifacts/sops-audit.txt
echo "Wrote artifacts/sops-audit.txt"
Makefile helpers (optional)
sops-keygen:
@test -f .sops.agekey || age-keygen -o .sops.agekey
@echo "export SOPS_AGE_KEY_FILE=.sops.agekey" > .env.sops
@echo "Run: source .env.sops"
sops-encrypt:
sops -e -i infra/k8s/secret.secret.yaml
sops-decrypt:
sops -d infra/k8s/secret.secret.yaml > /tmp/secret.plain.yaml && \
echo "Decrypted to /tmp/secret.plain.yaml (do not commit)"
k8s-apply-sops:
sops -d infra/k8s/secret.secret.yaml | kubectl apply -f -
Add
.sops.agekeyto.gitignore:
.sops.agekey
.env.sops
/tmp/secret.plain.yaml
Checklist
-
.sops.agekeygenerated and not committed -
.sops.yamlpresent with Kubernetes rules -
infra/k8s/secret.secret.yamlshows ciphertext in Git -
sops -d … | kubectl apply -f -works -
artifacts/sops-audit.txtsaved
Chapter 5. OPA and Conftest
Why this
Static linters (kube-linter / kube-score / Checkov) are great. OPA/Conftest lets you add your org’s rules that those tools don’t cover or you want to phrase differently, and run them before admission.
1) Repo layout
policy/ k8s.rego infra/k8s/
your Deployment/Service/etc.
2) Policy (copy/paste)
Create policy/k8s.rego with three practical rules:
package main
# Input is a list of Kubernetes resources (YAML files parsed by conftest)
deny[msg] {
some i
input[i].kind == "Deployment"
some c
containers := input[i].spec.template.spec.containers
containers[c].image endswith ":latest"
msg := sprintf("Deployment %q uses :latest tag in container %q", [input[i].metadata.name, containers[c].name])
}
deny[msg] {
some i
input[i].kind == "Deployment"
some c
containers := input[i].spec.template.spec.containers
not containers[c].securityContext.runAsNonRoot
msg := sprintf("Deployment %q container %q missing runAsNonRoot: true", [input[i].metadata.name, containers[c].name])
}
deny[msg] {
some i
input[i].kind == "Deployment"
some c
containers := input[i].spec.template.spec.containers
not containers[c].resources.requests.cpu
not containers[c].resources.requests.memory
msg := sprintf("Deployment %q container %q missing CPU/Memory requests (and likely limits)", [input[i].metadata.name, containers[c].name])
}
You can extend this file over time (e.g., require readiness/liveness, forbid privileged, etc.).
3) Run Conftest (Dockerized, no local install)
From repo root:
mkdir -p artifacts
docker run --rm -v "$PWD":/work -w /work openpolicyagent/conftest:v0.56.0 \
test infra/k8s -p policy | tee artifacts/conftest.txt || true
- Exit code
0= all good; non-zero = at least onedenyfired. - Either way, the report is saved to
./artifacts/conftest.txt.
4) Makefile target (optional)
k8s-conftest: init
docker run --rm -v $(PWD):/work -w /work openpolicyagent/conftest:v0.56.0 \
test infra/k8s -p policy | tee artifacts/conftest.txt || true
5) Integrate with your flow
- Run
make k8s-audit k8s-conftestbeforemake cd. - Keep Conftest in WARN for Week 1–2; promote to BLOCK once noise is low.
Checklist
-
policy/k8s.regocommitted with at least 3 rules -
artifacts/conftest.txtproduced and reviewed - Violations fixed (or exceptions recorded) before deploy
Chapter 5. Runtime Detection with Falco
Why this step
Admission policies reduce risky manifests, but runtime behavior can still go sideways. Falco watches syscalls and flags suspicious actions.
Install Falco
make falco-install
kubectl -n falco get pods
Add a simple rule (no interactive shells)
Ensure this file exists:
falco/rules.d/no-shell.yaml
(Already provided in the repo plan: flags /bin/sh, /bin/bash, /bin/zsh inside containers.)
Load custom rules following the Falco chart’s values—mount a ConfigMap with your
rules.d/contents. (You can keep this step optional for the first run.)
Generate an alert
Open a shell inside your app container:
kubectl -n ship exec -it deploy/app -- sh
exit
Tail logs:
make falco-logs
Export the last 10 minutes as evidence:
make falco-export
# -> artifacts/falco-last10m.log
Cleanup
helm -n falco uninstall falco
kubectl delete ns falco
Checklist
- Falco installed; you saw logs from the DaemonSet
- Entering a container shell produced an alert
- artifacts/falco-last10m.log saved
Chapter 5. Trivy Operator
Why this step
You already produce local artifacts in ./artifacts/. The operator gives you a cluster view of risks as Kubernetes resources.
Install
Follow the operator’s installation instructions (via Helm or manifests). On a successful install you can query:
kubectl get ns # confirm operator namespace
kubectl get vulnerabilityreports -A
kubectl get configauditreports -A
Evidence (optional)
Save a quick dump:
kubectl get vulnerabilityreports -A -o wide > artifacts/operator-vulnreports.txt
kubectl get configauditreports -A -o wide > artifacts/operator-configaudit.txt
Cleanup
Uninstall using the same method you installed with (helm uninstall or kubectl delete).
Checklist
- Operator installed and CRDs visible
- Export of reports saved under artifacts/ (optional)
- Uninstall instructions verified
Chapter 5. WAF Quickstart
Why this
When a code fix needs a sprint, a reverse-proxy filter can block a whole class of bad inputs as a stopgap. We’ll use the community ingress-nginx controller that ships with Minikube and add a basic rule via annotations on your Ingress.
Note: These steps use the Minikube ingress addon. They don’t require separate ingress images or cloud ingress.
1) Enable the Ingress controller
minikube -p ship addons enable ingress
kubectl -n ingress-nginx get pods
2) Create an Ingress for your app
Assuming your Service is app in namespace ship on port 8080, create infra/k8s/ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: ship
annotations:
# Turn on basic request filtering in the ingress (see next section for rule)
nginx.ingress.kubernetes.io/server-snippet: |
# Block very basic script injection in query/path
if ($request_uri ~* "<script") { return 403; }
spec:
ingressClassName: nginx
rules:
- host: app.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 8080
Apply it:
kubectl apply -f infra/k8s/ingress.yaml
We use
server-snippetto add a minimal block rule at the NGINX layer. For a richer rule set, you can layer ModSecurity/CRS if your ingress build enables it; start with the simple rule to prove the workflow.
3) Get a local URL for the Ingress
minikube -p ship tunnel >/dev/null 2>&1 &
# /etc/hosts entry for convenience (macOS/Linux):
echo "127.0.0.1 app.local" | sudo tee -a /etc/hosts
Verify:
curl -i http://app.local/healthz
4) Prove the block (virtual patch)
Try a normal request and a malicious one; capture both results:
{
echo "--- OK request $(date) ---"
curl -sSi "http://app.local/?input=hello" | head -n 1
echo "--- BLOCK attempt $(date) ---"
curl -sSi "http://app.local/?input=<script>alert(1)</script>" | head -n 1
} | tee -a artifacts/waf-test.txt
Expected:
- Normal request →
HTTP/1.1 200 OK - Attack string →
HTTP/1.1 403 Forbidden(blocked by the server snippet)
5) Optional: move the rule to a dedicated ConfigMap
If you prefer not to inline snippets in the Ingress, you can mount a global config via the ingress-nginx ConfigMap (cluster-wide) and reference it. For the local lab, the per-Ingress annotation is sufficient and keeps everything in your repo.
6) Clean up (optional)
# remove hosts entry if added
sudo sed -i.bak '/app\.local/d' /etc/hosts
# stop the tunnel process if left running in the background
pkill -f "minikube.*tunnel" || true
Makefile helper (optional)
waf-apply:
kubectl apply -f infra/k8s/ingress.yaml
@echo "Ingress applied. If needed, run: minikube -p ship tunnel"
waf-test:
@bash -lc '\
mkdir -p artifacts; \
echo "--- OK request $$(
date) ---" >> artifacts/waf-test.txt; \
curl -sSi "http://app.local/?input=hello" | head -n 1 >> artifacts/waf-test.txt || true; \
echo "--- BLOCK attempt $$(
date) ---" >> artifacts/waf-test.txt; \
curl -sSi "http://app.local/?input=<script>alert(1)</script>" | head -n 1 >> artifacts/waf-test.txt || true; \
echo "Wrote artifacts/waf-test.txt"'
Checklist
- Ingress controller running (
ingress-nginxPods are Ready) - Ingress created for your Service;
app.localresolves - Normal traffic returns 200; attack string returns 403
-
artifacts/waf-test.txtsaved with both outcomes