Compile NGINX App Protect WAF policies using NGINX Instance Manager

Overview

This guide describes how to use F5 NGINX Instance Manager to compile NGINX App Protect WAF Policies for use with NGINX Ingress Controller.

NGINX App Protect WAF uses policies to configure which security features are set. When these policies are changed, they need to be compiled so that the engine can begin to use them. Compiling policies can take a large amount of time and resources. You can do this with the NGINX Instance Manager. This reduces the impact on a NGINX Ingress Controller deployment.

By using NGINX Instance Manager to compile WAF policies, the policy bundle can also be used immediately by NGINX Ingress Controller without reloading.

The following steps describe how to use the NGINX Instance Manager API to create a new security policy, compile a bundle, then add it to NGINX Ingress Controller.

Before you start

Requirements

Create a new security policy

Tip:
You can skip this step if you intend to use an existing security policy.

Create a new security policy using the API: this will require the use of a tool such as curl or Postman

Create the file simple-policy.json with the contents below:

{
  "metadata": {
    "name": "Nginxbundletest",
    "displayName": "Nginxbundletest",
    "description": "Ignore cross-site scripting is a security policy that intentionally ignores cross site scripting."
  },
  "content": "ewoJInBvbGljeSI6IHsKCQkibmFtZSI6ICJzaW1wbGUtYmxvY2tpbmctcG9saWN5IiwKCQkic2lnbmF0dXJlcyI6IFsKCQkJewoJCQkJInNpZ25hdHVyZUlkIjogMjAwMDAxODM0LAoJCQkJImVuYWJsZWQiOiBmYWxzZQoJCQl9CgkJXSwKCQkidGVtcGxhdGUiOiB7CgkJCSJuYW1lIjogIlBPTElDWV9URU1QTEFURV9OR0lOWF9CQVNFIgoJCX0sCgkJImFwcGxpY2F0aW9uTGFuZ3VhZ2UiOiAidXRmLTgiLAoJCSJlbmZvcmNlbWVudE1vZGUiOiAiYmxvY2tpbmciCgl9Cn0="
}
Warning:
The content value must be base64 encoded or you will encounter an error.

Upload the policy JSON files with the API, which is the same method to create the bundle later.

In the same directory you created simple-policy.json, create a POST request for NGINX Instance Manager using the API.

curl -X POST https://{{NMS_FQDN}}/api/platform/v1/security/policies \
    -H "Authorization: Bearer <access token>" \
    -d @simple-policy.json

You should receive an API response similar to the following output, indicating the policy has been successfully created.

{
    "metadata": {
        "created": "2024-06-12T20:28:08.152171922Z",
        "description": "Ignore cross-site scripting is a security policy that intentionally ignores cross site scripting.",
        "displayName": "Nginxbundletest",
        "externalId": "",
        "externalIdType": "",
        "modified": "2024-06-12T20:28:08.152171922Z",
        "name": "Nginxbundletest",
        "revisionTimestamp": "2024-06-12T20:28:08.152171922Z",
        "uid": "6af9f261-658b-4be1-b07a-cebd83e917a1"
    },
    "selfLink": {
        "rel": "/api/platform/v1/security/policies/6af9f261-658b-4be1-b07a-cebd83e917a1"
    }
}
Important:
Take note of the uid field: "uid": "6af9f261-658b-4be1-b07a-cebd83e917a1" It is one of two unique IDs we will use to download the bundle: it will be referenced as policy-UID.

Create a new security bundle

Once you have created (Or selected) a security policy, create a security bundle using the API. The version in the bundle you create must match the WAF compiler version you intend to use.

You can check which version is installed in NGINX Instance Manager by checking the operating system packages. If the wrong version is noted in the JSON payload, you will receive an error similar to below:

{"code":13018,"message":"Error compiling the security policy set: One or more of the specified compiler versions does not exist. Check the compiler versions, then try again."}

Create the file security-policy-bundles.json:

{
  "bundles": [
    {
      "appProtectWAFVersion": "4.815.0",
      "policyName": "Nginxbundletest",
      "policyUID": "",
      "attackSignatureVersionDateTime": "latest",
      "threatCampaignVersionDateTime": "latest"
    }
  ]
}

The policyUID value is left blank, as it is generated as part of the creating the bundle.

Send a POST request to create the bundle through the API:

curl -X POST https://{{NMS_FQDN}}/api/platform/v1/security/policies/bundles \
    -H "Authorization: Bearer <access token>" \
    -d @security-policy-bundles.json

You should receive a response similar to the following:

{
    "items": [
        {
            "compilationStatus": {
                "message": "",
                "status": "compiling"
            },
            "content": "",
            "metadata": {
                "appProtectWAFVersion": "4.815.0",
                "attackSignatureVersionDateTime": "2024.02.21",
                "created": "2024-06-12T13:28:20.023775785-07:00",
                "modified": "2024-06-12T13:28:20.023775785-07:00",
                "policyName": "Nginxbundletest",
                "policyUID": "6af9f261-658b-4be1-b07a-cebd83e917a1",
                "threatCampaignVersionDateTime": "2024.02.25",
                "uid": "cbdf9577-6d81-43d6-8ce1-2e3d4714e8b5"
            }
        }
    ]
}

