Install NGINX Ingress Controller and F5 WAF for NGINX with Docker and Helm
This document describes how to build a local F5 WAF for NGINX v5 Docker image with NGINX Plus Ingress Controller, which can be used to compile WAF policies.
This is accomplished with the following steps:
- Prepare license secrets to enable a Kubernetes deployment
- Use a F5 WAF for NGINX Docker image to transform a policy JSON file into a compiled bundle
- Configure PersistentVolumes so the deployed F5 WAF for NGINX instance can access the compiled bundle
- Deploy NGINX Plus Ingress Controller with NGINX App Protect
- Test example services to validate that the WAF policies work
- Download your NGINX Ingress Controller subscription’s JSON Web Token, SSL Certificate, and Private Key from MyF5. You can use the same JSON Web Token, Certificate, and Key as NGINX Plus in your MyF5 portal.
- Rename the files to the following:
nginx-repo.crt
nginx-repo.key
nginx-repo.jwt
- Log in to the Docker registry using the contents of the JSON Web Token file:
docker login private-registry.nginx.com --username=$(cat nginx-repo.jwt) --password=none
The following table shows compatibility between NGINX Ingress Controller (NIC) and F5 WAF for NGINX (NAP-WAF) versions:
NIC Version | NAP-WAF Version | Config Manager | Enforcer |
---|---|---|---|
5.2.0 | 35+5.498 | 5.8.0 | 5.8.0 |
5.1.1 | 35+5.498 | 5.8.0 | 5.8.0 |
5.0.0 | 34+5.342 | 5.6.0 | 5.6.0 |
4.0.1 | 33+5.264 | 5.5.0 | 5.5.0 |
3.7.2 | 32+5.1 | 5.3.0 | 5.3.0 |
3.6.2 | 32+5.48 | 5.2.0 | 5.2.0 |
Pull the waf-compiler
image with:
docker pull private-registry.nginx.com/nap/waf-compiler:5.8.0
Download the provided WAF Policy JSON:
curl -L https://raw.githubusercontent.com/nginx/kubernetes-ingress/main/tests/data/ap-waf-v5/wafv5.json -o /tmp/wafv5.json
Use your pulled NAP Docker image (private-registry.nginx.com/nap/waf-compiler:5.8.0
) to compile the policy bundle:
# Using your newly created image docker run --rm \ -v /tmp:/tmp \ private-registry.nginx.com/nap/waf-compiler:5.8.0 \ -p /tmp/wafv5.json \ -o /tmp/compiled_policy.tgz
Move the downloaded JSON and compiled policy to your workspace:
mv /tmp/wafv5.json $(pwd)/wafv5.json mv /tmp/compiled_policy.tgz $(pwd)/compiled_policy.tgz
After this command, your workspace should contain:
├── nginx-repo.crt ├── nginx-repo.key ├── nginx-repo.jwt ├── wafv5.json └── compiled_policy.tgz
Save the following configuration data as pvc.yaml
in the same directory.
apiVersion: v1 kind: PersistentVolume metadata: name: task-pv-volume labels: type: local spec: storageClassName: manual capacity: storage: 1Gi accessModes: - ReadWriteOnce hostPath: path: "/tmp/" --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: task-pv-claim spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
This sets up a 1Gi disk and attaches a claim to it that you will reference in the deployment chart.
Create these with:
kubectl apply -f pvc.yaml
Verify that the persistent volume and claim are created:
# For the persistent volume kubectl get pv # For the persistent volume claim kubectl get pvc
Add the official NGINX Helm repository:
helm repo add nginx-stable https://helm.nginx.com/stable helm repo update
Create Kubernetes Docker and licensing secrets:
kubectl create secret \ docker-registry regcred \ --docker-server=private-registry.nginx.com \ --docker-username=$(cat nginx-repo.jwt) \ --docker-password=none kubectl create secret \ generic license-token \ --from-file=license.jwt=./nginx-repo.jwt \ --type=nginx.com/license
Install the required CRDs for NGINX Ingress Controller:
kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.2.0/deploy/crds.yaml
Using Helm, install NGINX Ingress Controller
helm upgrade --install nic nginx-stable/nginx-ingress \ --set controller.image.repository="private-registry.nginx.com/nginx-ic-nap-v5/nginx-plus-ingress" \ --set controller.image.tag="5.2.0-alpine-fips" \ --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.serviceAccount.imagePullSecretName=regcred \ --set 'controller.volumeMounts[0].name=app-protect-bundles' \ --set 'controller.volumeMounts[0].mountPath=/etc/app_protect/bundles/'
Verify deployment success:
kubectl get pods
Get the name of the pod from the kubectl get pods
command above.
Copy the file into the nginx-ingress
container within the pod:
kubectl cp ./compiled_policy.tgz \ <pod name>:/etc/app_protect/bundles/compiled_policy.tgz \ -c nginx-ingress
Replace <pod name>
with the actual name of the pod, for example:
kubectl cp ./compiled_policy.tgz \ nic-nginx-ingress-controller-9bd89589d-j925h:/etc/app_protect/bundles/compiled_policy.tgz \ -c nginx-ingress
Confirm that the policy file is in the pod. The following command should list compiled_policy.tgz
.
kubectl exec --stdin --tty \ -c nginx-ingress \ <pod name> \ -- ls -la /etc/app_protect/bundles
Save the following kubernetes config file as webapp.yaml
:
apiVersion: k8s.nginx.org/v1 kind: VirtualServer metadata: name: webapp spec: host: webapp.example.com policies: - name: waf-policy upstreams: - name: webapp service: webapp-svc port: 80 routes: - path: / action: pass: webapp --- apiVersion: k8s.nginx.org/v1 kind: Policy metadata: name: waf-policy spec: waf: enable: true apBundle: "compiled_policy.tgz" --- apiVersion: apps/v1 kind: Deployment metadata: name: webapp spec: replicas: 1 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: webapp image: nginxdemos/nginx-hello:plain-text ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: webapp-svc spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: webapp
Create the services with
kubectl apply -f webapp.yaml
Confirm that the services have started with
kubectl get pods
Get the public IP and port of your instance with the following command:
kubectl get svc
Save them in the following environment variables:
IC_IP=XXX.YYY.ZZZ.III IC_HTTP_PORT=<port number>
Send a valid request to the deployed application:
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/
Server address: 10.92.2.13:8080 Server name: webapp-7b7dfbff54-dtxzt Date: 18/Apr/2025:19:39:18 +0000 URI: / Request ID: 4f378a01fb8a36ae27e2c3059d264527
And send one that should be rejected
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP "http://webapp.example.com:$IC_HTTP_PORT/<script>"
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 11241918873745059631<br><br> <a href='javascript:history.back();'>[Go Back]</a></body></html>
This is mostly the same as the examples/custom_resources/app-protect-waf-v5 deployment in a single file with the policy bundle already set.
You now have a fully operational NGINX Ingress Controller instance with NGINX App Protect deployed in your Kubernetes environment.
For further details, troubleshooting, or support, refer to the official NGINX documentation or reach out directly to your F5/NGINX account team.