# Build NGINX Ingress Controller with F5 WAF for NGINX




This document explains how to build a F5 NGINX Ingress Controller image with F5 WAF for NGINX v5 from source code.

**Note:**  If you'd rather not build your own NGINX Ingress Controller image, see the [pre-built image options](#pre-built-images) at the end of this guide.

## Before you begin

- To use F5 WAF for NGINX with NGINX Ingress Controller, you must have NGINX Plus.

NGINX Ingress Controller supports the following versions of [F5 WAF for NGINX](https://docs.nginx.com/waf/):

| NIC Version         | NAP-WAF Version | Config Manager | Enforcer |
| ------------------- | --------------- | -------------- | -------- |
|  | 37.0+       |           |    |
| 5.4.3               | 37.0+5.635.1    | 5.13.1         | 5.13.1   |
| 5.3.4               | 35+5.527.0      | 5.11.2         | 5.11.2   |
| 5.2.1               | 35+5.527.0      | 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    |

## Prepare the environment

Get your system ready for building and pushing the NGINX Ingress Controller image with F5 WAF for NGINX v5.

1. Sign in to your private registry. Replace `<my-docker-registry>` with the path to your own private registry.

    ```shell
    docker login <my-docker-registry>
    ```

1. Pull the WAF Config Manager image:

    ```shell
    docker pull private-registry.nginx.com/nap/waf-config-mgr:<image-tag>
    ```

1. Pull the WAF Enforcer Docker image

    ```shell
    docker pull private-registry.nginx.com/nap/waf-enforcer:<image-tag>
    ```

1. Pull the WAF IP Intelligence image (if you plan to use the WAF IP Intelligence module with your WAF v5 policies):

    ```shell
    docker pull private-registry.nginx.com/nap/waf-ip-intelligence:<image-tag>
    ```

1. Clone the NGINX Ingress Controller repository:

    ```console
    git clone https://github.com/nginx/kubernetes-ingress.git --branch v
    cd kubernetes-ingress
    ```

## Build the image

Follow these steps to build the NGINX Controller Image with F5 WAF for NGINX v5.

1. Place your NGINX Plus license files (_nginx-repo.crt_ and _nginx-repo.key_) in the project's root folder. To verify they're in place, run:

    ```shell
    ls nginx-repo.*
    ```

   You should see:

    ```shell
    nginx-repo.crt  nginx-repo.key
    ```

2. Build the image. Replace `<makefile target>` with your chosen build option and `<my-docker-registry>` with your private registry's path. Refer to the [Makefile targets](#makefile-targets) table below for the list of build options.

    ```shell
    make <makefile target> PREFIX=<my-docker-registry>/nginx-plus-ingress TARGET=download
    ```

   For example, to build a Debian-based image with NGINX Plus and F5 WAF for NGINX v5, run:

    ```shell
    make debian-image-nap-v5-plus PREFIX=<my-docker-registry>/nginx-plus-ingress TARGET=download
    ```

   **What to expect**: The image is built and tagged with a version number, which is derived from the `VERSION` variable in the [_Makefile_](/nic/install/build.md#makefile-details). This version number is used for tracking and deployment purposes.

**Note:**  In the event a patch of NGINX Plus is released, make sure to rebuild your image to get the latest version. If your system is caching the Docker layers and not updating the packages, add `DOCKER_BUILD_OPTIONS="--pull --no-cache"` to the make command. 

### Makefile targets {#makefile-targets}

Create Docker image for NGINX Ingress Controller (Alpine with NGINX Plus, F5 WAF for NGINX v5 and FIPS)

| Makefile Target           | Description                                                       | Compatible Systems  |
|---------------------------|-------------------------------------------------------------------|---------------------|
| **alpine-image-nap-v5-plus-fips** | Builds a Alpine-based image with NGINX Plus and the [F5 WAF for NGINX v5](/nginx-app-protect-waf/v5/) module with FIPS. | Alpine  |
| **debian-image-nap-v5-plus** | Builds a Debian-based image with NGINX Plus and the [F5 WAF for NGINX v5](/nginx-app-protect-waf/v5/) module. | Debian  |
| **ubi-image-nap-v5-plus**    | Builds a UBI-based image with NGINX Plus and the [F5 WAF for NGINX v5](/nginx-app-protect-waf/v5/) module. | OpenShift |
| **ubi-image-nap-dos-v5-plus** | Builds a UBI-based image with NGINX Plus, [F5 WAF for NGINX v5](/nginx-app-protect-waf/v5/), and [F5 DoS for NGINX](/nginx-app-protect-dos/). | OpenShift |

**Note:**  For the complete list of _Makefile_ targets and customizable variables, see the [Build NGINX Ingress Controller](/nic/install/build.md#makefile-details) guide. 

If you intend to use [external references](/nginx-app-protect-waf/v5/configuration-guide/configuration/#external-references) in F5 WAF for NGINX policies, you may want to provide a custom CA certificate to authenticate with the hosting server.

To do so, place the `*.crt` file in the build folder and uncomment the lines following this comment:
`#Uncomment the lines below if you want to install a custom CA certificate`

**Note:**  External references are deprecated in NGINX Ingress Controller and will not be supported in future releases. 

## Push the images to your private registry

Once you've successfully pulled the WAF v5 manager and enforcer images and built the NGINX Ingress Controller image with F5 WAF for NGINX v5, the next step is to upload them to your private Docker registry. This makes the image available for deployment to your Kubernetes cluster.

To upload the image, run the following command. If you're using a custom tag, add `TAG=your-tag` to the end of the command. Replace `<my-docker-registry>` with your private registry's path.

```shell
make push PREFIX=<my-docker-registry>/nginx-plus-ingress
```

To upload the WAF config manager and enforcer images run the following commands:

```shell
docker push <my-docker-registry>/waf-config-mgr:<your-tag>
```

```shell
docker push <my-docker-registry>/waf-enforcer:<your-tag>
```

To make sure your NGINX Ingress Controller pods reach the `Ready` state, you'll need to create custom resource definitions (CRDs) for various components.

Alternatively, you can disable this requirement by setting the `-enable-custom-resources` command-line argument to `false`.

There are two ways you can install the custom resource definitions:

1. Using a URL to apply a single CRD yaml file, which we recommend.
1. Applying your local copy of the CRD yaml files, which requires you to clone the repository.

The core custom CRDs are the following:

- [VirtualServer and VirtualServerRoute](/nic/configuration/virtualserver-and-virtualserverroute-resources.md)
- [TransportServer](/nic/configuration/transportserver-resource.md)
- [Policy](/nic/configuration/policy-resource.md)
- [GlobalConfiguration](/nic/configuration/global-configuration/globalconfiguration-resource.md)

#### Install CRDs from single YAML

```shell
kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v/deploy/crds.yaml
```

#### Install CRDs after cloning the repo

**Note:**  

Read the steps outlined in [Upgrade from 3.x to 4.x](/nic/install/upgrade.md#upgrade-from-3x-to-4x) before running the CRD upgrade and perform the steps if applicable.

```shell
kubectl apply -f config/crd/bases/k8s.nginx.org_virtualservers.yaml
kubectl apply -f config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml
kubectl apply -f config/crd/bases/k8s.nginx.org_transportservers.yaml
kubectl apply -f config/crd/bases/k8s.nginx.org_policies.yaml
kubectl apply -f config/crd/bases/k8s.nginx.org_globalconfigurations.yaml
```

## Deploy NGINX Ingress Controller {#deploy-ingress-controller}

**important:**  NGINX Ingress Controller with the AppProtect WAF v5 module works only with policy bundles. You need to modify the Deployment, DaemonSet, or StatefulSet file to include volumes, volume mounts and two WAF 5 docker images: `waf-config-mgr` and `waf-enforcer`.

NGINX Ingress Controller **requires** the volume mount path to be `/etc/app_protect/bundles`. 

#### With Helm

Below are examples of a `PersistentVolume` and `PersistentVolumeClaim` that you can reference in your Helm values:

```yaml
...
volumes:
- name: <volume_name>
persistentVolumeClaim:
    claimName: <claim_name>
...
```

Add volume mounts to the `containers` section:

```yaml
...
volumeMounts:
- name: <volume_mount_name>
  mountPath: /etc/app_protect/bundles
...
```

### Enabling WAF v5

Start by setting `controller.appprotect.enable` to `true` in your Helm values. This will the standard F5 WAF for NGINX features.
Afterwords, set `controller.approtect.v5` to `true`.
This ensures that both the `waf-enforcer` and `waf-config-mgr` containers are deployed alongside the NGINX Ingress Controller containers.
These two additional containers are required when using F5 WAF for NGINX v5.

Your Helm values should look something like this:

```yaml
controller:
  ...
  ## Support for F5 WAF for NGINX
  appprotect:
    ## Enable the F5 WAF for NGINX module in the Ingress Controller.
    enable: true
    ## Enables F5 WAF for NGINX v5.
    v5: true
```

### Configuring volumes

Whether you have created a new `PersistentVolume` and `PersistentVolumeClaim`, or you are referencing an existing `PersistentVolumeClaim`, update the `app-protect-bundles` volume to reference your `PersistentVolumeClaim`.

Example helm values:

```yaml
...
controller:
  ...
  appprotect:
  ...
   volumes:
   - name: app-protect-bundles
     persistentVolumeClaim:
        claimName: <my_claim_name>
...
```

**Note:** 
By default, `emptyDir` mounts are used.
Bundles that are added to these kind of volume mounts will **NOT** persist across pod restarts.

Example default volumes:

```yaml
...
controller:
  ...
  appprotect:
  ...
   volumes:
   - name: app-protect-bundles
     emptyDir: {}
...
```

### Configuring `readOnlyRootFilesystem`

Create required volumes:

```yaml
volumes:
  - name: nginx-etc
    emptyDir: {}
  - name: nginx-cache # do not set this value in statefulset if volumeclaimtemplate is set
    emptyDir: {}      # do not set this value in statefulset if volumeclaimtemplate is set
  - name: nginx-lib
    emptyDir: {}
  - name: nginx-log
    emptyDir: {}
  - name: app-protect-bd-config
    emptyDir: {}
  - name: app-protect-config
    emptyDir: {}
  - name: app-protect-bundles
    emptyDir: {}
```

Set `controller.securityContext.readOnlyRootFilesystem` to `true`.

Example Helm values:

```yaml
controller:
  ...
  securityContext:
    readOnlyRootFilesystem: true
  ...
```

Set `controller.appprotect.enforcer.securityContext.readOnlyRootFilesystem` to `true`.

Example Helm values:

```yaml
controller:
  ...
  appprotect:
    ...
    enforcer:
      securityContext:
        readOnlyRootFilesystem: true
  ...
```

Set `controller.appprotect.configManager.securityContext.readOnlyRootFilesystem` to `true`.

Example Helm values:

```yaml
controller:
  ...
  appprotect:
    ...
    configManager:
      securityContext:
        readOnlyRootFilesystem: true
  ...
```

#### With Manifest

You have two options for deploying NGINX Ingress Controller:

- **Deployment**. Choose this method for the flexibility to dynamically change the number of NGINX Ingress Controller replicas.
- **DaemonSet**. Choose this method if you want NGINX Ingress Controller to run on all nodes or a subset of nodes.
- **StatefulSet**. Choose this method when you need stable, persistent storage and ordered deployment/scaling for your NGINX Ingress Controller pods.

### Set up role-based access control (RBAC) {#set-up-rbac}

**Note:** To complete these steps you need admin access to your cluster. Refer to to your Kubernetes platform's documentation to set up admin access. For Google Kubernetes Engine (GKE), you can refer to their [Role-Based Access Control guide](https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control).

1. Create a namespace and a service account:

    ```shell
    kubectl apply -f deployments/common/ns-and-sa.yaml
    ```

2. Create a cluster role and binding for the service account:

    ```shell
    kubectl apply -f deployments/rbac/rbac.yaml
    ```

### Volumes and VolumeMounts

Add a `volumes` section to deployment template spec:

```yaml
...
volumes:
- name: <volume_name>
persistentVolumeClaim:
    claimName: <claim_name>
...
```

Add volume mounts to the `containers` section:

```yaml
...
volumeMounts:
- name: <volume_mount_name>
    mountPath: /etc/app_protect/bundles
...
```

### WAF Config Manager and WAF Enforcer

Add `waf-config-mgr` image to the `containers` section:

```yaml
...
- name: waf-config-mgr
  image: private-registry.nginx.com/nap/waf-config-mgr:<version-tag>
  imagePullPolicy: IfNotPresent
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
        - all
  volumeMounts:
    - name: app-protect-bd-config
      mountPath: /opt/app_protect/bd_config
    - name: app-protect-config
      mountPath: /opt/app_protect/config
    - name: app-protect-bundles
      mountPath: /etc/app_protect/bundles
...
```

Add `waf-enforcer` image to the `containers` section:

```yaml
...
- name: waf-enforcer
  image: private-registry.nginx.com/nap/waf-enforcer:<version-tag>
  imagePullPolicy: IfNotPresent
  env:
    - name: ENFORCER_PORT
      value: "50000"
    - name: ENFORCER_CONFIG_TIMEOUT
      value: "0"
  volumeMounts:
    - name: app-protect-bd-config
      mountPath: /opt/app_protect/bd_config
...
```

### Update NIC container in deployment, daemonset, or statefulset

Add `volumeMounts` as below:

```yaml
...
- image: <my_docker_registry>:<version_tag>
  imagePullPolicy: IfNotPresent
  name: nginx-plus-ingress
  volumeMounts:
    - name: app-protect-bd-config
      mountPath: /opt/app_protect/bd_config
    - name: app-protect-config
      mountPath: /opt/app_protect/config
    - name: app-protect-bundles
      mountPath: /etc/app_protect/bundles
...
```

### Configure `readOnlyRootFilesystem`

Add `readOnlyRootFilesystem` to the NIC container and set valut to `true` as below:

```yaml
...
- image: <my_docker_registry>:<version_tag>
  imagePullPolicy: IfNotPresent
  name: nginx-plus-ingress
  ...
  securityContext:
    allowPrivilegeEscalation: false
      capabilities:
        add:
        - NET_BIND_SERVICE
        drop:
        - ALL
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 101
    readOnlyRootFilesystem: true
  ...
  volumeMounts:
    - mountPath: /etc/nginx
      name: nginx-etc
    - mountPath: /var/cache/nginx
      name: nginx-cache
    - mountPath: /var/lib/nginx
      name: nginx-lib
    - mountPath: /var/log/nginx
      name: nginx-log
    - mountPath: /opt/app_protect/bd_config
      name: app-protect-bd-config
    - mountPath: /opt/app_protect/config
      name: app-protect-config
    - mountPath: /etc/app_protect/bundles
      name: app-protect-bundles
...
```

Add `readOnlyRootFilesystem` to the `waf-config-mgr` container and set value to `true` as below:

```yaml
...
- name: waf-config-mgr
  image: private-registry.nginx.com/nap/waf-config-mgr:<version-tag>
  imagePullPolicy: IfNotPresent
  ...
  securityContext:
    readOnlyRootFilesystem: true
    ...
...
```

Add `readOnlyRootFilesystem` to the `waf-enforcer` container and set value to `true` as below:

```yaml
...
- name: waf-enforcer
  image: private-registry.nginx.com/nap/waf-enforcer:<version-tag>
  imagePullPolicy: IfNotPresent
  ...
  securityContext:
    readOnlyRootFilesystem: true
    ...
...
```

**Note:** 
**StatefulSet Volume Configuration**: When using StatefulSet deployments, the `nginx-cache` volume is automatically provided via `volumeClaimTemplates` for persistent storage. F5 WAF for NGINX v5 volumes (like app-protect-config, app-protect-bundles) are still configured as regular volumes in the `volumes` section. Use `emptyDir` for temporary data or PersistentVolumeClaims if you need persistence for App Protect configurations across pod restarts.

### Using a Deployment

For additional context on managing containers using Kubernetes Deployments, refer to the official Kubernetes [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) documentation.

When you deploy NGINX Ingress Controller as a Deployment, Kubernetes automatically sets up a single NGINX Ingress Controller pod.

- For NGINX, run:

    ```shell
    kubectl apply -f deployments/deployment/nginx-ingress.yaml
    ```

- For NGINX Plus, run:

    ```shell
    kubectl apply -f deployments/deployment/nginx-plus-ingress.yaml
    ```

    Update the `nginx-plus-ingress.yaml` file to include your chosen image from the F5 Container registry or your custom container image.

### Using a DaemonSet

For additional context on managing containers using Kubernetes DaemonSets, refer to the official Kubernetes [DaemonSets](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) documentation.

When you deploy NGINX Ingress Controller as a DaemonSet, Kubernetes creates an Ingress Controller pod on every node in the cluster.

- For NGINX, run:

    ```shell
    kubectl apply -f deployments/daemon-set/nginx-ingress.yaml
    ```

- For NGINX Plus, run:

    ```shell
    kubectl apply -f deployments/daemon-set/nginx-plus-ingress.yaml
    ```

    Update the `nginx-plus-ingress.yaml` file to include your chosen image from the F5 Container registry or your custom container image.

### Using a StatefulSet

For additional context on managing containers using Kubernetes StatefulSets, refer to the official Kubernetes [StatefulSets](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) documentation.

When you deploy NGINX Ingress Controller as a StatefulSet, Kubernetes creates pods with stable network identities and persistent storage.

- For NGINX, run:

    ```shell
    kubectl apply -f deployments/stateful-set/nginx-ingress.yaml
    ```

- For NGINX Plus, run:

    ```shell
    kubectl apply -f deployments/stateful-set/nginx-plus-ingress.yaml
    ```

    Update the `nginx-plus-ingress.yaml` file to include your chosen image from the F5 Container registry or your custom container image.

**Note:** 
StatefulSets include persistent volume claims for nginx cache storage via `volumeClaimTemplates`. You may need to configure a StorageClass in your cluster or modify the volumeClaimTemplates section in the manifest to match your storage requirements. Other volumes (like those needed for App Protect modules) are configured in the regular `volumes` section, not in volumeClaimTemplates.

### Enable F5 WAF for NGINX module

To enable the F5 DoS for NGINX Module:

- Add the `enable-app-protect` [command-line argument](/nic/configuration/global-configuration/command-line-arguments.md#cmdoption-enable-app-protect) to your Deployment, DaemonSet, or StatefulSet file.

## Confirm NGINX Ingress Controller is running

To confirm the NGINX Ingress Controller pods are operational, run:

```shell
kubectl get pods --namespace=nginx-ingress
```

For more information, see the [Configuration guide](/nic/integrations/app-protect-waf-v5/configuration.md) and the NGINX Ingress Controller with App Protect version 5 example resources on GitHub [for VirtualServer resources](https://github.com/nginx/kubernetes-ingress/tree/v/examples/custom-resources/app-protect-waf-v5).

## Alternatives to building your own image {#pre-built-images}

If you prefer not to build your own NGINX Ingress Controller image, you can use pre-built images. Here are your options:

- Download the image using your NGINX Ingress Controller subscription certificate and key. View the [Download NGINX Ingress Controller from the F5 Registry](/nic/install/images/registry-download.md) topic.
- The [Add an NGINX Ingress Controller image to your cluster](/nic/install/images/add-image-to-cluster.md) topic describes how to use your subscription JWT token to get the image.

