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.crtnginx-repo.keynginx-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.1 | 35+5.527 | 5.9.0 | 5.9.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.9.0Download 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.jsonUse your pulled NAP Docker image (private-registry.nginx.com/nap/waf-compiler:5.9.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.9.0 \
-p /tmp/wafv5.json \
-o /tmp/compiled_policy.tgzMove the downloaded JSON and compiled policy to your workspace:
mv /tmp/wafv5.json $(pwd)/wafv5.json
mv /tmp/compiled_policy.tgz $(pwd)/compiled_policy.tgzAfter this command, your workspace should contain:
├── nginx-repo.crt
├── nginx-repo.key
├── nginx-repo.jwt
├── wafv5.json
└── compiled_policy.tgzSave 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: 1GiThis 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.yamlVerify that the persistent volume and claim are created:
# For the persistent volume
kubectl get pv
# For the persistent volume claim
kubectl get pvcAdd the official NGINX Helm repository:
helm repo add nginx-stable https://helm.nginx.com/stable
helm repo updateCreate 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/licenseInstall the required CRDs for NGINX Ingress Controller:
kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v5.2.1/deploy/crds.yamlUsing 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.1-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 podsGet 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-ingressReplace <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-ingressConfirm 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/bundlesSave 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: webappCreate the services with
kubectl apply -f webapp.yamlConfirm that the services have started with
kubectl get podsGet the public IP and port of your instance with the following command:
kubectl get svcSave 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: 4f378a01fb8a36ae27e2c3059d264527And 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.