Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9bd5a76
docs: Add PLM infrastructure and WAF policy sections for NGF tutorial
travisamartin Aug 10, 2026
f186b23
Merge remote-tracking branch 'origin' into techdocs-5345-deploy-plm
travisamartin Aug 10, 2026
989aad4
Merge branch 'ngf-waf-plm' into techdocs-5345-deploy-plm
travisamartin Aug 10, 2026
f0536be
style: Make PLM includes product-agnostic for NGF/NIC reuse
travisamartin Aug 10, 2026
04fd349
style: Remove headings from PLM includes
travisamartin Aug 10, 2026
5604ab8
style: Restore headings in PLM includes at correct levels
travisamartin Aug 10, 2026
ee9508a
Add NGF PLM docs
sjberman Aug 10, 2026
51b563f
fix: Use concrete example values in PLM infrastructure include
travisamartin Aug 10, 2026
10c6283
style: Copy edit NGF WAF integration docs
travisamartin Aug 10, 2026
1b88338
style: Second copy edit pass on NGF WAF integration docs
travisamartin Aug 10, 2026
fa66d6e
fix: Replace non-RFC-5737 IP address across NGF docs
travisamartin Aug 10, 2026
25c48b1
Fix installation and version files
sjberman Aug 10, 2026
e7055dc
style: Align placeholders in bundle-method include with deploy include
travisamartin Aug 10, 2026
083161c
docs: Add example values table to Before you begin
travisamartin Aug 10, 2026
72ea1f7
docs: Add workflow diagram to tutorial introduction
travisamartin Aug 10, 2026
ea847cb
fix: Switch Mermaid diagram from flowchart to graph syntax
travisamartin Aug 10, 2026
ffe5a3e
docs: Simplify tutorial introduction
travisamartin Aug 10, 2026
3495abf
docs: Add persona context at role-shift points in PLM tutorial
travisamartin Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions content/includes/waf/plm-define-policy-bundle-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
f5-product: F5 WAF for NGINX
f5-files:
- content/ngf/waf-integration/get-started-plm.md
- content/nic/waf-integration/get-started-plm.md
---

The precompiled-bundle method lets you reference a `.tgz` policy bundle stored in an artifact registry (for example, Artifactory or Nexus). The Policy Controller imports the bundle and stores it in the SeaweedFS object store without recompiling it.

Use this method when:

- Your security team compiles and publishes bundles through an external pipeline.
- You want to decouple policy compilation from cluster operations.

Create an `APPolicy` resource that references your bundle:

```yaml
apiVersion: appprotect.f5.com/v1
kind: APPolicy
metadata:
name: <POLICY_NAME>
namespace: plm-system
spec:
policy:
$ref: "https://<ARTIFACT_REGISTRY_HOST>/<PATH/TO/POLICY_BUNDLE>.tgz"
```

Replace `<POLICY_NAME>`, `<ARTIFACT_REGISTRY_HOST>`, and `<PATH/TO/POLICY_BUNDLE>` with your values.

Apply the resource:

```shell
kubectl apply -f <POLICY_MANIFEST_FILE>.yaml
```

{{< call-out class="note" title="Note" >}}
The Policy Controller must be able to reach the artifact registry host over HTTPS. If the registry uses a private certificate authority, configure the Policy Controller to trust that CA. <!-- TODO: SME to confirm and document the Helm value or config mechanism for trusting a private CA on the bundle server. -->

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ohad-perets -- Can you verify this note? There's a TODO to resolve.

{{< /call-out >}}

#### Confirm the policy is ready

The Policy Controller processes the bundle and updates the `APPolicy` status. Check the `bundle.state` field:

```shell
kubectl get appolicy <POLICY_NAME> \
--namespace plm-system \
--output jsonpath='State: {.status.bundle.state}{"\n"}Bundle: {.status.bundle.location}{"\n"}isCompiled: {.status.processing.isCompiled}{"\n"}'
```

When the bundle is ready, the output looks like this:

```text
State: ready
Bundle: s3://plm-system/bundles/<POLICY_NAME>_imported_<HASH>.tgz
isCompiled: false
```

`isCompiled: false` confirms the bundle was imported as-is and not recompiled.

`bundle.state` can be one of:

| State | Meaning |
|-------|---------|
| `pending` | The Policy Controller has not yet processed the resource. |
| `processing` | The Policy Controller is importing or storing the bundle. |
| `ready` | The bundle is stored and ready to use. `bundle.location` is populated. |
| `invalid` | The bundle could not be imported. Check the status for error detail. |

#### Update a precompiled bundle

The Policy Controller does not poll the artifact registry for changes. To pick up a new version of a bundle, update the `APPolicy` resource to reference the new bundle URL (or bump its revision annotation) and re-apply it:

```shell
kubectl apply -f <UPDATED_POLICY_MANIFEST_FILE>.yaml
```
147 changes: 147 additions & 0 deletions content/includes/waf/plm-deploy-infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
f5-product: F5 WAF for NGINX
f5-files:
- content/ngf/waf-integration/get-started-plm.md
- content/nic/waf-integration/get-started-plm.md
---

