Connect F5 WAF for NGINX to NGINX One Console, NGINX Instance Manager, or HTTPS bundle sources

This document explains how to configure NGINX Ingress Controller to fetch pre-compiled F5 WAF for NGINX policy bundles from a remote source, instead of manually placing bundles on disk.

You can fetch bundles from:

  • NGINX One Console — for policies compiled and managed through NGINX One Console
  • NGINX Instance Manager — for policies compiled and managed through NGINX Instance Manager
  • HTTPS — for compiled .tgz bundles hosted on any HTTPS server
Bundle sources require F5 WAF for NGINX v5 and work with VirtualServer custom resources only. The deprecated securityLog (singular) field does not support bundle sources — use securityLogs instead.
There are complete NGINX Ingress Controller with F5 WAF for NGINX bundle source example resources on GitHub.

Important
NGINX Ingress Controller does not trigger compilation. Compilation happens when a policy is published in NGINX One Console. Ensure the policy has been published and a compiled bundle is available before continuing.

NGINX One Console uses APIToken authentication. Create a Secret of type nginx.com/waf-bundle with a token key containing your API token.

See the example Secret manifests in the NGINX Ingress Controller repository for the required format.

Create a Policy resource using apBundleSource with type: N1C:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: N1C
      url: "https://<tenant>.console.ves.volterra.io"
      policyName: "my-blocking-policy"
      policyNamespace: "default"
      secret: "n1c-credentials"
      enablePolling: true
      pollInterval: "5m"
EOF

Replace <tenant> with your NGINX One Console tenant hostname, policyName with the name of your published policy, and policyNamespace with the NGINX One Console namespace where the policy resides.

Caution
To skip TLS verification for testing, add insecureSkipVerify: true to the bundle source. Do not use this in production.

Reference the WAF Policy in your VirtualServer:

yaml
kubectl apply -f - <<EOF
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
EOF

  1. Check the Policy events for a successful fetch:

    kubectl describe policy waf-policy

    Look for a Normal event confirming the bundle was fetched. If you see a Warning event, check the message for the cause — common issues include an incorrect policyName, an invalid token, or a policy that has not been published yet.

  2. Send a legitimate request to confirm traffic flows normally:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      http://webapp.example.com:$IC_HTTP_PORT/
  3. Send a malicious request to confirm WAF is blocking:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      "http://webapp.example.com:$IC_HTTP_PORT/<script>"

    Expected response:

    <html><head><title>Request Rejected</title></head><body>

If the VirtualServer returns HTTP 500, the bundle has not been fetched yet. Check the Policy events and status for errors.

When enablePolling: true is set, NGINX Ingress Controller periodically checks whether a new bundle is available. For NGINX One Console, it uses a compile status hash — the full bundle is only downloaded when a new compilation is detected.

Check that polls are running without error:

kubectl describe policy waf-policy

Look for recent Normal events that confirm a poll completed. A Warning event means the last poll failed, but the existing bundle remains active — WAF protection is not interrupted.

To adjust the poll interval on an existing Policy:

shell
kubectl patch policy waf-policy --type merge -p '{
  "spec": {"waf": {"apBundleSource": {"pollInterval": "10m"}}}
}'

pollInterval must be at least 1m. It defaults to 5m if not set.

Security log profile bundles can also be fetched from NGINX One Console using apLogBundleSource in securityLogs[]:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: N1C
      url: "https://<tenant>.console.ves.volterra.io"
      policyName: "my-blocking-policy"
      policyNamespace: "default"
      secret: "n1c-credentials"
      enablePolling: true
      pollInterval: "5m"
    securityLogs:
    - enable: true
      apLogBundleSource:
        type: N1C
        url: "https://<tenant>.console.ves.volterra.io"
        policyName: "secops_dashboard"
        policyNamespace: "default"
        secret: "n1c-credentials"
        enablePolling: true
        pollInterval: "5m"
      logDest: "syslog:server=syslog-svc.default:514"
EOF

Verify log events are arriving at your syslog destination:

kubectl exec -it <SYSLOG_POD> -- cat /var/log/messages

Important
NGINX Ingress Controller does not trigger compilation. Compile the policy using the NGINX Instance Manager UI or POST /api/platform/v1/security/policies/bundles and verify compilation succeeded before continuing.