You can use the API to list the security bundles, verifying the new addition:

curl --location 'https://127.0.0.1/api/platform/v1/security/policies/bundles' \
-H "Authorization: Bearer <access_token>"
{
    "items": [
        {
            "compilationStatus": {
                "message": "",
                "status": "compiled"
            },
            "content": "",
            "metadata": {
                "appProtectWAFVersion": "4.815.0",
                "attackSignatureVersionDateTime": "2024.02.21",
                "created": "2024-06-13T09:09:10.809-07:00",
                "modified": "2024-06-13T09:09:20-07:00",
                "policyName": "Nginxbundletest",
                "policyUID": "ec8681eb-1e25-4b71-93bd-b91f67c5ac99",
                "threatCampaignVersionDateTime": "2024.02.25",
                "uid": "de08b324-99d8-4155-b2eb-fe687b21034e"
            }
        }
    ]
}
Important:

Take note of the uid field: "uid": "de08b324-99d8-4155-b2eb-fe687b21034e"

It is one of two unique IDs we will use to download the bundle: it will be referenced as bundle-UID.

Download the security bundle

Use a GET request to download the security bundle using the policy and bundle IDs:

curl -X GET "https://{NMS_FQDN}/api/platform/v1/security/policies/<policy-UID>/bundles/<bundle-UID>" -H "Authorization: Bearer <access token>" | jq -r '.content' | base64 -d > security-policy-bundle.tgz

This GET request uses the policy and bundle IDs from the previous examples:

curl -X GET -k 'https://127.0.0.1/api/platform/v1/security/policies/6af9f261-658b-4be1-b07a-cebd83e917a1/bundles/de08b324-99d8-4155-b2eb-fe687b21034e' \                                                                                                     
    -H "Authorization: Basic YWRtaW46UncxQXBQS3lRRTRuQXRXOFRYa1J4ZFdVSWVTSGtU" \
     | jq -r '.content' | base64 -d > security-policy-bundle.tgz

Add volumes and volumeMounts to NGINX Ingress Controller

To use WAF security bundles, your NGINX Ingress Controller instance must have volumes and volumeMounts. Precise paths are used to detect when bundles are uploaded to the cluster.

Here is an example of what to add:

volumes:
- name: <volume_name>
persistentVolumeClaim:
    claimName: <claim_name>

volumeMounts:
- name: <volume_mount_name>
    mountPath: /etc/nginx/waf/bundles

A full example of a deployment file with volumes and volumeMounts could look like the following:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress
  namespace: nginx-ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-ingress
  template:
    metadata:
      labels:
        app: nginx-ingress
        app.kubernetes.io/name: nginx-ingress
     #annotations:
       #prometheus.io/scrape: "true"
       #prometheus.io/port: "9113"
       #prometheus.io/scheme: http
    spec:
      serviceAccountName: nginx-ingress
      automountServiceAccountToken: true
      securityContext:
        seccompProfile:
          type: RuntimeDefault
      volumes:
      - name: nginx-bundle-mount
        emptydir: {}
      containers:
      - image: <replace>
        imagePullPolicy: IfNotPresent
        name: nginx-ingress
        ports:
        - name: http
          containerPort: 80
        - name: https
          containerPort: 443
        - name: readiness-port
          containerPort: 8081
        - name: prometheus
          containerPort: 9113
        readinessProbe:
          httpGet:
            path: /nginx-ready
            port: readiness-port
          periodSeconds: 1
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
         #limits:
         #  cpu: "1"
         #  memory: "1Gi"
        securityContext:
          allowPrivilegeEscalation: false
          runAsUser: 101 #nginx
          runAsNonRoot: true
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE
        volumeMounts:
        -  name: bundle-mount
           mountPath: /etc/nginx/waf/bundles
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        args:
          - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config
          - -report-ingress-status
          - -external-service=nginx-ingress

Create WAF policy

To process a bundle, you must create a new WAF policy. This policy is added to /etc/nginx/waf/bundles, allowing NGINX Ingress Controller to load it into WAF.

The example below shows the required WAF policy, and the apBundle and apLogConf fields you must use for the security bundle binary file (A tar ball).

apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: <waf-policy-name>
spec:
  waf:
    enable: true
    apBundle: "<bundle-name>.tgz"
    securityLogs:
    - enable: true
        apLogBundle: "<bundle-name>.tgz"
        logDest: "<security-log-destination-URL>"

Create VirtualServer resource and apply policy

Once the WAF policy has been created, link it to your virtualServer resource.

apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
  name: webapp
spec:
  host: webapp.example.com
  policies:
  - name: <waf-policy-name>
  upstreams:
  - name: webapp
    service: webapp-svc
    port: 80
  routes:
  - path: /
    action:
      pass: webapp

Upload the security bundle

To finish adding a security bundle, the binary file to the NGINX Ingress Controller pods.

kubectl cp /your/local/path/<bundle_name>.tgz  <namespace>/<pod-name>:etc/nginx/waf/bundles<bundle_name>.tgz

Once the bundle has been uploaded to the cluster, NGINX Ingress Controller will detect and automatically load the new WAF policy.