The Policy Lifecycle Manager (PLM) backend runs as a Kubernetes operator. It watches WAF custom resources and compiles WAF policies into bundles. The Policy Controller delegates compilation to a separate compiler service over gRPC. The resulting bundles are stored in an embedded SeaweedFS S3-compatible object store.

F5 WAF for NGINX is installed using a separate Helm chart from your NGINX data plane. The steps in this section install only the F5 WAF for NGINX PLM components and do not affect your existing NGINX installation.

### Install the CRDs

Install the four custom resource definitions (CRDs) that the Policy Controller manages:

```shell
kubectl apply -f https://raw.githubusercontent.com/nginx/waf-policy-controller/main/manifests/1-deploy-crds.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc PLM controller creates the CRDs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would only be beneficial for an upgrade since helm doesn't upgrade CRDs.

```

Confirm all four CRDs are present:

```shell
kubectl get crd | grep appprotect.f5.com
```

Expected output:

```text
appolicies.appprotect.f5.com
aplogconfs.appprotect.f5.com
apsignatures.appprotect.f5.com
apusersigs.appprotect.f5.com
```

### Create the registry pull secret

Create a namespace for the PLM components, then create the registry pull secret using the credentials from the previous section. Replace `<JWT>` with your F5 WAF for NGINX JWT.

<!-- TODO (TECHDOCS-5342 / Prerequisites): The Story 3 AC lists four credentials: JWT, certificate, key, and registry token. Confirm with SME what the registry token is and whether it's passed separately in the Helm install or covered by the JWT above. -->

```shell
kubectl create namespace plm-system

kubectl create secret docker-registry regcred \
--namespace plm-system \
--docker-server=private-registry.nginx.com \
--docker-username=<JWT> \
--docker-password=none \
--dry-run=client --output yaml | kubectl apply -f -
```

### Install the Policy Controller

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We absolutely require a prerequisite section for PLM detailing the storage requirements and configurations and detailing the default behaviour


Create a values file for the Helm installation. Replace `<BASE64_NGINX_REPO_CRT>` and `<BASE64_NGINX_REPO_KEY>` with the base64-encoded contents of your `nginx-repo.crt` and `nginx-repo.key` files. To encode them, run:

```shell
base64 --wrap=0 < nginx-repo.crt
base64 --wrap=0 < nginx-repo.key
```

Create `/tmp/plm-values.yaml`:

```yaml
imagePullSecrets:
- name: regcred
securityUpdatesRepo:
cert: "<BASE64_NGINX_REPO_CRT>"
key: "<BASE64_NGINX_REPO_KEY>"
policyController:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't detail anywhere in this deployment guide how to configure mTLS, but we specify it in the NGF guide, with no guidance in either on how to create/ manage/ provide the CA or certificates

image:
tag: "{{< version-waf-policy-controller >}}"
compiler:
image:
tag: "{{< version-waf-policy-controller >}}"
seaweedfsOperatorConfig:
seaweedfs:
image:
tag: "{{< version-waf-policy-controller >}}"
seaweedfs-operator:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to at the very least provide a link to the full values.yaml so the user knows all the possible configuration options available.

image:
tag: "{{< version-waf-policy-controller >}}"
pullSecrets: regcred
```

Add the NGINX Helm repository and install the chart:

```shell
helm repo add nginx-stable https://helm.nginx.com/stable
helm repo update nginx-stable

helm upgrade --install plm nginx-stable/f5-waf-policy-controller \
--version {{< version-waf-policy-controller >}} \
--namespace plm-system \
--values /tmp/plm-values.yaml
```

### Verify the deployment

Wait for all PLM components to become ready. The Policy Controller's init container waits for both the compiler service and the SeaweedFS S3 endpoint to be available before it starts, so the controller pod will show `Init:0/1` until SeaweedFS is ready.

Wait for the SeaweedFS storage backend:

```shell
kubectl rollout status deployment/plm-seaweedfs-operator \
--namespace plm-system --timeout=120s

kubectl wait pods \
--selector app.kubernetes.io/name=seaweedfs \
--for=condition=Ready \
--namespace plm-system \
--timeout=180s
```

Wait for the Policy Controller:

```shell
kubectl rollout status deployment/plm-f5-waf-policy-controller \
--namespace plm-system --timeout=180s
```

Confirm all eight pods are running:

```shell
kubectl get pods --namespace plm-system
```

Expected output:

```text
NAME READY STATUS RESTARTS
plm-f5-waf-compiler-service-xxxxx 1/1 Running 0
plm-f5-waf-policy-controller-xxxxx 1/1 Running 0
plm-seaweedfs-operator-xxxxx 1/1 Running 0
plm-f5-waf-seaweed-master-0 1/1 Running 0
plm-f5-waf-seaweed-filer-0 1/1 Running 0
plm-f5-waf-seaweed-volume-0 1/1 Running 0
plm-f5-waf-seaweed-volume-1 1/1 Running 0
plm-f5-waf-seaweed-volume-2 1/1 Running 0
```

