Configure F5 WAF for NGINX for NGINX Gateway Fabric
This document describes how to configure F5 NGINX Gateway Fabric to enable integration with F5 WAF for NGINX and protect your application traffic.
You need:
- Administrator access to a Kubernetes cluster.
- Helm and kubectl installed locally.
- NGINX Plus is required for F5 WAF support. You need valid NGINX Plus credentials to pull images from
private-registry.nginx.com. - A valid F5 WAF for NGINX license.
F5 WAF support requires NGINX Plus. Before installing NGINX Gateway Fabric, you need to configure your NGINX Plus credentials as Kubernetes Secrets. See Install NGINX Gateway Fabric with NGINX Plus for full details. The steps below summarize what is required.
Create these Secrets in thenginx-gatewaynamespace. If the namespace does not yet exist, create it first withkubectl create namespace nginx-gateway.
- Log in to MyF5.
- Go to My Products & Plans > Subscriptions to see your active subscriptions.
- Find your NGINX products or services subscription, and select the Subscription ID for details.
- Download the JSON Web Token (JWT) from the subscription page.
The Connectivity Stack for Kubernetes JWT does not work with NGINX Plus reporting. A regular NGINX Plus instance JWT must be used.
If you would rather pull the NGINX Plus image and push to a private registry, you can skip this specific step and instead follow this step.
If the nginx-gateway namespace does not yet exist, create it:
kubectl create namespace nginx-gatewayCreate a Kubernetes docker-registry secret type using the contents of the JWT as the username and none for password (as the password is not used). The name of the docker server is private-registry.nginx.com.
kubectl create secret docker-registry nginx-plus-registry-secret --docker-server=private-registry.nginx.com --docker-username=<JWT Token> --docker-password=none -n nginx-gatewayIt is important that the --docker-username=<JWT Token> contains the contents of the token and is not pointing to the token itself.
When you copy the contents of the JWT, ensure there are no additional characters such as extra whitespaces.
This can invalidate the token, causing 401 errors when trying to authenticate to the registry.
Place the JWT in a file called license.jwt. Create a Kubernetes Secret using the contents of the JWT file.
kubectl create secret generic nplus-license --from-file license.jwt -n nginx-gatewayYou can now delete the license.jwt file.
If you need to update the JWT at any time, update the license.jwt field in the Secret using kubectl edit and apply the changes.
F5 WAF for NGINX relies on the Policy Lifecycle Manager (PLM), which compiles WAF policies and stores them in an in-cluster S3-compatible object store (SeaweedFS). NGINX Gateway Fabric includes PLM as an optional subchart.
PLM’s internal SeaweedFS storage requires TLS certificates. The following steps use cert-manager to generate self-signed certificates as a convenient starting point. In production environments, you can manage TLS secrets with your own certificate infrastructure and skip this section, provided you create secrets with the following names in the nginx-gateway namespace:
| Secret name | Purpose |
|---|---|
ngf-f5-waf-seaweedfs-ca-cert |
CA certificate |
ngf-f5-waf-seaweedfs-master-cert |
SeaweedFS master TLS |
ngf-f5-waf-seaweedfs-volume-cert |
SeaweedFS volume TLS |
ngf-f5-waf-seaweedfs-filer-cert |
SeaweedFS filer TLS |
ngf-f5-waf-seaweedfs-client-cert |
Client mTLS certificate |
The secret names are derived from your Helm release name (ngf in this guide). If you use a different release name, adjust the names accordingly: {release-name}-f5-waf-seaweedfs-{component}.
-
Install cert-manager:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.4/cert-manager.yamlWait for the cert-manager pods to be ready before continuing:
kubectl wait --for=condition=Available deployment --all -n cert-manager --timeout=120s -
Apply the SeaweedFS certificate resources. This creates a self-signed CA Issuer and TLS certificates for the SeaweedFS master, volume, filer, and client components:
yaml kubectl apply -f - <<EOF apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: nginx-gateway-f5-waf-seaweedfs-issuer namespace: nginx-gateway spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nginx-gateway-f5-waf-seaweedfs-ca-cert namespace: nginx-gateway spec: secretName: nginx-gateway-f5-waf-seaweedfs-ca-cert commonName: "seaweedfs-root-ca" isCA: true issuerRef: name: nginx-gateway-f5-waf-seaweedfs-issuer kind: Issuer duration: 87600h # 10 years renewBefore: 720h # 30 days --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: nginx-gateway-f5-waf-seaweedfs-ca-issuer namespace: nginx-gateway spec: ca: secretName: nginx-gateway-f5-waf-seaweedfs-ca-cert --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nginx-gateway-f5-waf-seaweedfs-master-cert namespace: nginx-gateway spec: secretName: nginx-gateway-f5-waf-seaweedfs-master-cert issuerRef: name: nginx-gateway-f5-waf-seaweedfs-ca-issuer kind: Issuer commonName: "SeaweedFS CA" dnsNames: - '*.nginx-gateway' - '*.nginx-gateway.svc' - '*.nginx-gateway.svc.cluster.local' - '*.seaweedfs-master' - '*.seaweedfs-master.nginx-gateway' - '*.seaweedfs-master.nginx-gateway.svc' - '*.seaweedfs-master.nginx-gateway.svc.cluster.local' privateKey: algorithm: RSA size: 2048 duration: 2160h # 90 days renewBefore: 360h # 15 days --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nginx-gateway-f5-waf-seaweedfs-volume-cert namespace: nginx-gateway spec: secretName: nginx-gateway-f5-waf-seaweedfs-volume-cert issuerRef: name: nginx-gateway-f5-waf-seaweedfs-ca-issuer kind: Issuer commonName: "SeaweedFS CA" dnsNames: - '*.nginx-gateway' - '*.nginx-gateway.svc' - '*.nginx-gateway.svc.cluster.local' - '*.seaweedfs-volume' - '*.seaweedfs-volume.nginx-gateway' - '*.seaweedfs-volume.nginx-gateway.svc' - '*.seaweedfs-volume.nginx-gateway.svc.cluster.local' privateKey: algorithm: RSA size: 2048 duration: 2160h renewBefore: 360h --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nginx-gateway-f5-waf-seaweedfs-filer-cert namespace: nginx-gateway spec: secretName: nginx-gateway-f5-waf-seaweedfs-filer-cert issuerRef: name: nginx-gateway-f5-waf-seaweedfs-ca-issuer kind: Issuer commonName: "SeaweedFS CA" dnsNames: - '*.nginx-gateway' - '*.nginx-gateway.svc' - '*.nginx-gateway.svc.cluster.local' - '*.seaweedfs-filer' - '*.seaweedfs-filer.nginx-gateway' - '*.seaweedfs-filer.nginx-gateway.svc' - '*.seaweedfs-filer.nginx-gateway.svc.cluster.local' privateKey: algorithm: RSA size: 2048 duration: 2160h renewBefore: 360h --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: nginx-gateway-f5-waf-seaweedfs-client-cert namespace: nginx-gateway spec: secretName: nginx-gateway-f5-waf-seaweedfs-client-cert issuerRef: name: nginx-gateway-f5-waf-seaweedfs-ca-issuer kind: Issuer commonName: "SeaweedFS CA" dnsNames: - '*.nginx-gateway' - '*.nginx-gateway.svc' - '*.nginx-gateway.svc.cluster.local' - client privateKey: algorithm: RSA size: 2048 duration: 2160h renewBefore: 360h EOFThe example uses a self-signed CA. For production use, replace these with certificates from your own CA or certificate management solution.
Install NGINX Gateway Fabric with the PLM subchart enabled. When f5-waf-plm.enabled=true, Helm automatically configures the control plane to connect to PLM’s SeaweedFS storage — no additional plmStorage values are required.
helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
--namespace nginx-gateway \
--create-namespace \
--set nginx.plus=true \
--set nginx.imagePullSecret=nginx-plus-registry-secret \
--set f5-waf-plm.enabled=true \
--set f5-waf-plm.imagePullSecrets[0]=nginx-plus-registry-secret \
--set f5-waf-plm.seaweedfsOperatorConfig.seaweedfs.certificates.enabled=trueThe certificates.enabled=true flag tells PLM to use the TLS secrets created in the previous step.
Verify that the NGF and PLM pods are running:
kubectl get pods -n nginx-gatewayDeploy a syslog server to receive WAF security event logs, and the sample cafe application. The coffee service is configured to return sensitive data (credit card numbers and SSNs) in its responses — this is intentional, to demonstrate WAF blocking behaviour.
-
Deploy the syslog server:
yaml kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: syslog spec: replicas: 1 selector: matchLabels: app: syslog template: metadata: labels: app: syslog spec: containers: - name: syslog image: balabit/syslog-ng:4.11.0 ports: - containerPort: 514 --- apiVersion: v1 kind: Service metadata: name: syslog-svc spec: ports: - port: 514 targetPort: 514 protocol: TCP selector: app: syslog EOF -
Deploy the cafe application:
yaml kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: coffee spec: replicas: 1 selector: matchLabels: app: coffee template: metadata: labels: app: coffee spec: containers: - name: coffee image: hashicorp/http-echo:latest args: - "-listen=:8080" - "-text=Welcome to Coffee Shop!\nCustomer: John Doe\nCredit Card: 4111-1111-1111-1111\nSSN: 123-45-6789\n" ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: coffee spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: coffee --- apiVersion: apps/v1 kind: Deployment metadata: name: tea spec: replicas: 1 selector: matchLabels: app: tea template: metadata: labels: app: tea spec: containers: - name: tea image: nginxdemos/nginx-hello:plain-text ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: tea spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: tea EOF
Create the APLogConf and APPolicy resources that define your WAF policy.
-
Create the log configuration, which defines the format and content of WAF security logs:
yaml kubectl apply -f - <<EOF apiVersion: appprotect.f5.com/v1 kind: APLogConf metadata: name: logconf spec: content: format: default max_message_size: 64k max_request_size: any filter: request_type: all EOF -
Create the WAF policy. This
APPolicyenables Data Guard, which blocks responses containing credit card numbers and US Social Security Numbers:yaml kubectl apply -f - <<EOF apiVersion: appprotect.f5.com/v1 kind: APPolicy metadata: name: dataguard-blocking spec: policy: name: dataguard_blocking template: name: POLICY_TEMPLATE_NGINX_BASE applicationLanguage: utf-8 enforcementMode: blocking blocking-settings: violations: - name: VIOL_DATA_GUARD alarm: true block: true data-guard: enabled: true maskData: true creditCardNumbers: true usSocialSecurityNumbers: true enforcementMode: ignore-urls-in-list enforcementUrls: [] EOF
-
Create an
NginxProxyresource with WAF enabled. Settingwaf: "enabled"instructs NGINX Gateway Fabric to inject the WAF enforcer and config manager sidecar containers into the NGINX data plane pod when a Gateway references this proxy configuration:yaml kubectl apply -f - <<EOF apiVersion: gateway.nginx.org/v1alpha2 kind: NginxProxy metadata: name: waf-enabled-proxy spec: waf: "enabled" EOF -
Create the
Gateway, referencing the WAF-enabledNginxProxyviainfrastructure.parametersRef:yaml kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: gateway spec: gatewayClassName: nginx infrastructure: parametersRef: name: waf-enabled-proxy group: gateway.nginx.org kind: NginxProxy listeners: - name: http port: 80 protocol: HTTP hostname: "*.example.com" EOFAfter creating the Gateway resource, NGINX Gateway Fabric will provision an NGINX Pod and Service to route traffic. Because the
NginxProxyhaswaf: "enabled", the NGINX Pod will include two additional WAF sidecar containers alongside the main NGINX container:- waf-enforcer: enforces WAF policies on traffic passing through NGINX.
- waf-config-mgr: manages the local WAF policy configuration, tracking which compiled policy bundles are available and providing the enforcer with the information it needs to apply them.
Verify the gateway is created and the status shows
Accepted:kubectl describe gateways.gateway.networking.k8s.io gatewaytext Status: Addresses: Type: IPAddress Value: 10.12.13.141 Conditions: Last Transition Time: 2026-03-12T15:16:03Z Message: The Gateway is accepted Observed Generation: 1 Reason: Accepted Status: True Type: Accepted Last Transition Time: 2026-03-12T15:16:03Z Message: The Gateway is programmed Observed Generation: 1 Reason: Programmed Status: True Type: Programmed Last Transition Time: 2026-03-12T15:16:03Z Message: The ParametersRef resource is resolved Observed Generation: 1 Reason: ResolvedRefs Status: True Type: ResolvedRefsSave the public IP address and port of the Gateway into shell variables:
shell export GW_IP=XXX.YYY.ZZZ.III export GW_PORT=<http port number>In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for. -
Create the
HTTPRouteresources for the coffee and tea services:yaml kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: coffee spec: parentRefs: - name: gateway sectionName: http hostnames: - "cafe.example.com" rules: - matches: - path: type: PathPrefix value: /coffee backendRefs: - name: coffee port: 80 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: tea spec: parentRefs: - name: gateway sectionName: http hostnames: - "cafe.example.com" rules: - matches: - path: type: Exact value: /tea backendRefs: - name: tea port: 80 EOF
Before applying the WAF binding policy, confirm that the coffee service responds with sensitive data.
Send a request to the coffee service:
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffeeThe response contains the sensitive data that the WAF policy will block:
Welcome to Coffee Shop!
Customer: John Doe
Credit Card: 4111-1111-1111-1111
SSN: 123-45-6789The WAFGatewayBindingPolicy binds the compiled APPolicy to a Gateway and configures where WAF security logs are sent.
kubectl apply -f - <<EOF
apiVersion: gateway.nginx.org/v1alpha1
kind: WAFGatewayBindingPolicy
metadata:
name: gateway-base-protection
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gateway
apPolicySource:
name: dataguard-blocking
securityLogs:
- apLogConfSource:
name: logconf
destination:
type: syslog
syslog:
server: syslog-svc.default.svc.cluster.local:514
EOFVerify that the policy is accepted and programmed in the data plane:
kubectl describe wafgatewaybindingpolicy gateway-base-protectionThe status conditions should show all three conditions set to True:
Status:
Ancestors:
Ancestor Ref:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: gateway
Namespace: default
Conditions:
Last Transition Time: 2026-03-12T15:16:03Z
Message: The Policy is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Last Transition Time: 2026-03-12T15:16:03Z
Message: All references are resolved
Observed Generation: 1
Reason: ResolvedRefs
Status: True
Type: ResolvedRefs
Last Transition Time: 2026-03-12T15:16:03Z
Message: Policy is programmed in the data plane
Observed Generation: 1
Reason: Programmed
Status: True
Type: Programmed
Controller Name: gateway.nginx.org/nginx-gateway-controller
Events: <none>Send another request to the coffee service. The WAF policy now blocks the response containing sensitive data:
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffeeThe request is rejected by the WAF:
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 11294711299894599313<br><br><a href='javascript:history.back();'>[Go Back]</a></body></html>The WAF Data Guard policy has successfully blocked the response containing the credit card number and SSN. Security log events are forwarded to the syslog server deployed earlier.
TODO:
- Add AP* API reference for full configuration options
- Detail how to configure signature updates (including needing nginx.crt and nginx.key for pulling updates)
- Document how to configure policies from external resources, if supported, otherwise add note that they are not yet supported as these options are in the CRD