NGINX Instance Manager requires a Secret of type nginx.com/waf-bundle. The Secret must contain either a token key (bearer auth) or username + password keys (basic auth).

See the example Secret manifests in the NGINX Ingress Controller repository for the required format.

Create a Policy resource using apBundleSource with type: NIM:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: NIM
      url: "https://nim.example.com"
      policyName: "my-blocking-policy"
      secret: "nim-credentials"
      enablePolling: true
      pollInterval: "5m"
EOF

Replace url with your NGINX Instance Manager base URL and policyName with the name of your compiled policy.

Caution
To skip TLS verification for testing, add insecureSkipVerify: true to the bundle source. Do not use this in production.

Reference the WAF Policy in your VirtualServer:

yaml
kubectl apply -f - <<EOF
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
EOF

  1. Check the Policy events for a successful fetch:

    kubectl describe policy waf-policy

    Look for a Normal event confirming the bundle was fetched. If you see a Warning event, check the message for the cause — common issues include an incorrect policyName, authentication failure, or a bundle that has not been compiled yet.

  2. Send a legitimate request to confirm traffic flows normally:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      http://webapp.example.com:$IC_HTTP_PORT/
  3. Send a malicious request to confirm WAF is blocking:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      "http://webapp.example.com:$IC_HTTP_PORT/<script>"

    Expected response:

    <html><head><title>Request Rejected</title></head><body>

If the VirtualServer returns HTTP 500, the bundle has not been fetched yet. Check the Policy events and status for errors.

When enablePolling: true is set, NGINX Ingress Controller periodically checks whether a new bundle is available. For NGINX Instance Manager, it uses a metadata hash comparison — the full bundle is only downloaded when the hash has changed.

Check that polls are running without error:

kubectl describe policy waf-policy

Look for recent Normal events that confirm a poll completed. A Warning event means the last poll failed, but the existing bundle remains active — WAF protection is not interrupted.

To adjust the poll interval on an existing Policy:

shell
kubectl patch policy waf-policy --type merge -p '{
  "spec": {"waf": {"apBundleSource": {"pollInterval": "10m"}}}
}'

pollInterval must be at least 1m. It defaults to 5m if not set.

Security log profile bundles can also be fetched from NGINX Instance Manager using apLogBundleSource in securityLogs[]:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: NIM
      url: "https://nim.example.com"
      policyName: "my-blocking-policy"
      secret: "nim-credentials"
      enablePolling: true
      pollInterval: "5m"
    securityLogs:
    - enable: true
      apLogBundleSource:
        type: NIM
        url: "https://nim.example.com"
        policyName: "secops_dashboard"
        secret: "nim-credentials"
        enablePolling: true
        pollInterval: "5m"
      logDest: "syslog:server=syslog-svc.default:514"
EOF

Verify log events are arriving at your syslog destination:

kubectl exec -it <SYSLOG_POD> -- cat /var/log/messages

The url field must point directly to the compiled .tgz bundle file — for example, https://bundles.example.com/waf/my-policy.tgz. NGINX Ingress Controller downloads the file at this URL and does not follow redirects (3xx responses are treated as errors for SSRF protection).

You can host bundles on any HTTPS-capable server:

  • In-cluster bundle server — Deploy an NGINX-based server inside your cluster that serves compiled bundles over HTTPS. For an example deployment, see the bundle server manifest in the NGINX Ingress Controller repository.
  • Object storage — Use S3, GCS, Azure Blob Storage, or another object store with HTTPS access.
  • Artifact registry — Serve bundles from a CI/CD artifact repository or container registry with download URLs.
  • Static file server — Any HTTPS server (NGINX, Apache, Caddy) that can serve .tgz files.

After compiling your policy with the F5 WAF compiler, upload the .tgz file to your server and note the full URL.

Skip this step if your HTTPS server uses a publicly trusted certificate.

  • Custom CA certificate — If your server uses a self-signed or internal CA, create a Secret of type nginx.org/ca with a ca.crt key, and reference it in trustedCertSecret.
  • Client mTLS — Create a kubernetes.io/tls Secret with tls.crt and tls.key, and reference it in secret.