Confirm the four CRDs are present:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a troubleshooting section.


```shell
kubectl get crd | grep appprotect.f5.com
```

All eight pods running and all four CRDs present confirms the PLM backend is ready.
5 changes: 5 additions & 0 deletions content/ngf/reference/cli-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ This command runs the NGINX Gateway Fabric control plane.
| _leader-election-lock-name_ | _string_ | The name of the leader election lock. A lease object with this name will be created in the same namespace as the controller (Default: `"nginx-gateway-leader-election-lock"`). |
| _product-telemetry-disable_ | _bool_ | Disable the collection of product telemetry (Default: `false`). |
| _nginx-docker-secret_ | _list_ | The name of the NGINX docker registry Secret(s). Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). |
| _plm-storage-url_ | _string_ | The URL of the Policy Lifecycle Manager (PLM) storage service (HTTP or HTTPS). |
| _plm-storage-credentials-secret_ | _string_ | The name of the Secret containing the PLM storage S3 secret access key (`seaweedfs_admin_secret`). Must exist in the same namespace as the NGINX Gateway Fabric control plane (default: `nginx-gateway`), unless prefixed with `<NAMESPACE>/`. |
| _plm-storage-ca-secret_ | _string_ | The name of the Secret containing the CA certificate (`ca.crt`) for verifying the PLM storage TLS server certificate. Must exist in the same namespace as the NGINX Gateway Fabric control plane (default: `nginx-gateway`), unless prefixed with `<NAMESPACE>/`. |
| _plm-storage-client-ssl-secret_ | _string_ | The name of the Secret containing the client certificate and key (`tls.crt`/`tls.key`) for mutual TLS with PLM storage. Must exist in the same namespace as the NGINX Gateway Fabric control plane (default: `nginx-gateway`), unless prefixed with `<NAMESPACE>/`. |
| _plm-storage-skip-verify_ | _bool_ | Disable TLS certificate verification for PLM storage connections. Use for testing only (Default: `false`). |
| _usage-report-secret_ | _string_ | The name of the Secret containing the JWT for NGINX Plus usage reporting. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway) |
| _usage-report-endpoint_ | _string_ | The endpoint of the NGINX Plus usage reporting server. |
| _usage-report-resolver_ | _string_ | The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager. |
Expand Down
2 changes: 1 addition & 1 deletion content/ngf/traffic-security/basic-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ kubectl describe gateways.gateway.networking.k8s.io cafe-gateway
```text
Addresses:
Type: IPAddress
Value: 10.96.20.187
Value: 192.0.2.1
```

Save the public IP address and port(s) of the Gateway into shell variables:
Expand Down
2 changes: 1 addition & 1 deletion content/ngf/traffic-security/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ kubectl describe gateways.gateway.networking.k8s.io gateway
```text
Addresses:
Type: IPAddress
Value: 10.96.20.187
Value: 192.0.2.1
```

Save the public IP address and port(s) of the Gateway into shell variables:
Expand Down
2 changes: 1 addition & 1 deletion content/ngf/traffic-security/jwt-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ kubectl describe gateways.gateway.networking.k8s.io cafe-gateway
```text
Addresses:
Type: IPAddress
Value: 10.96.20.187
Value: 192.0.2.1
```

Save the public IP address and port of the Gateway into shell variables:
Expand Down
2 changes: 1 addition & 1 deletion content/ngf/traffic-security/oidc-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ kubectl describe gateways.gateway.networking.k8s.io gateway
```text
Addresses:
Type: IPAddress
Value: 10.96.20.187
Value: 192.0.2.1
```

Save the IP and port into shell variables:
Expand Down
3 changes: 2 additions & 1 deletion content/ngf/waf-integration/get-started-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ kubectl describe gateways.gateway.networking.k8s.io gateway
```text
Addresses:
Type: IPAddress
Value: 10.96.20.187
Value: 192.0.2.1
```

Save the public IP address and port(s) of the Gateway into shell variables:
Expand Down Expand Up @@ -530,6 +530,7 @@ SSN: *******6789

## Next steps

- [Get started with F5 WAF for NGINX using PLM]({{< ref "/ngf/waf-integration/get-started-plm.md" >}}) for a Kubernetes-native policy workflow using PLM.
- [Configure policy sources (NIM and NGINX One Console)]({{< ref "/ngf/waf-integration/policy-sources.md" >}}) for managed policy workflows.
- [Configure WAF settings]({{< ref "/ngf/waf-integration/configuration.md" >}}) for polling, TLS, authentication, security logging, and fail-open behavior.
- [Troubleshoot WAFPolicy status]({{< ref "/ngf/waf-integration/troubleshooting.md" >}}) if a condition is `False`.
Loading
Loading