Configure remote F5 WAF for NGINX bundle sources

Overview

This guide explains how to configure NGINX Ingress Controller to fetch pre-compiled F5 WAF for NGINX policy bundles from a remote source for VirtualServer resources, instead of manually copying bundles on to the cluster.

This guide focuses on source-specific configuration details and validation steps.

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

Complete end-to-end NGINX Ingress Controller with F5 WAF for NGINX bundle source examples are available on GitHub: N1C and NIM examples and HTTPS bundle server files.

NGINX One Console

Before you begin

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.

Create a credentials Secret

Create a Secret of type nginx.com/waf-bundle in the same namespace as the Policy. The Secret must contain a token key with your NGINX One Console API token:

To create an API token, see Authentication.

shell
kubectl create secret generic n1c-credentials \
  --type=nginx.com/waf-bundle \
  --from-literal=token=<YOUR_API_TOKEN>

Create a WAF Policy

Create a Policy resource using apBundleSource with type: N1C:

yaml
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"

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.

The field name is policyName for both apBundleSource and apLogBundleSource. In apBundleSource, set it to the published WAF policy name. In apLogBundleSource, set it to the log profile name (for example, secops_dashboard).
Caution
To skip TLS verification for testing, add insecureSkipVerify: true to the bundle source. Do not use this in production.

Apply the policy to a VirtualServer

After waf-policy is created, apply a VirtualServer that references it in spec.policies.

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

For complete HTTPS setup manifests, see the bundle server files.

Verify the bundle was fetched

  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.

Confirm polling is working

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.

Add a security log bundle source (optional)

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

In your existing Policy, add this under spec:

yaml
waf:
  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"

Verify log events are arriving at your syslog destination:

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

NGINX Instance Manager

Before you begin

Important
NGINX Ingress Controller does not trigger compilation. Compile the policy in NGINX Instance Manager and verify compilation succeeded before continuing.

Create a credentials Secret

Create a Secret of type nginx.com/waf-bundle in the same namespace as the Policy. Use a token key for bearer auth, or username and password keys for basic auth:

If you use bearer auth, get an access token using your configured authentication flow. For supported methods, see API Overview.

shell
kubectl create secret generic nim-credentials \
  --type=nginx.com/waf-bundle \
  --from-literal=token=<YOUR_TOKEN>
shell
kubectl create secret generic nim-credentials \
  --type=nginx.com/waf-bundle \
  --from-literal=username=<YOUR_USERNAME> \
  --from-literal=password=<YOUR_PASSWORD>

Create a WAF Policy

Create a Policy resource using apBundleSource with type: NIM:

yaml
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"

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.

Apply the policy to a VirtualServer

After waf-policy is created, apply a VirtualServer that references it in spec.policies.

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

For complete end-to-end manifests, see the waf-management-plane examples.

Verify the bundle was fetched

  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.

Confirm polling is working

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.

Add a security log bundle source (optional)

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

In your existing Policy, add this under spec:

yaml
waf:
  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"

Verify log events are arriving at your syslog destination:

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

HTTPS

Before you begin

Host compiled bundles on an HTTPS server

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:

Host the compiled .tgz bundle at a direct HTTPS URL, for example:

  • https://bundles.example.com/waf/my-policy.tgz
  • https://storage.example.com/bundles/my-policy.tgz

Common options for URL endpoints:

  • Object storage (S3, GCS, Azure Blob)
  • Artifact registry with direct download URLs
  • Static file server (NGINX, Apache, Caddy)

Deploy an in-cluster bundle server that hosts compiled bundles over HTTPS.

For an example deployment, see the bundle server files in the NGINX Ingress Controller repository.

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

Create a TLS Secret (optional)

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:

    shell
    kubectl create secret generic bundle-ca-cert \
      --type=nginx.org/ca \
      --from-file=ca.crt=</path/to/ca.crt>
  • Client mTLS — Create a kubernetes.io/tls Secret with your client certificate and key, and reference it in secret:

    shell
    kubectl create secret tls bundle-client-cert \
      --cert=</path/to/tls.crt> \
      --key=</path/to/tls.key>

Create a WAF Policy

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

yaml
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

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.

Apply the policy to a VirtualServer

After waf-policy is created, apply a VirtualServer that references it in spec.policies.

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

For complete end-to-end manifests, see the waf-management-plane examples.

Verify the bundle was fetched

  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.

Enable polling (optional)

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.

Skip unchanged bundles with checksum verification (optional)

Set verifyChecksum: true to have NGINX Ingress Controller reject a downloaded bundle if its SHA-256 hash does not match the expected value. This protects against tampered or corrupted bundles during both the initial fetch and recurring polls.

shell
kubectl patch policy waf-policy --type merge -p '{
  "spec": {"waf": {"apBundleSource": {"verifyChecksum": true}}}
}'
For NGINX Instance Manager and NGINX One Console sources, change detection uses native metadata hashes rather than downloading the bundle, making verifyChecksum less useful for those source types.

Add a security log bundle source (optional)

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

In your existing Policy, add this under spec:

yaml
waf:
  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"

Verify log events are arriving at your syslog destination:

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

Troubleshooting

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.