See the example Secret manifests in the NGINX Ingress Controller repository for the required format.

Create a Policy resource using apBundleSource with type: HTTPS. The url must be the full path to the .tgz bundle file:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: HTTPS
      url: "https://bundles.example.com/waf/my-policy.tgz"
      trustedCertSecret: "bundle-ca-cert"
      enablePolling: false
EOF

Replace url with the full URL of your compiled bundle. Remove trustedCertSecret if your server uses a publicly trusted certificate.

Caution
To skip TLS verification for testing, add insecureSkipVerify: true to the bundle source. Do not use this in production.

Reference the WAF Policy in your VirtualServer:

yaml
kubectl apply -f - <<EOF
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
EOF

  1. Check the Policy events for a successful fetch:

    kubectl describe policy waf-policy

    Look for a Normal event confirming the bundle was fetched. If you see a Warning event, check the message for the cause — common issues include an unreachable URL or a TLS certificate error.

  2. Send a legitimate request to confirm traffic flows normally:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      http://webapp.example.com:$IC_HTTP_PORT/
  3. Send a malicious request to confirm WAF is blocking:

    shell
    curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \
      "http://webapp.example.com:$IC_HTTP_PORT/<script>"

    Expected response:

    <html><head><title>Request Rejected</title></head><body>

If the VirtualServer returns HTTP 500, the bundle has not been fetched yet. Check the Policy events and status for errors.

Polling lets NGINX Ingress Controller detect and deploy updated bundles without modifying the Policy resource. For HTTPS sources, NGINX Ingress Controller uses ETag and If-Modified-Since headers — a 304 Not Modified response skips the download entirely.

Enable polling on the existing Policy:

shell
kubectl patch policy waf-policy --type merge -p '{
  "spec": {"waf": {"apBundleSource": {"enablePolling": true, "pollInterval": "10m"}}}
}'

pollInterval must be at least 1m. It defaults to 5m if not set.

Set verifyChecksum: true to have NGINX Ingress Controller fetch a companion <url>.sha256 file and compare the SHA-256 digest against the downloaded bundle. The bundle is rejected if the digest does not match.

  1. Generate the checksum file alongside your bundle:

    sha256sum my-policy.tgz > my-policy.tgz.sha256
  2. Upload both files to the same location on your HTTPS server.

  3. Update the Policy to enable verification:

    shell
    kubectl patch policy waf-policy --type merge -p '{
      "spec": {"waf": {"apBundleSource": {"verifyChecksum": true}}}
    }'

NGINX Ingress Controller appends .sha256 to the bundle URL automatically.

verifyChecksum is only supported for HTTPS sources. NGINX Instance Manager and NGINX One Console sources use native integrity checks.

Security log profile bundles can also be fetched from a remote source using apLogBundleSource in securityLogs[]:

yaml
kubectl apply -f - <<EOF
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: waf-policy
spec:
  waf:
    enable: true
    apBundleSource:
      type: HTTPS
      url: "https://bundles.example.com/waf/my-policy.tgz"
      enablePolling: true
      pollInterval: "5m"
    securityLogs:
    - enable: true
      apLogBundleSource:
        type: HTTPS
        url: "https://bundles.example.com/waf/my-log-profile.tgz"
        enablePolling: true
        pollInterval: "5m"
      logDest: "syslog:server=syslog-svc.default:514"
EOF

Verify log events are arriving at your syslog destination:

kubectl exec -it <SYSLOG_POD> -- cat /var/log/messages

Failure handling and recovery

Initial fetch failure

When a bundle cannot be fetched on the first attempt:

  • A Warning event is emitted on the Policy resource.
  • The Policy status is updated with the error details.
  • Any VirtualServer referencing the Policy returns HTTP 500 until the bundle arrives.

Check the events for details:

kubectl describe policy waf-policy

Recovery

Update the Policy with a corrected URL, credentials, or policyName. NGINX Ingress Controller detects the change and retries immediately. Once the bundle is fetched, WAF protection becomes active and the VirtualServer stops returning 500.

Stale bundles

If polling is enabled and a poll cycle fails after a previous successful fetch, the existing bundle remains active. WAF protection continues without interruption. A Warning event is emitted, and NGINX Ingress Controller retries on the next poll cycle.


See also