diff --git a/content/nic/install/plm-installation.md b/content/nic/install/plm-installation.md new file mode 100644 index 000000000..59bd61d0f --- /dev/null +++ b/content/nic/install/plm-installation.md @@ -0,0 +1,547 @@ +--- +title: Install NGINX Ingress Controller with F5 WAF for NGINX using PLM +weight: 200 +toc: true +f5-content-type: tutorial +f5-product: F5 NGINX Ingress Controller +f5-description: > + Install F5 NGINX Ingress Controller with F5 WAF for NGINX and Policy + Lifecycle Management (PLM). Deploy WAF-protected traffic using either a + VirtualServer or an Ingress resource. +f5-audience: operator +--- + +Use this guide to install F5 NGINX Ingress Controller with F5 WAF for NGINX using Policy Lifecycle Management (PLM). PLM defines WAF policies as Kubernetes custom resources, compiles them automatically, and stores the compiled bundles in an in-cluster S3-compatible object store. NGINX Ingress Controller then fetches the bundles from PLM storage and enforces the policies at request time. + +By the end of this tutorial, you'll have: + +- A running PLM backend and NGINX Ingress Controller deployment configured for PLM storage. +- An `APPolicy` and an `APLogConf` resource compiled by PLM. +- A `k8s.nginx.org/v1` Policy that references the compiled resources. +- The Policy attached to a VirtualServer or an Ingress, with traffic flowing normally and attack payloads blocked. + +## Before you begin + +Before you start, make sure you have: + +- `kubectl` access to a Kubernetes cluster. +- Helm installed. +- Credentials for `private-registry.nginx.com`. + +This tutorial uses the following example values. If you use different values, replace them consistently throughout. + +| Example value | What it represents | +|---|---| +| `plm-system` | Namespace for the PLM backend | +| `plm` | Helm release name for PLM | +| `nginx-ingress` | Namespace for NGINX Ingress Controller | +| `nic` | Helm release name for NGINX Ingress Controller | +| `security` | Namespace for `APPolicy` and `APLogConf` resources | +| `default` | Namespace for the Policy, sample application, and routing resource | +| `webapp.example.com` | Example hostname for VirtualServer routing | +| `cafe.example.com` | Example hostname for Ingress routing | + +## Deploy PLM infrastructure + +Install the PLM backend before installing NGINX Ingress Controller. The PLM backend provisions the App Protect v1 CRDs, the Policy Controller, the compiler service, and the SeaweedFS storage backend in the `plm-system` namespace. + +{{< include "waf/plm-deploy-infrastructure.md" >}} + +Confirm the CRDs are installed: + +```shell +kubectl get crd | grep appprotect.f5.com +``` + +Expected output: + +```text +aplogconfs.appprotect.f5.com +appolicies.appprotect.f5.com +apsignatures.appprotect.f5.com +apusersigs.appprotect.f5.com +``` + +## Look up the PLM storage endpoint and credentials + +NGINX Ingress Controller connects to PLM's SeaweedFS filer over S3 to fetch compiled bundles. Before you install the controller, collect these four values from the PLM installation: + +1. **PLM storage URL**: the SeaweedFS filer endpoint (HTTPS or HTTP). +2. **Credentials Secret**: the S3 credentials Secret. The access key ID is `admin` by default. The secret access key is in the `seaweedfs_admin_secret` field. +3. **CA Secret** (HTTPS only): verifies the SeaweedFS filer certificate. +4. **Client TLS Secret** (mutual TLS only): presented by NGINX Ingress Controller when it connects to the filer. + +List the Services PLM created and identify the filer: + +```shell +kubectl get service --namespace plm-system +``` + +Expected output includes an entry similar to: + +```text +NAME TYPE CLUSTER-IP PORT(S) +plm-f5-waf-seaweed-filer ClusterIP 10.0.0.10 8333/TCP,9333/TCP,... +``` + +Assemble the URL from the service name, namespace, and port. Use `9333` for HTTPS and `8333` for HTTP: + +- HTTPS (mTLS): `https://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:9333` +- HTTP: `http://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:8333` + +List the Secrets PLM created: + +```shell +kubectl get secret --namespace plm-system +``` + +The default PLM install creates three Secrets that NGINX Ingress Controller references: + +- `plm-f5-waf-seaweedfs-auth`: SeaweedFS credentials. +- `plm-f5-waf-seaweedfs-ca-cert`: CA certificate for the HTTPS filer. +- `plm-f5-waf-seaweedfs-client-cert`: client TLS certificate for mTLS. + +Record the Secret references in `` form. You'll pass all four values to NGINX Ingress Controller using `--set controller.appprotect.plmStorage.*` flags. + +## Install NGINX Ingress Controller with PLM storage + +Because PLM owns the `appprotect.f5.com/v1` CRDs, you must apply the controller's own CRDs before running `helm install`. The `deploy/crds.yaml` bundle contains every CRD the controller needs (`VirtualServer`, `VirtualServerRoute`, `Policy`, `TransportServer`, `GlobalConfiguration`, `DNSEndpoint`). The bundle deliberately excludes the App Protect CRDs, which PLM owns. + +Download your NGINX Ingress Controller subscription’s JSON Web Token and rename it to `nginx-repo.jwt`. + +```shell +kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v{{< nic-version >}}/deploy/crds.yaml +``` + +Create a namespace and image pull Secret for NGINX Ingress Controller: + +```shell +kubectl create namespace nginx-ingress +kubectl create secret docker-registry regcred \ + --namespace nginx-ingress \ + --docker-server=private-registry.nginx.com \ + --docker-username=$(cat nginx-repo.jwt) \ + --docker-password=none + +kubectl create secret generic license-token \ + --namespace nginx-ingress \ + --from-file=license.jwt=nginx-repo.jwt \ + --type=nginx.com/license +``` + +Add the NGINX Helm repository: + +```shell +helm repo add nginx-stable https://helm.nginx.com/stable +helm repo update nginx-stable +``` + +Install NGINX Ingress Controller with PLM storage turned on. This example uses HTTPS with mutual TLS. For HTTP storage, set only `controller.appprotect.plmStorage.url` and `controller.appprotect.plmStorage.credentialsSecret`. + +```shell +helm install nic nginx-stable/nginx-ingress \ + --namespace nginx-ingress \ + --skip-crds \ + --set controller.image.repository="private-registry.nginx.com/nginx-ic-nap-v5/nginx-plus-ingress" \ + --set controller.image.tag="{{< nic-version >}}" \ + --set controller.nginxplus=true \ + --set controller.appprotect.enable=true \ + --set controller.appprotect.v5=true \ + --set controller.appprotect.plmStorage.url="https://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:9333" \ + --set controller.appprotect.plmStorage.credentialsSecret="plm-system/plm-f5-waf-seaweedfs-auth" \ + --set controller.appprotect.plmStorage.caSecret="plm-system/plm-f5-waf-seaweedfs-ca-cert" \ + --set controller.appprotect.plmStorage.clientSSLSecret="plm-system/plm-f5-waf-seaweedfs-client-cert" \ + --set controller.appprotect.plmStorage.insecureSkipVerify=false \ + --set controller.serviceAccount.imagePullSecretName=regcred +``` + +Wait for the controller pod to become ready. Each pod runs three containers: `nginx-ingress`, `waf-enforcer`, and `waf-config-mgr`. + +```shell +kubectl wait --for=condition=Ready pods \ + --namespace nginx-ingress \ + --selector app.kubernetes.io/name=nginx-ingress \ + --timeout=180s + +kubectl get pods --namespace nginx-ingress +``` + +Expected output: + +```text +NAME READY STATUS RESTARTS AGE +nic-nginx-ingress-controller-xxxxx 3/3 Running 0 2m +``` + +Save the public IP address and HTTP port of the NGINX Ingress Controller LoadBalancer service to shell variables: + +```shell +IC_IP= +IC_HTTP_PORT= +``` + +## Define the WAF policy + +Create the `security` namespace to hold the `APPolicy` and `APLogConf` resources: + +```shell +kubectl create namespace security +``` + +Save the following as `waf-resources.yaml`. The file defines an `APPolicy` that blocks attack signatures and masks credit card numbers and social security numbers in responses. It also defines an `APLogConf` for security logging. + +```yaml +apiVersion: appprotect.f5.com/v1 +kind: APPolicy +metadata: + name: dataguard-blocking + namespace: security +spec: + policy: + name: dataguard-blocking + template: + name: POLICY_TEMPLATE_NGINX_BASE + applicationLanguage: utf-8 + enforcementMode: blocking + blocking-settings: + violations: + - name: VIOL_DATA_GUARD + alarm: true + block: true + data-guard: + enabled: true + maskData: true + creditCardNumbers: true + usSocialSecurityNumbers: true +--- +apiVersion: appprotect.f5.com/v1 +kind: APLogConf +metadata: + name: log-default + namespace: security +spec: + content: + format: default + max_message_size: 64k + max_request_size: any + filter: + request_type: all +``` + +Apply the file: + +```shell +kubectl apply -f waf-resources.yaml +``` + +Wait for both resources to reach `ready`: + +```shell +kubectl wait --for=jsonpath='{.status.bundle.state}'=ready \ + appolicy/dataguard-blocking --namespace security --timeout=180s + +kubectl wait --for=jsonpath='{.status.bundle.state}'=ready \ + aplogconf/log-default --namespace security --timeout=180s +``` + +Confirm the compiled bundle location: + +```shell +kubectl get appolicy dataguard-blocking --namespace security \ + --output jsonpath='State: {.status.bundle.state}{"\n"}Location: {.status.bundle.location}{"\n"}' +``` + +## Create the Policy + +Save the following as `waf-policy.yaml`. The `apPolicy` and `apLogConf` fields accept `[/]`. When you omit the namespace, NGINX Ingress Controller defaults to the Policy's own namespace. + +```yaml +apiVersion: k8s.nginx.org/v1 +kind: Policy +metadata: + name: waf-policy + namespace: default +spec: + waf: + enable: true + apPolicy: "security/dataguard-blocking" + securityLogs: + - enable: true + apLogConf: "security/log-default" + logDest: "syslog:server=syslog-svc.default:514" +``` + +Apply the file: + +```shell +kubectl apply -f waf-policy.yaml +``` + +Wait for the Policy to become `Valid`: + +```shell +kubectl wait --for=jsonpath='{.status.state}'=Valid \ + policy/waf-policy --namespace default --timeout=180s +``` + +If the Policy stays in `Warning` with a `BundleFetchFailed` reason, see [Troubleshooting](#troubleshooting). + +Choose how to attach the Policy: +- [Attach to a VirtualServer](#attach-to-a-virtualserver) +- [Attach to an Ingress](#attach-to-an-ingress) + +The Policy resource is the same for both. + +## Attach to a VirtualServer + +Save the following as `webapp.yaml`. The file defines the sample application Deployment, its Service, and a VirtualServer that references the `waf-policy` Policy. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webapp + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: webapp + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: webapp-svc + namespace: default +spec: + selector: + app: webapp + ports: + - name: http + port: 80 + targetPort: 8080 +--- +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: webapp + namespace: default +spec: + host: webapp.example.com + policies: + - name: waf-policy + upstreams: + - name: webapp + service: webapp-svc + port: 80 + routes: + - path: / + action: + pass: webapp +``` + +Apply the file: + +```shell +kubectl apply -f webapp.yaml +``` + +Send a normal request to confirm the application responds: + +```shell +curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \ + http://webapp.example.com:$IC_HTTP_PORT/ +``` + +Expected output: + +```text +Server address: 10.0.0.1:8080 +Server name: webapp-xxxxx +``` + +Send a request that triggers the data guard violation: + +```shell +curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \ + "http://webapp.example.com:$IC_HTTP_PORT/" +``` + +Expected output: + +```text +Request Rejected +The requested URL was rejected. Please consult with your administrator. +... + +``` + +## Attach to an Ingress + +Save the following as `cafe.yaml`. The file defines the sample cafe application (`coffee` and `tea` Deployments and Services) and an Ingress. The `nginx.com/policies` annotation attaches the `waf-policy` Policy to every route on the Ingress. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/hello:plain-text + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee-svc + namespace: default +spec: + selector: + app: coffee + ports: + - port: 80 + targetPort: 80 + name: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tea + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: tea + template: + metadata: + labels: + app: tea + spec: + containers: + - name: tea + image: nginxdemos/hello:plain-text + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: tea-svc + namespace: default +spec: + selector: + app: tea + ports: + - port: 80 + targetPort: 80 + name: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cafe-ingress + namespace: default + annotations: + nginx.com/policies: "waf-policy" +spec: + ingressClassName: nginx + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +``` + +Apply the file: + +```shell +kubectl apply -f cafe.yaml +``` + +Send a normal request: + +```shell +curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP \ + http://cafe.example.com:$IC_HTTP_PORT/coffee +``` + +Send a request that triggers the data guard violation: + +```shell +curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP \ + "http://cafe.example.com:$IC_HTTP_PORT/coffee/" +``` + +The response body is `Request Rejected`. + +Under PLM, the Ingress-only App Protect annotations (`appprotect.f5.com/app-protect-policy` and `appprotect.f5.com/app-protect-security-log`) aren't supported. Use the `k8s.nginx.org/v1` Policy resource and the `nginx.com/policies` annotation instead, as shown above. + +## Verify bundles on disk + +Confirm the compiled bundles are present in the ingress controller pod: + +```shell +NIC_POD=$(kubectl get pods --namespace nginx-ingress \ + --selector app.kubernetes.io/name=nginx-ingress \ + --output jsonpath='{.items[0].metadata.name}') + +kubectl exec --namespace nginx-ingress $NIC_POD --container nginx-ingress -- \ + ls -ltr /etc/app_protect/bundles/ +``` + +Expected output: + +```text +total 1860 +-rw------- 1 nginx nginx 1654 Aug 12 10:06 fetched_default_waf-policy_log_0.tgz +-rw------- 1 nginx nginx 1898698 Aug 12 13:32 fetched_default_waf-policy_policy.tgz +``` + +Check the Policy status: + +```shell +kubectl describe policy waf-policy +``` + +A `State: Valid` and `Reason: AddedOrUpdated` status confirms the bundles were fetched successfully. + +## Troubleshooting + +- **Policy status is `Warning` with reason `BundleFetchFailed`.** Run `kubectl describe appolicy --namespace security` and confirm `status.bundle.state` is `ready`. If PLM hasn't compiled the resource yet, the Policy fetch can't proceed. +- **NGINX Ingress Controller reports the referenced namespace isn't watched.** If `controller.watchNamespace` is set, include the namespace that holds the `APPolicy` and `APLogConf` resources. If `controller.watchSecretNamespace` is set, include the PLM namespace so the controller can observe storage Secret rotation. diff --git a/content/nic/install/plm-upgrade.md b/content/nic/install/plm-upgrade.md new file mode 100644 index 000000000..f186ba1db --- /dev/null +++ b/content/nic/install/plm-upgrade.md @@ -0,0 +1,233 @@ +--- +title: Upgrade NGINX Ingress Controller to F5 WAF for NGINX with PLM +weight: 210 +toc: true +f5-content-type: how-to +f5-product: F5 NGINX Ingress Controller +f5-description: > + Upgrade an existing F5 NGINX Ingress Controller deployment from in-pod App Protect policy compilation (v1beta1 CRDs) to F5 WAF for NGINX with Policy Lifecycle Management (PLM). +f5-audience: operator +--- + +Use this guide to upgrade an existing NGINX Ingress Controller + F5 WAF for NGINX deployment from in-pod App Protect policy compilation to Policy Lifecycle Management (PLM). PLM compiles `APPolicy` and `APLogConf` resources in a dedicated controller and stores the resulting bundles in an in-cluster S3-compatible object store. NGINX Ingress Controller then fetches the compiled bundles instead of compiling them in the data plane. + +Under PLM, the fields you already write on the NGINX Ingress Controller `Policy` resource (`waf.apPolicy` and `waf.securityLogs[].apLogConf`) continue to work unchanged. You don't need to rewrite the `k8s.nginx.org/v1` Policy manifest for a VirtualServer or Ingress during the upgrade. + +By the end of this guide, you'll have: + +- The PLM backend installed alongside your existing NGINX Ingress Controller deployment. +- NGINX Ingress Controller upgraded to a PLM-capable release with PLM storage configured. +- Existing `APPolicy` and `APLogConf` resources adopted by PLM and compiled into bundles. +- WAF-protected traffic served by NGINX Ingress Controller, with bundles fetched from PLM storage instead of compiled in the data plane. + +## Before you begin + +Before you start, make sure you have: + +- An existing NGINX Ingress Controller + F5 WAF for NGINX deployment running in your cluster. +- Existing `APPolicy` and `APLogConf` resources served by the `appprotect.f5.com/v1beta1` CRDs shipped with your current NGINX Ingress Controller installation. +- `kubectl` and Helm access to the cluster. +- Credentials for `private-registry.nginx.com`. + +Record your current values before you begin: + +| Value | Where to find it | +|---|---| +| NGINX Ingress Controller release name | `helm list --namespace ` | +| NGINX Ingress Controller chart version | `helm list --namespace ` | +| Existing NGINX Ingress Controller values | `helm get values --namespace ` | +| Existing `APPolicy` resources | `kubectl get appolicy --all-namespaces` | +| Existing `APLogConf` resources | `kubectl get aplogconf --all-namespaces` | + +This guide uses `nic` as the NGINX Ingress Controller release name, `nginx-ingress` as the NGINX Ingress Controller namespace, `plm-system` as the PLM namespace, and `plm` as the PLM release name. Replace these with your own values consistently throughout. + +## Deploy PLM infrastructure + +Install the PLM backend into the `plm-system` namespace. The install adds the `appprotect.f5.com/v1` versions to the existing `appprotect.f5.com` CRDs. Because the v1 versions are a superset of v1beta1, adding them doesn't affect NGINX Ingress Controller while it still watches v1beta1. + +{{< include "waf/plm-deploy-infrastructure.md" >}} + +After the install completes, PLM adopts every existing `APPolicy` and `APLogConf` resource in the cluster. It does this by adding the `appprotect.f5.com/finalizer` finalizer and compiling each resource against its current signature package. Your running NGINX Ingress Controller continues to compile the same resources in-pod. The two mechanisms operate independently until you upgrade NGINX Ingress Controller. + +Confirm PLM has compiled the existing resources: + +```shell +kubectl get appolicy --all-namespaces \ + --output custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATE:.status.bundle.state' + +kubectl get aplogconf --all-namespaces \ + --output custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATE:.status.bundle.state' +``` + +Every resource should report `STATE: ready`. If any resource isn't `ready`, inspect the PLM policy-controller logs, resolve the compilation errors, and then continue. + +## Look up the PLM storage endpoint and credentials + +NGINX Ingress Controller connects to PLM's SeaweedFS filer over S3 to fetch compiled bundles. Before you run `helm upgrade`, collect these four values from the PLM installation: + +1. **PLM storage URL**: the SeaweedFS filer endpoint (HTTPS or HTTP). +2. **Credentials Secret**: the S3 credentials Secret. The access key ID is `admin` by default. The secret access key is in the `seaweedfs_admin_secret` field. +3. **CA Secret** (HTTPS only): verifies the SeaweedFS filer certificate. +4. **Client TLS Secret** (mutual TLS only): presented by NGINX Ingress Controller when it connects to the filer. + +List the Services PLM created and identify the filer: + +```shell +kubectl get service --namespace plm-system +``` + +Expected output includes an entry similar to: + +```text +NAME TYPE CLUSTER-IP PORT(S) +plm-f5-waf-seaweed-filer ClusterIP 10.0.0.10 8333/TCP,9333/TCP,... +``` + +Assemble the URL from the service name, namespace, and port. Use `9333` for HTTPS and `8333` for HTTP: + +- HTTPS (mTLS): `https://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:9333` +- HTTP: `http://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:8333` + +List the Secrets PLM created: + +```shell +kubectl get secret --namespace plm-system +``` + +The default PLM install creates three Secrets that NGINX Ingress Controller references: + +- `plm-f5-waf-seaweedfs-auth`: SeaweedFS credentials. +- `plm-f5-waf-seaweedfs-ca-cert`: CA certificate for the HTTPS filer. +- `plm-f5-waf-seaweedfs-client-cert`: client TLS certificate for mTLS. + +Record the Secret references in `/` form. When you run `helm upgrade`, pass all four values to NGINX Ingress Controller using `--set controller.appprotect.plmStorage.*` flags. + +## Apply the NGINX Ingress Controller CRDs + +Before you run `helm upgrade`, apply the NGINX Ingress Controller CRDs from the bundled manifest for your target release. The `deploy/crds.yaml` bundle contains every CRD the controller needs (`VirtualServer`, `VirtualServerRoute`, `Policy`, `TransportServer`, `GlobalConfiguration`, `DNSEndpoint`). The bundle deliberately excludes the App Protect CRDs, which PLM owns. + +```shell +kubectl apply -f https://raw.githubusercontent.com/nginx/kubernetes-ingress/v{{< nic-version >}}/deploy/crds.yaml +``` + +## Section 1: Upgrade a VirtualServer-based deployment + +Follow this section if your existing NGINX Ingress Controller deployment routes traffic through `k8s.nginx.org/v1` VirtualServer resources that reference a Policy resource with `waf.apPolicy` and `waf.securityLogs[].apLogConf` fields. + +### 1. Upgrade NGINX Ingress Controller with PLM storage + +Upgrade the NGINX Ingress Controller release. `--reuse-values` preserves your existing configuration. The `--set` flags overlay PLM storage on top. `--skip-crds` prevents Helm from touching CRDs, because you applied the NGINX Ingress Controller CRDs in the previous step and PLM owns the App Protect CRDs. + +This example uses HTTPS PLM storage with mutual TLS. For HTTP storage, set `controller.appprotect.plmStorage.url` to `http://:8333` and omit the `caSecret` and `clientSSLSecret` flags. + +```shell +helm upgrade nic nginx-stable/nginx-ingress \ + --namespace nginx-ingress \ + --skip-crds \ + --reuse-values \ + --set controller.image.repository="private-registry.nginx.com/nginx-ic-nap-v5/nginx-plus-ingress" \ + --set controller.image.tag="{{< nic-version >}}" \ + --set controller.appprotect.plmStorage.url="https://plm-f5-waf-seaweed-filer.plm-system.svc.cluster.local:9333" \ + --set controller.appprotect.plmStorage.credentialsSecret="plm-system/plm-f5-waf-seaweedfs-auth" \ + --set controller.appprotect.plmStorage.caSecret="plm-system/plm-f5-waf-seaweedfs-ca-cert" \ + --set controller.appprotect.plmStorage.clientSSLSecret="plm-system/plm-f5-waf-seaweedfs-client-cert" \ + --set controller.appprotect.plmStorage.insecureSkipVerify=false +``` + +Wait for the rollout to complete: + +```shell +kubectl rollout status deployment/nic-nginx-ingress-controller \ + --namespace nginx-ingress \ + --timeout=180s +``` + +Confirm NGINX Ingress Controller is watching the v1 CRDs by inspecting the controller log: + +```shell +kubectl logs deployment/nic-nginx-ingress-controller \ + --namespace nginx-ingress \ + --container nginx-ingress | grep 'appprotect.f5.com/v' +``` + +Expected output includes: + +```text +Using appprotect.f5.com/v1 CRDs +``` + +### 2. Verify Policy status + +Check each WAF Policy referenced by a VirtualServer: + +```shell +kubectl get policy --all-namespaces \ + --output custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATE:.status.state,REASON:.status.reason' +``` + +Expected output after the first successful fetch: + +```text +NAMESPACE NAME STATE REASON +default waf-policy Valid AddedOrUpdated +``` + +If a Policy remains in `Warning` with `BundlePending`, the referenced `APPolicy` or `APLogConf` isn't yet `ready` in PLM. Confirm that PLM has compiled the resource, then retry. + +### 3. Verify traffic + +Send a normal request to the VirtualServer and confirm the application responds: + +```shell +curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP \ + http://webapp.example.com:$IC_HTTP_PORT/ +``` + +Then send a request that triggers the configured WAF violation and confirm the response is `Request Rejected`. + +### 4. Confirm bundles come from PLM storage + +Check the bundle files in the ingress controller pod: + +```shell +NIC_POD=$(kubectl get pods --namespace nginx-ingress \ + --selector app.kubernetes.io/name=nginx-ingress \ + --output jsonpath='{.items[0].metadata.name}') + +kubectl exec --namespace nginx-ingress $NIC_POD --container nginx-ingress -- \ + ls -l /etc/app_protect/bundles/ +``` + +Files named `fetched___policy.tgz` and `fetched___log_.tgz` confirm that the bundles were fetched from PLM storage. + +## Section 2: Upgrade an Ingress-based deployment + +Follow this section if your existing NGINX Ingress Controller deployment routes traffic through Kubernetes Ingress resources. The procedure is the same as Section 1, with one additional Ingress-specific step. + +### 1. Audit Ingress annotations + +Under PLM, NGINX Ingress Controller doesn't support the App Protect Ingress annotations: + +- `appprotect.f5.com/app-protect-policy` +- `appprotect.f5.com/app-protect-security-log` +- `appprotect.f5.com/app-protect-security-log-enable` + +An Ingress that uses these annotations is accepted but produces a warning after the upgrade. WAF isn't applied to that route. Before you turn on PLM storage, migrate every such Ingress to a `k8s.nginx.org/v1` Policy resource. + +### 2. Upgrade NGINX Ingress Controller with PLM storage + +Run the same `helm upgrade` command shown in [Section 1, step 1](#1-upgrade-nginx-ingress-controller-with-plm-storage). + +### 3. Verify Ingress traffic + +For each Ingress, send a normal request and confirm the application responds. Then send a request that matches a WAF violation and confirm the response is `Request Rejected`. + +### 4. Confirm bundles come from PLM storage + +Use the same procedure as [Section 1, step 4](#4-confirm-bundles-come-from-plm-storage). + +## Troubleshooting + +- **Policy stays in `BundlePending` after the upgrade.** The referenced `APPolicy` or `APLogConf` isn't `ready` in PLM. Run `kubectl describe appolicy ` and inspect the PLM policy-controller logs. +- **NGINX Ingress Controller reports the referenced namespace isn't watched.** If the deployment sets `controller.watchNamespace`, include the namespace of every `APPolicy` and `APLogConf` resource. Also include the PLM namespace in `controller.watchSecretNamespace` so NGINX Ingress Controller can observe storage Secret rotation. +- **Helm upgrade fails with a CRD conflict.** Confirm PLM is installed and its v1 CRDs are present, then run the upgrade with `--skip-crds`.