See the prerequisites
This deployment is for NGINX Ingress Controller and F5 WAF for NGINX with precompiled WAF policies and log profiles
- Create NGINX Ingress Controller namespace
kubectl create namespace nginx-ingress
- Create Kubernetes secret to pull images from NGINX private registry
kubectl create secret docker-registry regcred --docker-server=private-registry.nginx.com --docker-username=`cat <nginx-one-eval.jwt>` --docker-password=none -n nginx-ingress
Note: <nginx-one-eval.jwt> is the path and filename of your nginx-one-eval.jwt file
- Create Kubernetes secret holding the NGINX Plus license
kubectl create secret generic license-token --from-file=license.jwt=<nginx-one-eval.jwt> --type=nginx.com/license -n nginx-ingress
Note: <nginx-one-eval.jwt> is the path and filename of your nginx-one-eval.jwt file
- List available NGINX Ingress Controller docker images that include NGINX App Protect WAF
curl -s https://private-registry.nginx.com/v2/nginx-ic-nap-v5/nginx-plus-ingress/tags/list --key <nginx-one-eval.key> --cert <nginx-one-eval.crt> | jq
Note: <nginx-one-eval.key> and <nginx-one-eval.key> are the path and filename of your nginx-one-eval.crt and nginx-one-eval.crt files respectively
Pick the latest version (5.4.1 at the time of writing)
- Apply NGINX Ingress Controller custom resources (make sure the URI below references the latest available
5.xNGINX Ingress Controller version)
kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.4.1/deploy/crds.yaml
kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.4.1/deploy/crds-nap-waf.yaml
- Create the PVCs to store compiled WAF policy bundles and logging profiles
kubectl apply -f ./deployment/pvcs.yaml
- Install NGINX Ingress Controller with NGINX App Protect through its Helm chart (set
nginx.image.tagto the latest5.xavailable NGINX Ingress Controller version and adjust the chart version accordingly)
helm install nic oci://ghcr.io/nginx/charts/nginx-ingress \
--version 2.5.1 \
--set controller.image.repository=private-registry.nginx.com/nginx-ic-nap-v5/nginx-plus-ingress \
--set controller.image.tag=5.4.1 \
--set controller.nginxplus=true \
--set controller.appprotect.enable=true \
--set controller.appprotect.v5=true \
--set-json 'controller.appprotect.volumes=[{"name":"app-protect-bd-config","emptyDir":{}},{"name":"app-protect-config","emptyDir":{}},{"name":"app-protect-bundles","persistentVolumeClaim":{"claimName":"task-pv-claim"}}]' \
--set controller.volumeMounts[0].name=app-protect-bundles \
--set controller.volumeMounts[0].mountPath="/etc/app_protect/bundles/" \
--set controller.serviceAccount.imagePullSecretName=regcred \
--set controller.mgmt.licenseTokenSecretName=license-token \
--set controller.service.type=NodePort \
-n nginx-ingress
- Check NGINX Ingress Controller pod status
kubectl get pods -n nginx-ingress
Pod should be in the Running state
NAME READY STATUS RESTARTS AGE
nic-nginx-ingress-controller-8b75b59bf-5zcc2 3/3 Running 0 3m28s
- Check NGINX Ingress Controller logs
kubectl logs -l app.kubernetes.io/instance=nic -n nginx-ingress -c nginx-ingress
Output should be similar to
I20260409 15:57:09.501899 1 main.go:112] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"nginx-ingress", Name:"nic-nginx-ingress", UID:"72830f00-d69c-400c-9f11-611df7a9b418", APIVersion:"v1", ResourceVersion:"108817766", FieldPath:""}): type: 'Normal' reason: 'Updated' ConfigMap nginx-ingress/nic-nginx-ingress updated without error
I20260409 15:57:09.501926 1 main.go:112] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"nginx-ingress", Name:"nic-nginx-ingress-mgmt", UID:"4bfd2675-012e-43cb-a7a2-2aa57da79561", APIVersion:"v1", ResourceVersion:"108817764", FieldPath:""}): type: 'Normal' reason: 'Updated' MGMT ConfigMap nginx-ingress/nic-nginx-ingress-mgmt updated without error
2026/04/09 15:57:09 [notice] 15#15: signal 17 (SIGCHLD) received from 20
2026/04/09 15:57:09 [notice] 15#15: worker process 20 exited with code 0
2026/04/09 15:57:09 [notice] 15#15: signal 29 (SIGIO) received
2026/04/09 15:57:09 [notice] 15#15: signal 17 (SIGCHLD) received from 21
2026/04/09 15:57:09 [notice] 15#15: worker process 21 exited with code 0
2026/04/09 15:57:09 [notice] 15#15: signal 29 (SIGIO) received
BD_MISC|NOTICE|Apr 09 15:57:25.411|0036|/builds/t1_xXBa_N/12/waf/waf-general/secore/bd/bd/temp_func.c:2874|UMU: 0 0 || 0 0 0 0 0 0 0 0 0 0 0 0 || 0 0 0 0 0 0 0
BD_MISC|NOTICE|Apr 09 15:57:25.411|0036|/builds/t1_xXBa_N/12/waf/waf-general/secore/bd/bd/temp_func.c:2875|UMU: total 0 ( 0Kb) VM (486M) RSS ( 49M) SWAP ( 0M) Cache (0) trans 0
- Check Kubernetes service status
kubectl get svc -n nginx-ingress
NGINX Ingress Controller should be listening on TCP ports 80 and 443
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nic-nginx-ingress-controller NodePort 10.111.219.126 <none> 80:30106/TCP,443:31131/TCP 4m25s
- Check the
ingressclass
kubectl get ingressclass
The nginx ingressclass should be available
NAME CONTROLLER PARAMETERS AGE
nginx nginx.org/ingress-controller <none> 4m45s
- Clone this repository and
cdinto it
git clone https://github.com/f5devcentral/NGINX-Ingress-Controller-Lab.git
cd NGINX-Ingress-Controller-Lab
- List available F5 WAF for NGINX compiler versions
curl -s https://private-registry.nginx.com/v2/nap/waf-compiler/tags/list --key <nginx-one-eval.key> --cert <nginx-one-eval.crt> | jq
- Set up Docker to authenticate to the private registry
sudo mkdir -p /etc/docker/certs.d/private-registry.nginx.com
sudo cp <nginx-one-eval.crt> /etc/docker/certs.d/private-registry.nginx.com/client.cert
sudo cp <nginx-one-eval.key> /etc/docker/certs.d/private-registry.nginx.com/client.key
- Build the F5 WAF for NGINX compiler (
5.11.2at the time of writing):
docker build -f deployment/Dockerfile --no-cache --platform linux/amd64 \
--secret id=nginx-crt,src=<nginx-one-eval.crt> \
--secret id=nginx-key,src=<nginx-one-eval.key> \
-t waf-compiler-5.11.2:custom .
- The output should be similar to
[+] Building 67.8s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.36kB 0.0s
=> resolve image config for docker-image://docker.io/docker/dockerfile:1 0.7s
=> [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:b6afd42430b15f2d2a4c5a02b919e98a525b785b1aaff16747d2f623364e39b6 0.0s
=> [internal] load metadata for private-registry.nginx.com/nap/waf-compiler:5.11.2 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [stage-0 1/2] FROM private-registry.nginx.com/nap/waf-compiler:5.11.2@sha256:aee2af5e9b7a8e2f6b5e3b37e42623514d97470ee033fed514aad61182e0bc94 0.0s
=> [stage-0 2/2] RUN --mount=type=secret,id=nginx-crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 --mount=type=secret,id=nginx-key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 ap 65.9s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:4dcfcaac1acad8934a193b86c6b46152e72db1fa99cdd81db74d9a7996820792 0.0s
=> => naming to docker.io/library/waf-compiler-5.11.2:custom 0.0s
- Uninstall NGINX Ingress Controller through its Helm chart
helm uninstall nic -n nginx-ingress
- Delete the namespace
kubectl delete namespace nginx-ingress
- Delete custom resources
kubectl delete -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.4.1/deploy/crds.yaml
kubectl delete -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.4.1/deploy/crds-nap-waf.yaml