Compile F5 WAF for NGINX policies using NGINX Instance Manager

Overview

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

F5 WAF for NGINX 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

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:

json
{
  "metadata": {
    "name": "Nginxbundletest",
    "displayName": "Nginxbundletest",
    "description": "Ignore cross-site scripting is a security policy that intentionally ignores cross site scripting."
  },
  "content": "ewoJInBvbGljeSI6IHsKCQkibmFtZSI6ICJzaW1wbGUtYmxvY2tpbmctcG9saWN5IiwKCQkic2lnbmF0dXJlcyI6IFsKCQkJewoJCQkJInNpZ25hdHVyZUlkIjogMjAwMDAxODM0LAoJCQkJImVuYWJsZWQiOiBmYWxzZQoJCQl9CgkJXSwKCQkidGVtcGxhdGUiOiB7CgkJCSJuYW1lIjogIlBPTElDWV9URU1QTEFURV9OR0lOWF9CQVNFIgoJCX0sCgkJImFwcGxpY2F0aW9uTGFuZ3VhZ2UiOiAidXRmLTgiLAoJCSJlbmZvcmNlbWVudE1vZGUiOiAiYmxvY2tpbmciCgl9Cn0="
}
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.

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

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

json
{
    "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"
    }
}
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:

json
{
  "bundles": [
    {
      "appProtectWAFVersion": "5.527.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:

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

You should receive a response similar to the following:

json
{
    "items": [
        {
            "compilationStatus": {
                "message": "",
                "status": "compiling"
            },
            "content": "",
            "metadata": {
                "appProtectWAFVersion": "5.527.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:

shell
curl --location 'https://127.0.0.1/api/platform/v1/security/policies/bundles' \
-H "Authorization: Bearer <access_token>"
json
{
    "items": [
        {
            "compilationStatus": {
                "message": "",
                "status": "compiled"
            },
            "content": "",
            "metadata": {
                "appProtectWAFVersion": "5.527.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"
            }
        }
    ]
}

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 policy 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:

shell
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 <auth token>" \
     | jq -r '.content' | base64 -d > security-policy-bundle.tgz

Download the security log bundle

Use a GET request to download the secops_dashboard security log bundle. The security log bundle adjusts the format of the policy events to be compatible with NGINX Instance Manager:

curl -X GET "https://{NMS_FQDN}/api/platform/v1/security/logprofiles/secops_dashboard/5.527.0/bundle" -H "Authorization: Bearer <access token>" | jq -r .compiledBundle | base64 -d > secops_dashboard.tgz

Add volumes and volumeMounts

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:

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

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

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

yaml

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/app_protect/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

Upload the security log bundle

Upload the security log bundle binary file to the NGINX Ingress Controller pods.

kubectl cp /your/local/path/secops_dashboard.tgz  <namespace>/<pod-name>:etc/app_protect/bundles/secops_dashboard.tgz -c nginx-ingress
kubectl cp /your/local/path/secops_dashboard.tgz  <namespace>/<pod-name>:etc/app_protect/bundles/secops_dashboard.tgz -c nginx-plus-ingress

Upload the security policy bundle

Upload the binary file to the NGINX Ingress Controller pods.

kubectl cp /your/local/path/<bundle_name>.tgz  <namespace>/<pod-name>:etc/app_protect/bundles<bundle_name>.tgz -c nginx-ingress
kubectl cp /your/local/path/<bundle_name>.tgz  <namespace>/<pod-name>:etc/app_protect/bundles<bundle_name>.tgz -c nginx-plus-ingress

Create WAF policy

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

The example below shows the required WAF policy, for the apBundle field you must use the security bundle binary file (a tarball). The apLogBundle field contains the secops_dashboard.tgz file.

yaml
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
  name: <waf-policy-name>
spec:
  waf:
    enable: true
    apBundle: "<bundle-name>.tgz"
    securityLogs:
    - enable: true
        apLogBundle: "secops_dashboard.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.

yaml
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

Your VirtualServer should now apply the generated security policy to your traffic and emit security events to NGINX Instance Manager.