diff --git a/Makefile b/Makefile index dc3588e32d..3e85ddab80 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,9 @@ CHART_DIR = $(SELF_DIR)charts/nginx-gateway-fabric NGINX_CONF_DIR = internal/controller/nginx/conf NJS_DIR = internal/controller/nginx/modules/src KIND_CONFIG_FILE = $(SELF_DIR)config/cluster/kind-cluster.yaml +NAP_WAF_ALPINE_VERSION = 3.19 NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=$(SELF_DIR)nginx-repo.crt --secret id=nginx-repo.key,src=$(SELF_DIR)nginx-repo.key +NGINX_DOCKER_BUILD_NAP_WAF_ARGS = --build-arg ALPINE_VERSION=$(NAP_WAF_ALPINE_VERSION) --build-arg INCLUDE_NAP_WAF=true BUILD_AGENT = local PROD_TELEMETRY_ENDPOINT = oss.edge.df.f5.com:443 @@ -77,6 +79,9 @@ build-images: build-ngf-image build-nginx-image ## Build the NGF and nginx docke .PHONY: build-images-with-plus build-images-with-plus: build-ngf-image build-nginx-plus-image ## Build the NGF and NGINX Plus docker images +.PHONY: build-images-nap-waf +build-images-with-nap-waf: build-ngf-image build-nginx-plus-image-with-nap-waf ## Build the NGF and NGINX Plus with WAF docker images + .PHONY: build-prod-ngf-image build-prod-ngf-image: TELEMETRY_ENDPOINT=$(PROD_TELEMETRY_ENDPOINT) build-prod-ngf-image: build-ngf-image ## Build the NGF docker image for production @@ -99,6 +104,13 @@ build-prod-nginx-plus-image: build-nginx-plus-image ## Build the custom nginx pl build-nginx-plus-image: check-for-docker ## Build the custom nginx plus image docker build --platform linux/$(GOARCH) $(strip $(NGINX_DOCKER_BUILD_OPTIONS)) $(strip $(NGINX_DOCKER_BUILD_PLUS_ARGS)) -f $(SELF_DIR)build/Dockerfile.nginxplus -t $(strip $(NGINX_PLUS_PREFIX)):$(strip $(TAG)) $(strip $(SELF_DIR)) +.PHONY: build-nginx-plus-image-with-nap-waf +build-nginx-plus-image-with-nap-waf: check-for-docker ## Build the custom nginx plus image with NAP WAF. Note that arm is NOT supported. + @if [ $(GOARCH) = "arm64" ]; then \ + echo "\033[0;31mIMPORTANT:\033[0m The nginx-plus-waf image cannot be built for arm64 architecture and will be built for amd64."; \ + fi + docker build --platform linux/amd64 $(strip $(NGINX_DOCKER_BUILD_OPTIONS)) $(strip $(NGINX_DOCKER_BUILD_PLUS_ARGS)) $(strip $(NGINX_DOCKER_BUILD_NAP_WAF_ARGS)) -f $(SELF_DIR)build/Dockerfile.nginxplus -t $(strip $(NGINX_PLUS_PREFIX)):$(strip $(TAG)) $(strip $(SELF_DIR)) + .PHONY: check-for-docker check-for-docker: ## Check if Docker is installed @docker -v || (code=$$?; printf "\033[0;31mError\033[0m: there was a problem with Docker\n"; exit $$code) diff --git a/apis/v1alpha2/nginxproxy_types.go b/apis/v1alpha2/nginxproxy_types.go index 43b509d06d..bf77daf511 100644 --- a/apis/v1alpha2/nginxproxy_types.go +++ b/apis/v1alpha2/nginxproxy_types.go @@ -72,12 +72,35 @@ type NginxProxySpec struct { // // +optional DisableHTTP2 *bool `json:"disableHTTP2,omitempty"` + // WAF enables NGINX App Protect WAF functionality. + // When enabled, NGINX Gateway Fabric will deploy additional WAF containers + // (waf-enforcer and waf-config-mgr) alongside the main NGINX container. + // Default is "disabled". + // + // +optional + // +kubebuilder:default:=disabled + WAF *WAFState `json:"waf,omitempty"` // Kubernetes contains the configuration for the NGINX Deployment and Service Kubernetes objects. // // +optional Kubernetes *KubernetesSpec `json:"kubernetes,omitempty"` } +// WAFState defines the state of WAF functionality. +// +// +kubebuilder:validation:Enum=enabled;disabled +type WAFState string + +const ( + // WAFEnabled enables NGINX App Protect WAF functionality. + // This will deploy additional containers for WAF enforcement and configuration management. + WAFEnabled WAFState = "enabled" + + // WAFDisabled disables NGINX App Protect WAF functionality. + // Only the standard NGINX container will be deployed. + WAFDisabled WAFState = "disabled" +) + // Telemetry specifies the OpenTelemetry configuration. type Telemetry struct { // DisabledFeatures specifies OpenTelemetry features to be disabled. @@ -388,6 +411,12 @@ type DeploymentSpec struct { // +optional Replicas *int32 `json:"replicas,omitempty"` + // WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + // These containers are only deployed when WAF is enabled in the NginxProxy spec. + // + // +optional + WAFContainers *WAFContainerSpec `json:"wafContainers,omitempty"` + // Pod defines Pod-specific fields. // // +optional @@ -401,6 +430,12 @@ type DeploymentSpec struct { // DaemonSet is the configuration for the NGINX DaemonSet. type DaemonSetSpec struct { + // WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + // These containers are only deployed when WAF is enabled in the NginxProxy spec. + // + // +optional + WAFContainers *WAFContainerSpec `json:"wafContainers,omitempty"` + // Pod defines Pod-specific fields. // // +optional @@ -485,6 +520,40 @@ type ContainerSpec struct { VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } +// WAFContainerSpec defines the container specifications for NGINX App Protect WAF v5. +// NAP v5 requires two additional containers: waf-enforcer and waf-config-mgr. +type WAFContainerSpec struct { + // Enforcer defines the configuration for the WAF enforcer container. + // This container performs the actual WAF enforcement and policy application. + // + // +optional + Enforcer *WAFContainerConfig `json:"enforcer,omitempty"` + + // ConfigManager defines the configuration for the WAF configuration manager container. + // This container manages policy configuration and communication with the enforcer. + // + // +optional + ConfigManager *WAFContainerConfig `json:"configManager,omitempty"` +} + +// WAFContainerConfig defines the configuration for a single WAF container. +type WAFContainerConfig struct { + // Image is the container image to use for this WAF container. + // + // +optional + Image *Image `json:"image,omitempty"` + + // Resources describes the compute resource requirements for this WAF container. + // + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + + // VolumeMounts describe the mounting of Volumes within the WAF container. + // + // +optional + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` +} + // Image is the NGINX image to use. type Image struct { // Repository is the image path. diff --git a/apis/v1alpha2/zz_generated.deepcopy.go b/apis/v1alpha2/zz_generated.deepcopy.go index 4b0f8bf9f8..3a48f3c5e9 100644 --- a/apis/v1alpha2/zz_generated.deepcopy.go +++ b/apis/v1alpha2/zz_generated.deepcopy.go @@ -56,6 +56,11 @@ func (in *ContainerSpec) DeepCopy() *ContainerSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DaemonSetSpec) DeepCopyInto(out *DaemonSetSpec) { *out = *in + if in.WAFContainers != nil { + in, out := &in.WAFContainers, &out.WAFContainers + *out = new(WAFContainerSpec) + (*in).DeepCopyInto(*out) + } in.Pod.DeepCopyInto(&out.Pod) in.Container.DeepCopyInto(&out.Container) } @@ -78,6 +83,11 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { *out = new(int32) **out = **in } + if in.WAFContainers != nil { + in, out := &in.WAFContainers, &out.WAFContainers + *out = new(WAFContainerSpec) + (*in).DeepCopyInto(*out) + } in.Pod.DeepCopyInto(&out.Pod) in.Container.DeepCopyInto(&out.Container) } @@ -333,6 +343,11 @@ func (in *NginxProxySpec) DeepCopyInto(out *NginxProxySpec) { *out = new(bool) **out = **in } + if in.WAF != nil { + in, out := &in.WAF, &out.WAF + *out = new(WAFState) + **out = **in + } if in.Kubernetes != nil { in, out := &in.Kubernetes, &out.Kubernetes *out = new(KubernetesSpec) @@ -698,3 +713,60 @@ func (in *Tracing) DeepCopy() *Tracing { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFContainerConfig) DeepCopyInto(out *WAFContainerConfig) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(Image) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]v1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFContainerConfig. +func (in *WAFContainerConfig) DeepCopy() *WAFContainerConfig { + if in == nil { + return nil + } + out := new(WAFContainerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFContainerSpec) DeepCopyInto(out *WAFContainerSpec) { + *out = *in + if in.Enforcer != nil { + in, out := &in.Enforcer, &out.Enforcer + *out = new(WAFContainerConfig) + (*in).DeepCopyInto(*out) + } + if in.ConfigManager != nil { + in, out := &in.ConfigManager, &out.ConfigManager + *out = new(WAFContainerConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFContainerSpec. +func (in *WAFContainerSpec) DeepCopy() *WAFContainerSpec { + if in == nil { + return nil + } + out := new(WAFContainerSpec) + in.DeepCopyInto(out) + return out +} diff --git a/build/Dockerfile.nginxplus b/build/Dockerfile.nginxplus index ece5d6c453..566a7600cb 100644 --- a/build/Dockerfile.nginxplus +++ b/build/Dockerfile.nginxplus @@ -1,14 +1,20 @@ # syntax=docker/dockerfile:1.16 + +# renovate: datasource=docker depName=alpine +ARG ALPINE_VERSION=3.21 + FROM scratch AS nginx-files # the following links can be replaced with local files if needed, i.e. ADD --chown=101:1001 ADD --link --chown=101:1001 https://cs.nginx.com/static/keys/nginx_signing.rsa.pub nginx_signing.rsa.pub -FROM alpine:3.21 +FROM alpine:${ALPINE_VERSION} ARG NGINX_PLUS_VERSION=R34 # renovate: datasource=github-tags depName=nginx/agent extractVersion=^v?(?.*)$ ARG NGINX_AGENT_VERSION=3.0.0 +ARG APP_PROTECT_VERSION=34.5.342 +ARG INCLUDE_NAP_WAF=false ARG NJS_DIR ARG NGINX_CONF_DIR ARG BUILD_AGENT @@ -20,6 +26,10 @@ RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/apk/cert.pem,mode=0644 \ && adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx \ && printf "%s\n" "https://pkgs.nginx.com/plus/${NGINX_PLUS_VERSION}/alpine/v$(grep -E -o '^[0-9]+\.[0-9]+' /etc/alpine-release)/main" >> /etc/apk/repositories \ && printf "%s\n" "https://pkgs.nginx.com/nginx-agent/alpine/v$(egrep -o '^[0-9]+\.[0-9]+' /etc/alpine-release)/main" >> /etc/apk/repositories \ + && if [ "${INCLUDE_NAP_WAF}" = "true" ]; then \ + printf "%s\n" "https://pkgs.nginx.com/app-protect-x-plus/alpine/v$(grep -E -o '^[0-9]+\.[0-9]+' /etc/alpine-release)/main" >> /etc/apk/repositories \ + && apk add --no-cache app-protect-module-plus~=${APP_PROTECT_VERSION}; \ + fi \ && apk add --no-cache nginx-plus nginx-plus-module-njs nginx-plus-module-otel nginx-agent=${NGINX_AGENT_VERSION} RUN apk add --no-cache libcap bash \ @@ -45,4 +55,5 @@ USER 101:1001 LABEL org.nginx.ngf.image.build.agent="${BUILD_AGENT}" +ENV USE_NAP_WAF=${INCLUDE_NAP_WAF} ENTRYPOINT ["/agent/entrypoint.sh"] diff --git a/build/entrypoint.sh b/build/entrypoint.sh index 9e9552b338..d562eede0d 100755 --- a/build/entrypoint.sh +++ b/build/entrypoint.sh @@ -27,6 +27,11 @@ trap 'handle_quit' QUIT rm -rf /var/run/nginx/*.sock +# Bootstrap the necessary app protect files +if [ "${USE_NAP_WAF:-false}" = "true" ]; then + touch /opt/app_protect/bd_config/policy_path.map +fi + # Launch nginx echo "starting nginx ..." diff --git a/charts/nginx-gateway-fabric/README.md b/charts/nginx-gateway-fabric/README.md index 65a006b592..4576984a7d 100644 --- a/charts/nginx-gateway-fabric/README.md +++ b/charts/nginx-gateway-fabric/README.md @@ -259,7 +259,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `certGenerator.serverTLSSecretName` | The name of the Secret containing TLS CA, certificate, and key for the NGINX Gateway Fabric control plane to securely communicate with the NGINX Agent. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `"server-tls"` | | `clusterDomain` | The DNS cluster domain of your Kubernetes cluster. | string | `"cluster.local"` | | `gateways` | A list of Gateway objects. View https://gateway-api.sigs.k8s.io/reference/spec/#gateway for full Gateway reference. | list | `[]` | -| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` | +| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false},"wafContainers":{}}` | | `nginx.config` | The configuration for the data plane that is contained in the NginxProxy resource. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` | | `nginx.container` | The container configuration for the NGINX container. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` | | `nginx.debug` | Enable debugging for NGINX. Uses the nginx-debug binary. The NGINX error log level should be set to debug in the NginxProxy resource. | bool | `false` | @@ -283,6 +283,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `nginx.usage.resolver` | The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager. | string | `""` | | `nginx.usage.secretName` | 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). | string | `"nplus-license"` | | `nginx.usage.skipVerify` | Disable client verification of the NGINX Plus usage reporting server certificate. | bool | `false` | +| `nginx.wafContainers` | Configuration for NGINX App Protect WAF v5 containers. These containers are only deployed when WAF is enabled via nginx.config.waf: "Enabled". All settings are optional overrides - defaults are provided by NGF. | object | `{}` | | `nginxGateway` | The nginxGateway section contains configuration for the NGINX Gateway Fabric control plane deployment. | object | `{"affinity":{},"config":{"logging":{"level":"info"}},"configAnnotations":{},"extraVolumeMounts":[],"extraVolumes":[],"gatewayClassAnnotations":{},"gatewayClassName":"nginx","gatewayControllerName":"gateway.nginx.org/nginx-gateway-controller","gwAPIExperimentalFeatures":{"enable":false},"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric","tag":"edge"},"kind":"deployment","labels":{},"leaderElection":{"enable":true,"lockName":""},"lifecycle":{},"metrics":{"enable":true,"port":9113,"secure":false},"nodeSelector":{},"podAnnotations":{},"productTelemetry":{"enable":true},"readinessProbe":{"enable":true,"initialDelaySeconds":3,"port":8081},"replicas":1,"resources":{},"service":{"annotations":{}},"serviceAccount":{"annotations":{},"imagePullSecret":"","imagePullSecrets":[],"name":""},"snippetsFilters":{"enable":false},"terminationGracePeriodSeconds":30,"tolerations":[],"topologySpreadConstraints":[]}` | | `nginxGateway.affinity` | The affinity of the NGINX Gateway Fabric control plane pod. | object | `{}` | | `nginxGateway.config.logging.level` | Log level. | string | `"info"` | diff --git a/charts/nginx-gateway-fabric/templates/nginxproxy.yaml b/charts/nginx-gateway-fabric/templates/nginxproxy.yaml index b5e33292c8..7605e7af17 100644 --- a/charts/nginx-gateway-fabric/templates/nginxproxy.yaml +++ b/charts/nginx-gateway-fabric/templates/nginxproxy.yaml @@ -26,6 +26,10 @@ spec: {{- if .Values.nginx.debug }} debug: {{ .Values.nginx.debug }} {{- end }} + {{- if and .Values.nginx.wafContainers }} + wafContainers: + {{- toYaml .Values.nginx.wafContainers | nindent 8 }} + {{- end }} {{- end }} {{- if eq .Values.nginx.kind "daemonSet" }} daemonSet: @@ -42,6 +46,10 @@ spec: {{- if .Values.nginx.debug }} debug: {{ .Values.nginx.debug }} {{- end }} + {{- if and .Values.nginx.wafContainers }} + wafContainers: + {{- toYaml .Values.nginx.wafContainers | nindent 8 }} + {{- end }} {{- end }} {{- if .Values.nginx.service }} service: diff --git a/charts/nginx-gateway-fabric/values.schema.json b/charts/nginx-gateway-fabric/values.schema.json index f78fedae4d..d0a4a7b3d3 100644 --- a/charts/nginx-gateway-fabric/values.schema.json +++ b/charts/nginx-gateway-fabric/values.schema.json @@ -268,6 +268,15 @@ }, "required": [], "type": "object" + }, + "waf": { + "description": "WAF enables NGINX App Protect WAF functionality.", + "enum": [ + "enabled", + "disabled" + ], + "required": [], + "type": "string" } }, "required": [], @@ -488,6 +497,12 @@ "required": [], "title": "usage", "type": "object" + }, + "wafContainers": { + "description": "Configuration for NGINX App Protect WAF v5 containers.\nThese containers are only deployed when WAF is enabled via nginx.config.waf: \"Enabled\".\nAll settings are optional overrides - defaults are provided by NGF.", + "required": [], + "title": "wafContainers", + "type": "object" } }, "required": [], diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index cf8f826981..97fc4b82f7 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -367,6 +367,12 @@ nginx: # - IPAddress # value: # type: string + # waf: + # type: string + # description: WAF enables NGINX App Protect WAF functionality. + # enum: + # - enabled + # - disabled # @schema # -- The configuration for the data plane that is contained in the NginxProxy resource. This is applied globally to all Gateways # managed by this instance of NGINX Gateway Fabric. @@ -406,6 +412,33 @@ nginx: # -- extraVolumeMounts are the additional volume mounts for the NGINX container. # extraVolumeMounts: [] + # -- Configuration for NGINX App Protect WAF v5 containers. + # These containers are only deployed when WAF is enabled via nginx.config.waf: "Enabled". + # All settings are optional overrides - defaults are provided by NGF. + wafContainers: {} + # -- WAF Enforcer container configuration. + # enforcer: + # image: {} + + # -- The resource requirements of the WAF Enforcer container. + # resources: {} + # + # # -- Additional volume mounts for the WAF enforcer container. + # # NAP v5 shared volumes are automatically configured by NGF. + # volumeMounts: [] + + # -- WAF Configuration Manager container configuration. + # configManager: + # # -- Container image configuration + # image: {} + # + # # -- The resource requirements of the WAF config manager container. + # resources: {} + + # # -- Additional volume mounts for the WAF config manager container. + # # NAP v5 shared volumes are automatically configured by NGF. + # volumeMounts: [] + # -- The service configuration for the NGINX data plane. This is applied globally to all Gateways managed by this # instance of NGINX Gateway Fabric. service: diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml index 9947e34eb7..df9ac5540d 100644 --- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml +++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml @@ -3453,6 +3453,322 @@ spec: type: object type: array type: object + wafContainers: + description: |- + WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + These containers are only deployed when WAF is enabled in the NginxProxy spec. + properties: + configManager: + description: |- + ConfigManager defines the configuration for the WAF configuration manager container. + This container manages policy configuration and communication with the enforcer. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + enforcer: + description: |- + Enforcer defines the configuration for the WAF enforcer container. + This container performs the actual WAF enforcement and policy application. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + type: object type: object deployment: description: |- @@ -6843,6 +7159,322 @@ spec: description: Number of desired Pods. format: int32 type: integer + wafContainers: + description: |- + WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + These containers are only deployed when WAF is enabled in the NginxProxy spec. + properties: + configManager: + description: |- + ConfigManager defines the configuration for the WAF configuration manager container. + This container manages policy configuration and communication with the enforcer. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + enforcer: + description: |- + Enforcer defines the configuration for the WAF enforcer container. + This container performs the actual WAF enforcement and policy application. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + type: object type: object service: description: Service is the configuration for the NGINX Service. @@ -7138,6 +7770,17 @@ spec: - key x-kubernetes-list-type: map type: object + waf: + default: disabled + description: |- + WAF enables NGINX App Protect WAF functionality. + When enabled, NGINX Gateway Fabric will deploy additional WAF containers + (waf-enforcer and waf-config-mgr) alongside the main NGINX container. + Default is "disabled". + enum: + - enabled + - disabled + type: string type: object required: - spec diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 7517ce1c4a..2c691b9def 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -4038,6 +4038,322 @@ spec: type: object type: array type: object + wafContainers: + description: |- + WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + These containers are only deployed when WAF is enabled in the NginxProxy spec. + properties: + configManager: + description: |- + ConfigManager defines the configuration for the WAF configuration manager container. + This container manages policy configuration and communication with the enforcer. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + enforcer: + description: |- + Enforcer defines the configuration for the WAF enforcer container. + This container performs the actual WAF enforcement and policy application. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + type: object type: object deployment: description: |- @@ -7428,6 +7744,322 @@ spec: description: Number of desired Pods. format: int32 type: integer + wafContainers: + description: |- + WAFContainers defines container specifications for NGINX App Protect WAF v5 containers. + These containers are only deployed when WAF is enabled in the NginxProxy spec. + properties: + configManager: + description: |- + ConfigManager defines the configuration for the WAF configuration manager container. + This container manages policy configuration and communication with the enforcer. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + enforcer: + description: |- + Enforcer defines the configuration for the WAF enforcer container. + This container performs the actual WAF enforcement and policy application. + properties: + image: + description: Image is the container image to use for + this WAF container. + properties: + pullPolicy: + default: IfNotPresent + description: PullPolicy describes a policy for + if/when to pull a container image. + enum: + - Always + - Never + - IfNotPresent + type: string + repository: + description: |- + Repository is the image path. + Default is ghcr.io/nginx/nginx-gateway-fabric/nginx. + type: string + tag: + description: Tag is the image tag to use. Default + matches the tag of the control plane. + type: string + type: object + resources: + description: Resources describes the compute resource + requirements for this WAF container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: VolumeMounts describe the mounting of + Volumes within the WAF container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + type: object + type: object type: object service: description: Service is the configuration for the NGINX Service. @@ -7723,6 +8355,17 @@ spec: - key x-kubernetes-list-type: map type: object + waf: + default: disabled + description: |- + WAF enables NGINX App Protect WAF functionality. + When enabled, NGINX Gateway Fabric will deploy additional WAF containers + (waf-enforcer and waf-config-mgr) alongside the main NGINX container. + Default is "disabled". + enum: + - enabled + - disabled + type: string type: object required: - spec diff --git a/internal/controller/nginx/config/base_http_config.go b/internal/controller/nginx/config/base_http_config.go index f808e86b3e..9a755af9da 100644 --- a/internal/controller/nginx/config/base_http_config.go +++ b/internal/controller/nginx/config/base_http_config.go @@ -13,6 +13,7 @@ var baseHTTPTemplate = gotemplate.Must(gotemplate.New("baseHttp").Parse(baseHTTP type httpConfig struct { Includes []shared.Include HTTP2 bool + WAF bool } func executeBaseHTTPConfig(conf dataplane.Configuration) []executeResult { @@ -21,6 +22,7 @@ func executeBaseHTTPConfig(conf dataplane.Configuration) []executeResult { hc := httpConfig{ HTTP2: conf.BaseHTTPConfig.HTTP2, Includes: includes, + WAF: conf.WAF.Enabled, } results := make([]executeResult, 0, len(includes)+1) diff --git a/internal/controller/nginx/config/base_http_config_template.go b/internal/controller/nginx/config/base_http_config_template.go index 5163904e26..fb4f81a818 100644 --- a/internal/controller/nginx/config/base_http_config_template.go +++ b/internal/controller/nginx/config/base_http_config_template.go @@ -3,6 +3,10 @@ package config const baseHTTPTemplateText = ` {{- if .HTTP2 }}http2 on;{{ end }} +{{ if .WAF -}} +app_protect_enforcer_address 127.0.0.1:50000; +{{ end -}} + # Set $gw_api_compliant_host variable to the value of $http_host unless $http_host is empty, then set it to the value # of $host. We prefer $http_host because it contains the original value of the host header, which is required by the # Gateway API. However, in an HTTP/1.0 request, it's possible that $http_host can be empty. In this case, we will use diff --git a/internal/controller/nginx/config/base_http_config_test.go b/internal/controller/nginx/config/base_http_config_test.go index 31cc7aff52..7f65404b33 100644 --- a/internal/controller/nginx/config/base_http_config_test.go +++ b/internal/controller/nginx/config/base_http_config_test.go @@ -58,6 +58,51 @@ func TestExecuteBaseHttp_HTTP2(t *testing.T) { } } +func TestExecuteBaseHttp_WAF(t *testing.T) { + t.Parallel() + confOn := dataplane.Configuration{ + WAF: dataplane.WAFConfig{ + Enabled: true, + }, + } + + confOff := dataplane.Configuration{ + WAF: dataplane.WAFConfig{ + Enabled: false, + }, + } + + expSubStr := "app_protect_enforcer_address 127.0.0.1:50000;" + + tests := []struct { + name string + conf dataplane.Configuration + expCount int + }{ + { + name: "waf on", + conf: confOn, + expCount: 1, + }, + { + name: "waf off", + expCount: 0, + conf: confOff, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + res := executeBaseHTTPConfig(test.conf) + g.Expect(res).To(HaveLen(1)) + g.Expect(test.expCount).To(Equal(strings.Count(string(res[0].data), expSubStr))) + }) + } +} + func TestExecuteBaseHttp_Snippets(t *testing.T) { t.Parallel() diff --git a/internal/controller/nginx/config/main_config_template.go b/internal/controller/nginx/config/main_config_template.go index 2811a4f77d..d0dca3ca6b 100644 --- a/internal/controller/nginx/config/main_config_template.go +++ b/internal/controller/nginx/config/main_config_template.go @@ -5,6 +5,10 @@ const mainConfigTemplateText = ` load_module modules/ngx_otel_module.so; {{ end -}} +{{ if .Conf.WAF.Enabled -}} +load_module modules/ngx_http_app_protect_module.so; +{{ end -}} + error_log stderr {{ .Conf.Logging.ErrorLevel }}; {{ range $i := .Includes -}} diff --git a/internal/controller/nginx/config/main_config_test.go b/internal/controller/nginx/config/main_config_test.go index 5c3f6dcfdf..516668979e 100644 --- a/internal/controller/nginx/config/main_config_test.go +++ b/internal/controller/nginx/config/main_config_test.go @@ -56,6 +56,55 @@ func TestExecuteMainConfig_Telemetry(t *testing.T) { } } +func TestExecuteMainConfig_Waf(t *testing.T) { + t.Parallel() + + wafOff := dataplane.Configuration{ + WAF: dataplane.WAFConfig{ + Enabled: false, + }, + } + wafOn := dataplane.Configuration{ + WAF: dataplane.WAFConfig{ + Enabled: true, + }, + } + loadModuleDirective := "load_module modules/ngx_http_app_protect_module.so;" + + tests := []struct { + name string + conf dataplane.Configuration + expLoadModuleDirective bool + }{ + { + name: "waf off", + conf: wafOff, + expLoadModuleDirective: false, + }, + { + name: "waf on", + conf: wafOn, + expLoadModuleDirective: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + res := executeMainConfig(test.conf) + g.Expect(res).To(HaveLen(1)) + g.Expect(res[0].dest).To(Equal(mainIncludesConfigFile)) + if test.expLoadModuleDirective { + g.Expect(res[0].data).To(ContainSubstring(loadModuleDirective)) + } else { + g.Expect(res[0].data).ToNot(ContainSubstring(loadModuleDirective)) + } + }) + } +} + func TestExecuteMainConfig_Logging(t *testing.T) { t.Parallel() diff --git a/internal/controller/provisioner/objects.go b/internal/controller/provisioner/objects.go index 3b43ac8b66..9cd5702d32 100644 --- a/internal/controller/provisioner/objects.go +++ b/internal/controller/provisioner/objects.go @@ -36,6 +36,17 @@ const ( defaultNginxImagePath = "ghcr.io/nginx/nginx-gateway-fabric/nginx" defaultNginxPlusImagePath = "private-registry.nginx.com/nginx-gateway-fabric/nginx-plus" defaultImagePullPolicy = corev1.PullIfNotPresent + + // WAF container defaults. + defaultWAFEnforcerImagePath = "private-registry.nginx.com/nap/waf-enforcer" + defaultWAFConfigMgrImagePath = "private-registry.nginx.com/nap/waf-config-mgr" + // FIXME(ciarams87): Figure out best way to handle WAF image tags. + defaultWAFImageTag = "5.6.0" + + // WAF shared volume names. + appProtectBundlesVolumeName = "app-protect-bundles" + appProtectConfigVolumeName = "app-protect-config" + appProtectBdConfigVolumeName = "app-protect-bd-config" ) var emptyDirVolumeSource = corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}} @@ -553,7 +564,7 @@ func (p *NginxProvisioner) buildNginxDeployment( return deployment } -//nolint:gocyclo // will refactor at some point +// buildNginxPodTemplateSpec builds the complete pod template spec. func (p *NginxProvisioner) buildNginxPodTemplateSpec( objectMeta metav1.ObjectMeta, nProxyCfg *graph.EffectiveNginxProxy, @@ -566,20 +577,63 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec( caSecretName string, clientSSLSecretName string, ) corev1.PodTemplateSpec { + // Build container ports and pod annotations + containerPorts, podAnnotations := p.buildContainerPortsAndAnnotations(ports, nProxyCfg, objectMeta.Annotations) + + // Build NGINX container + nginxContainer := p.buildNginxContainer(containerPorts, nProxyCfg) + + // Build base volumes + volumes := p.buildBaseVolumes(ngxIncludesConfigMapName, ngxAgentConfigMapName, agentTLSSecretName) + + // Build containers list + containers := []corev1.Container{nginxContainer} + + // Configure WAF if enabled + if graph.WAFEnabledForNginxProxy(nProxyCfg) { + containers, volumes = p.configureWAF(containers, volumes, nProxyCfg) + } + + // Build init containers + initContainers := p.buildInitContainers(nProxyCfg) + + // Create base pod template spec + spec := p.buildBasePodTemplateSpec(objectMeta, podAnnotations, containers, initContainers, volumes) + + // Apply user configuration overrides + p.applyUserConfiguration(&spec, nProxyCfg) + + // Configure image pull secrets + p.configureImagePullSecrets(&spec, dockerSecretNames) + + // Configure NGINX Plus if enabled + if p.cfg.Plus { + p.configureNginxPlus(&spec, jwtSecretName, caSecretName, clientSSLSecretName) + } + + return spec +} + +// buildContainerPortsAndAnnotations builds container ports and pod annotations. +func (p *NginxProvisioner) buildContainerPortsAndAnnotations( + ports map[int32]struct{}, + nProxyCfg *graph.EffectiveNginxProxy, + baseAnnotations map[string]string, +) ([]corev1.ContainerPort, map[string]string) { containerPorts := make([]corev1.ContainerPort, 0, len(ports)) for port := range ports { - containerPort := corev1.ContainerPort{ + containerPorts = append(containerPorts, corev1.ContainerPort{ Name: fmt.Sprintf("port-%d", port), ContainerPort: port, - } - containerPorts = append(containerPorts, containerPort) + }) } podAnnotations := make(map[string]string) - maps.Copy(podAnnotations, objectMeta.Annotations) + maps.Copy(podAnnotations, baseAnnotations) - metricsPort := config.DefaultNginxMetricsPort + // Add metrics port if enabled if port, enabled := graph.MetricsEnabledForNginxProxy(nProxyCfg); enabled { + metricsPort := config.DefaultNginxMetricsPort if port != nil { metricsPort = *port } @@ -599,191 +653,242 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec( return containerPorts[i].ContainerPort < containerPorts[j].ContainerPort }) + return containerPorts, podAnnotations +} + +// buildNginxContainer builds the base NGINX container. +func (p *NginxProvisioner) buildNginxContainer( + containerPorts []corev1.ContainerPort, + nProxyCfg *graph.EffectiveNginxProxy, +) corev1.Container { image, pullPolicy := p.buildImage(nProxyCfg) - tokenAudience := fmt.Sprintf("%s.%s.svc", p.cfg.GatewayPodConfig.ServiceName, p.cfg.GatewayPodConfig.Namespace) - spec := corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: objectMeta.Labels, - Annotations: podAnnotations, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "nginx", - Image: image, - ImagePullPolicy: pullPolicy, - Ports: containerPorts, - SecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"NET_BIND_SERVICE"}, - Drop: []corev1.Capability{"ALL"}, - }, - ReadOnlyRootFilesystem: helpers.GetPointer(true), - RunAsGroup: helpers.GetPointer[int64](1001), - RunAsUser: helpers.GetPointer[int64](101), - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - VolumeMounts: []corev1.VolumeMount{ - {MountPath: "/etc/nginx-agent", Name: "nginx-agent"}, - {MountPath: "/var/run/secrets/ngf", Name: "nginx-agent-tls"}, - {MountPath: "/var/run/secrets/ngf/serviceaccount", Name: "token"}, - {MountPath: "/var/log/nginx-agent", Name: "nginx-agent-log"}, - {MountPath: "/var/lib/nginx-agent", Name: "nginx-agent-lib"}, - {MountPath: "/etc/nginx/conf.d", Name: "nginx-conf"}, - {MountPath: "/etc/nginx/stream-conf.d", Name: "nginx-stream-conf"}, - {MountPath: "/etc/nginx/main-includes", Name: "nginx-main-includes"}, - {MountPath: "/etc/nginx/secrets", Name: "nginx-secrets"}, - {MountPath: "/var/run/nginx", Name: "nginx-run"}, - {MountPath: "/var/cache/nginx", Name: "nginx-cache"}, - {MountPath: "/etc/nginx/includes", Name: "nginx-includes"}, - }, - }, + return corev1.Container{ + Name: "nginx", + Image: image, + ImagePullPolicy: pullPolicy, + Ports: containerPorts, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_BIND_SERVICE"}, + Drop: []corev1.Capability{"ALL"}, }, - InitContainers: []corev1.Container{ - { - Name: "init", - Image: p.cfg.GatewayPodConfig.Image, - ImagePullPolicy: pullPolicy, - Command: []string{ - "/usr/bin/gateway", - "initialize", - "--source", "/agent/nginx-agent.conf", - "--destination", "/etc/nginx-agent", - "--source", "/includes/main.conf", - "--destination", "/etc/nginx/main-includes", - }, - Env: []corev1.EnvVar{ + ReadOnlyRootFilesystem: helpers.GetPointer(true), + RunAsGroup: helpers.GetPointer[int64](1001), + RunAsUser: helpers.GetPointer[int64](101), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + {MountPath: "/etc/nginx-agent", Name: "nginx-agent"}, + {MountPath: "/var/run/secrets/ngf", Name: "nginx-agent-tls"}, + {MountPath: "/var/run/secrets/ngf/serviceaccount", Name: "token"}, + {MountPath: "/var/log/nginx-agent", Name: "nginx-agent-log"}, + {MountPath: "/var/lib/nginx-agent", Name: "nginx-agent-lib"}, + {MountPath: "/etc/nginx/conf.d", Name: "nginx-conf"}, + {MountPath: "/etc/nginx/stream-conf.d", Name: "nginx-stream-conf"}, + {MountPath: "/etc/nginx/main-includes", Name: "nginx-main-includes"}, + {MountPath: "/etc/nginx/secrets", Name: "nginx-secrets"}, + {MountPath: "/var/run/nginx", Name: "nginx-run"}, + {MountPath: "/var/cache/nginx", Name: "nginx-cache"}, + {MountPath: "/etc/nginx/includes", Name: "nginx-includes"}, + }, + } +} + +// buildBaseVolumes builds the base volumes needed for NGINX. +func (p *NginxProvisioner) buildBaseVolumes( + ngxIncludesConfigMapName string, + ngxAgentConfigMapName string, + agentTLSSecretName string, +) []corev1.Volume { + tokenAudience := fmt.Sprintf("%s.%s.svc", p.cfg.GatewayPodConfig.ServiceName, p.cfg.GatewayPodConfig.Namespace) + + return []corev1.Volume{ + { + Name: "token", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ { - Name: "POD_UID", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.uid", - }, + ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ + Path: "token", + Audience: tokenAudience, }, }, }, - VolumeMounts: []corev1.VolumeMount{ - {MountPath: "/agent", Name: "nginx-agent-config"}, - {MountPath: "/etc/nginx-agent", Name: "nginx-agent"}, - {MountPath: "/includes", Name: "nginx-includes-bootstrap"}, - {MountPath: "/etc/nginx/main-includes", Name: "nginx-main-includes"}, - }, - SecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{"ALL"}, - }, - ReadOnlyRootFilesystem: helpers.GetPointer(true), - RunAsGroup: helpers.GetPointer[int64](1001), - RunAsUser: helpers.GetPointer[int64](101), - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, + }, + }, + }, + {Name: "nginx-agent", VolumeSource: emptyDirVolumeSource}, + { + Name: "nginx-agent-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: ngxAgentConfigMapName, }, }, }, - ImagePullSecrets: []corev1.LocalObjectReference{}, - ServiceAccountName: objectMeta.Name, - SecurityContext: &corev1.PodSecurityContext{ - FSGroup: helpers.GetPointer[int64](1001), - RunAsNonRoot: helpers.GetPointer(true), + }, + { + Name: "nginx-agent-tls", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: agentTLSSecretName, + }, }, - Volumes: []corev1.Volume{ - { - Name: "token", - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: []corev1.VolumeProjection{ - { - ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ - Path: "token", - Audience: tokenAudience, - }, - }, - }, - }, + }, + {Name: "nginx-agent-log", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-agent-lib", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-conf", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-stream-conf", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-main-includes", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-secrets", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-run", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-cache", VolumeSource: emptyDirVolumeSource}, + {Name: "nginx-includes", VolumeSource: emptyDirVolumeSource}, + { + Name: "nginx-includes-bootstrap", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: ngxIncludesConfigMapName, }, }, - {Name: "nginx-agent", VolumeSource: emptyDirVolumeSource}, + }, + }, + } +} + +// buildInitContainers builds the init containers. +func (p *NginxProvisioner) buildInitContainers(nProxyCfg *graph.EffectiveNginxProxy) []corev1.Container { + _, pullPolicy := p.buildImage(nProxyCfg) + + return []corev1.Container{ + { + Name: "init", + Image: p.cfg.GatewayPodConfig.Image, + ImagePullPolicy: pullPolicy, + Command: []string{ + "/usr/bin/gateway", + "initialize", + "--source", "/agent/nginx-agent.conf", + "--destination", "/etc/nginx-agent", + "--source", "/includes/main.conf", + "--destination", "/etc/nginx/main-includes", + }, + Env: []corev1.EnvVar{ { - Name: "nginx-agent-config", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: ngxAgentConfigMapName, - }, + Name: "POD_UID", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.uid", }, }, }, - { - Name: "nginx-agent-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: agentTLSSecretName, - }, - }, + }, + VolumeMounts: []corev1.VolumeMount{ + {MountPath: "/agent", Name: "nginx-agent-config"}, + {MountPath: "/etc/nginx-agent", Name: "nginx-agent"}, + {MountPath: "/includes", Name: "nginx-includes-bootstrap"}, + {MountPath: "/etc/nginx/main-includes", Name: "nginx-main-includes"}, + }, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, }, - {Name: "nginx-agent-log", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-agent-lib", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-conf", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-stream-conf", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-main-includes", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-secrets", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-run", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-cache", VolumeSource: emptyDirVolumeSource}, - {Name: "nginx-includes", VolumeSource: emptyDirVolumeSource}, - { - Name: "nginx-includes-bootstrap", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: ngxIncludesConfigMapName, - }, - }, - }, + ReadOnlyRootFilesystem: helpers.GetPointer(true), + RunAsGroup: helpers.GetPointer[int64](1001), + RunAsUser: helpers.GetPointer[int64](101), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, }, } +} - if nProxyCfg != nil && nProxyCfg.Kubernetes != nil { - var podSpec *ngfAPIv1alpha2.PodSpec - var containerSpec *ngfAPIv1alpha2.ContainerSpec - if nProxyCfg.Kubernetes.Deployment != nil { - podSpec = &nProxyCfg.Kubernetes.Deployment.Pod - containerSpec = &nProxyCfg.Kubernetes.Deployment.Container - } else if nProxyCfg.Kubernetes.DaemonSet != nil { - podSpec = &nProxyCfg.Kubernetes.DaemonSet.Pod - containerSpec = &nProxyCfg.Kubernetes.DaemonSet.Container - } +// buildBasePodTemplateSpec builds the base pod template spec. +func (p *NginxProvisioner) buildBasePodTemplateSpec( + objectMeta metav1.ObjectMeta, + podAnnotations map[string]string, + containers []corev1.Container, + initContainers []corev1.Container, + volumes []corev1.Volume, +) corev1.PodTemplateSpec { + return corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: objectMeta.Labels, + Annotations: podAnnotations, + }, + Spec: corev1.PodSpec{ + Containers: containers, + InitContainers: initContainers, + ImagePullSecrets: []corev1.LocalObjectReference{}, + ServiceAccountName: objectMeta.Name, + SecurityContext: &corev1.PodSecurityContext{ + FSGroup: helpers.GetPointer[int64](1001), + RunAsNonRoot: helpers.GetPointer(true), + }, + Volumes: volumes, + }, + } +} - if podSpec != nil { - spec.Spec.TerminationGracePeriodSeconds = podSpec.TerminationGracePeriodSeconds - spec.Spec.Affinity = podSpec.Affinity - spec.Spec.NodeSelector = podSpec.NodeSelector - spec.Spec.Tolerations = podSpec.Tolerations - spec.Spec.Volumes = append(spec.Spec.Volumes, podSpec.Volumes...) - spec.Spec.TopologySpreadConstraints = podSpec.TopologySpreadConstraints - } +// applyUserConfiguration applies user-defined configuration overrides. +func (p *NginxProvisioner) applyUserConfiguration( + spec *corev1.PodTemplateSpec, + nProxyCfg *graph.EffectiveNginxProxy, +) { + if nProxyCfg == nil || nProxyCfg.Kubernetes == nil { + return + } - if containerSpec != nil { - container := spec.Spec.Containers[0] - if containerSpec.Resources != nil { - container.Resources = *containerSpec.Resources - } - container.Lifecycle = containerSpec.Lifecycle - container.VolumeMounts = append(container.VolumeMounts, containerSpec.VolumeMounts...) + var podSpec *ngfAPIv1alpha2.PodSpec + var containerSpec *ngfAPIv1alpha2.ContainerSpec - if containerSpec.Debug != nil && *containerSpec.Debug { - container.Command = append(container.Command, "/agent/entrypoint.sh") - container.Args = append(container.Args, "debug") - } - spec.Spec.Containers[0] = container + if nProxyCfg.Kubernetes.Deployment != nil { + podSpec = &nProxyCfg.Kubernetes.Deployment.Pod + containerSpec = &nProxyCfg.Kubernetes.Deployment.Container + } else if nProxyCfg.Kubernetes.DaemonSet != nil { + podSpec = &nProxyCfg.Kubernetes.DaemonSet.Pod + containerSpec = &nProxyCfg.Kubernetes.DaemonSet.Container + } + + // Apply pod-level configuration + if podSpec != nil { + spec.Spec.TerminationGracePeriodSeconds = podSpec.TerminationGracePeriodSeconds + spec.Spec.Affinity = podSpec.Affinity + spec.Spec.NodeSelector = podSpec.NodeSelector + spec.Spec.Tolerations = podSpec.Tolerations + spec.Spec.Volumes = append(spec.Spec.Volumes, podSpec.Volumes...) + spec.Spec.TopologySpreadConstraints = podSpec.TopologySpreadConstraints + } + + // Apply container-level configuration (NGINX container only) + if containerSpec != nil { + container := spec.Spec.Containers[0] + if containerSpec.Resources != nil { + container.Resources = *containerSpec.Resources } + container.Lifecycle = containerSpec.Lifecycle + container.VolumeMounts = append(container.VolumeMounts, containerSpec.VolumeMounts...) + + if containerSpec.Debug != nil && *containerSpec.Debug { + container.Command = append(container.Command, "/agent/entrypoint.sh") + container.Args = append(container.Args, "debug") + } + spec.Spec.Containers[0] = container } +} +// configureImagePullSecrets configures image pull secrets. +func (p *NginxProvisioner) configureImagePullSecrets( + spec *corev1.PodTemplateSpec, + dockerSecretNames map[string]string, +) { for name := range dockerSecretNames { ref := corev1.LocalObjectReference{Name: name} spec.Spec.ImagePullSecrets = append(spec.Spec.ImagePullSecrets, ref) @@ -794,73 +899,84 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec( sort.Slice(spec.Spec.ImagePullSecrets, func(i, j int) bool { return spec.Spec.ImagePullSecrets[i].Name < spec.Spec.ImagePullSecrets[j].Name }) +} - if p.cfg.Plus { - initCmd := spec.Spec.InitContainers[0].Command - initCmd = append(initCmd, - "--source", "/includes/mgmt.conf", "--destination", "/etc/nginx/main-includes", "--nginx-plus") - spec.Spec.InitContainers[0].Command = initCmd +// configureNginxPlus configures NGINX Plus specific settings. +func (p *NginxProvisioner) configureNginxPlus( + spec *corev1.PodTemplateSpec, + jwtSecretName string, + caSecretName string, + clientSSLSecretName string, +) { + // Update init container command + initCmd := spec.Spec.InitContainers[0].Command + initCmd = append(initCmd, + "--source", "/includes/mgmt.conf", + "--destination", "/etc/nginx/main-includes", + "--nginx-plus", + ) + spec.Spec.InitContainers[0].Command = initCmd - volumeMounts := spec.Spec.Containers[0].VolumeMounts + // Add NGINX Plus volumes and volume mounts + volumeMounts := spec.Spec.Containers[0].VolumeMounts + // Add nginx-lib volume + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "nginx-lib", + MountPath: "/var/lib/nginx/state", + }) + spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{ + Name: "nginx-lib", + VolumeSource: emptyDirVolumeSource, + }) + + // Add JWT license if configured + if jwtSecretName != "" { volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "nginx-lib", - MountPath: "/var/lib/nginx/state", + Name: "nginx-plus-license", + MountPath: "/etc/nginx/license.jwt", + SubPath: "license.jwt", }) spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{ - Name: "nginx-lib", - VolumeSource: emptyDirVolumeSource, + Name: "nginx-plus-license", + VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: jwtSecretName}}, }) + } - if jwtSecretName != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "nginx-plus-license", - MountPath: "/etc/nginx/license.jwt", - SubPath: "license.jwt", - }) - spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{ - Name: "nginx-plus-license", - VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: jwtSecretName}}, + // Add usage certs if configured + if caSecretName != "" || clientSSLSecretName != "" { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "nginx-plus-usage-certs", + MountPath: "/etc/nginx/certs-bootstrap/", + }) + + sources := []corev1.VolumeProjection{} + if caSecretName != "" { + sources = append(sources, corev1.VolumeProjection{ + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: caSecretName}, + }, }) } - if caSecretName != "" || clientSSLSecretName != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "nginx-plus-usage-certs", - MountPath: "/etc/nginx/certs-bootstrap/", - }) - - sources := []corev1.VolumeProjection{} - - if caSecretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: caSecretName}, - }, - }) - } - - if clientSSLSecretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: clientSSLSecretName}, - }, - }) - } - - spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{ - Name: "nginx-plus-usage-certs", - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: sources, - }, + if clientSSLSecretName != "" { + sources = append(sources, corev1.VolumeProjection{ + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: clientSSLSecretName}, }, }) } - spec.Spec.Containers[0].VolumeMounts = volumeMounts + spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{ + Name: "nginx-plus-usage-certs", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: sources, + }, + }, + }) } - return spec + spec.Spec.Containers[0].VolumeMounts = volumeMounts } func (p *NginxProvisioner) buildImage(nProxyCfg *graph.EffectiveNginxProxy) (string, corev1.PullPolicy) { @@ -868,6 +984,10 @@ func (p *NginxProvisioner) buildImage(nProxyCfg *graph.EffectiveNginxProxy) (str tag := p.cfg.GatewayPodConfig.Version pullPolicy := defaultImagePullPolicy + if p.cfg.Plus { + image = defaultNginxPlusImagePath + } + getImageAndPullPolicy := func(container ngfAPIv1alpha2.ContainerSpec) (string, string, corev1.PullPolicy) { if container.Image != nil { if container.Image.Repository != nil { @@ -895,6 +1015,223 @@ func (p *NginxProvisioner) buildImage(nProxyCfg *graph.EffectiveNginxProxy) (str return fmt.Sprintf("%s:%s", image, tag), pullPolicy } +// configureWAF configures WAF containers, volume mounts, and volumes. +func (p *NginxProvisioner) configureWAF( + containers []corev1.Container, + volumes []corev1.Volume, + nProxyCfg *graph.EffectiveNginxProxy, +) ([]corev1.Container, []corev1.Volume) { + // Add WAF containers + wafContainers := p.buildWAFContainers(nProxyCfg) + containers = append(containers, wafContainers...) + + // Add WAF volume mounts to NGINX container + nginxContainer := containers[0] + nginxContainer.VolumeMounts = append(nginxContainer.VolumeMounts, buildNginxWAFVolumeMounts()...) + containers[0] = nginxContainer + + // Add WAF volumes + wafVolumes := buildWAFSharedVolumes() + volumes = append(volumes, wafVolumes...) + + return containers, volumes +} + +// buildWAFSharedVolumes creates the required shared volumes for WAF containers. +func buildWAFSharedVolumes() []corev1.Volume { + return []corev1.Volume{ + { + Name: appProtectBundlesVolumeName, + VolumeSource: emptyDirVolumeSource, + }, + { + Name: appProtectConfigVolumeName, + VolumeSource: emptyDirVolumeSource, + }, + { + Name: appProtectBdConfigVolumeName, + VolumeSource: emptyDirVolumeSource, + }, + } +} + +// buildNginxWAFVolumeMounts creates the required volume mounts for NGINX container when WAF is enabled. +func buildNginxWAFVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + { + Name: appProtectBundlesVolumeName, + MountPath: "/etc/app_protect/bundles", + }, + { + Name: appProtectConfigVolumeName, + MountPath: "/opt/app_protect/config", + }, + { + Name: appProtectBdConfigVolumeName, + MountPath: "/opt/app_protect/bd_config", + }, + } +} + +// buildWAFContainers creates the WAF enforcer and config manager containers. +func (p *NginxProvisioner) buildWAFContainers(nProxyCfg *graph.EffectiveNginxProxy) []corev1.Container { + var containers []corev1.Container + var wafContainersCfg *ngfAPIv1alpha2.WAFContainerSpec + + // Get WAF container configuration + if nProxyCfg != nil && nProxyCfg.Kubernetes != nil { + if nProxyCfg.Kubernetes.Deployment != nil { + wafContainersCfg = nProxyCfg.Kubernetes.Deployment.WAFContainers + } else if nProxyCfg.Kubernetes.DaemonSet != nil { + wafContainersCfg = nProxyCfg.Kubernetes.DaemonSet.WAFContainers + } + } + + // Build WAF Enforcer container + enforcerContainer := p.buildWAFEnforcerContainer(wafContainersCfg) + containers = append(containers, enforcerContainer) + + // Build WAF Config Manager container + configMgrContainer := p.buildWAFConfigManagerContainer(wafContainersCfg) + containers = append(containers, configMgrContainer) + + return containers +} + +// buildWAFEnforcerContainer creates the WAF enforcer container. +func (p *NginxProvisioner) buildWAFEnforcerContainer( + wafContainersCfg *ngfAPIv1alpha2.WAFContainerSpec, +) corev1.Container { + image := p.buildWAFImage( + defaultWAFEnforcerImagePath, + defaultWAFImageTag, + wafContainersCfg, + "enforcer", + ) + + container := corev1.Container{ + Name: "waf-enforcer", + Image: image, + ImagePullPolicy: defaultImagePullPolicy, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: helpers.GetPointer[int64](101), + AllowPrivilegeEscalation: helpers.GetPointer(false), + RunAsNonRoot: helpers.GetPointer(true), + ReadOnlyRootFilesystem: helpers.GetPointer(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"all"}, + }, + }, + Env: []corev1.EnvVar{ + {Name: "ENFORCER_PORT", Value: "50000"}, + {Name: "ENFORCER_CONFIG_TIMEOUT", Value: "0"}, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: appProtectBdConfigVolumeName, + MountPath: "/opt/app_protect/bd_config", + }, + }, + } + + // Apply user-configured settings + if wafContainersCfg != nil && wafContainersCfg.Enforcer != nil { + if wafContainersCfg.Enforcer.Resources != nil { + container.Resources = *wafContainersCfg.Enforcer.Resources + } + if len(wafContainersCfg.Enforcer.VolumeMounts) > 0 { + container.VolumeMounts = append(container.VolumeMounts, wafContainersCfg.Enforcer.VolumeMounts...) + } + } + + return container +} + +// buildWAFConfigManagerContainer creates the WAF config manager container. +func (p *NginxProvisioner) buildWAFConfigManagerContainer( + wafContainersCfg *ngfAPIv1alpha2.WAFContainerSpec, +) corev1.Container { + image := p.buildWAFImage( + defaultWAFConfigMgrImagePath, + defaultWAFImageTag, + wafContainersCfg, + "configManager", + ) + + container := corev1.Container{ + Name: "waf-config-mgr", + Image: image, + ImagePullPolicy: defaultImagePullPolicy, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: helpers.GetPointer(false), + RunAsNonRoot: helpers.GetPointer(false), + RunAsUser: helpers.GetPointer[int64](101), + ReadOnlyRootFilesystem: helpers.GetPointer(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"all"}, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: appProtectBdConfigVolumeName, + MountPath: "/opt/app_protect/bd_config", + }, + { + Name: appProtectConfigVolumeName, + MountPath: "/opt/app_protect/config", + }, + { + Name: appProtectBundlesVolumeName, + MountPath: "/etc/app_protect/bundles", + }, + }, + } + + // Apply user-configured settings + if wafContainersCfg != nil && wafContainersCfg.ConfigManager != nil { + if wafContainersCfg.ConfigManager.Resources != nil { + container.Resources = *wafContainersCfg.ConfigManager.Resources + } + if len(wafContainersCfg.ConfigManager.VolumeMounts) > 0 { + container.VolumeMounts = append(container.VolumeMounts, wafContainersCfg.ConfigManager.VolumeMounts...) + } + } + + return container +} + +// buildWAFImage builds the WAF container image string. +func (p *NginxProvisioner) buildWAFImage( + defaultImagePath, + defaultTag string, + wafContainersCfg *ngfAPIv1alpha2.WAFContainerSpec, + containerType string, +) string { + image := defaultImagePath + tag := defaultTag + + if wafContainersCfg != nil { + var containerCfg *ngfAPIv1alpha2.WAFContainerConfig + switch containerType { + case "enforcer": + containerCfg = wafContainersCfg.Enforcer + case "configManager": + containerCfg = wafContainersCfg.ConfigManager + } + + if containerCfg != nil && containerCfg.Image != nil { + if containerCfg.Image.Repository != nil { + image = *containerCfg.Image.Repository + } + if containerCfg.Image.Tag != nil { + tag = *containerCfg.Image.Tag + } + } + } + + return fmt.Sprintf("%s:%s", image, tag) +} + // TODO(sberman): see about how this can be made more elegant. Maybe create some sort of Object factory // that can better store/build all the objects we need, to reduce the amount of duplicate object lists that we // have everywhere. diff --git a/internal/controller/provisioner/objects_test.go b/internal/controller/provisioner/objects_test.go index 96710f8902..3999bc1bbc 100644 --- a/internal/controller/provisioner/objects_test.go +++ b/internal/controller/provisioner/objects_test.go @@ -654,6 +654,7 @@ func TestBuildNginxResourceObjects_DaemonSet(t *testing.T) { } nProxyCfg := &graph.EffectiveNginxProxy{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ DaemonSet: &ngfAPIv1alpha2.DaemonSetSpec{ Pod: ngfAPIv1alpha2.PodSpec{ @@ -701,6 +702,11 @@ func TestBuildNginxResourceObjects_DaemonSet(t *testing.T) { g.Expect(container.ImagePullPolicy).To(Equal(corev1.PullAlways)) g.Expect(container.Resources.Limits).To(HaveKey(corev1.ResourceCPU)) g.Expect(container.Resources.Limits[corev1.ResourceCPU].Format).To(Equal(resource.Format("100m"))) + + // verify WAF container is present - we can assume the rest of the WAF configuration is correct + // as it is tested elsewhere + wafContainer := template.Spec.Containers[1] + g.Expect(wafContainer.Image).To(ContainSubstring("private-registry.nginx.com/nap/waf-enforcer")) } func TestBuildNginxResourceObjects_OpenShift(t *testing.T) { @@ -1002,3 +1008,205 @@ func TestSetIPFamily(t *testing.T) { g.Expect(svc.Spec.IPFamilyPolicy).To(Equal(helpers.GetPointer(corev1.IPFamilyPolicySingleStack))) g.Expect(svc.Spec.IPFamilies).To(Equal([]corev1.IPFamily{corev1.IPv6Protocol})) } + +func TestBuildNginxResourceObjects_WAF(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + agentTLSSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: agentTLSTestSecretName, + Namespace: ngfNamespace, + }, + Data: map[string][]byte{"tls.crt": []byte("tls")}, + } + fakeClient := fake.NewFakeClient(agentTLSSecret) + + provisioner := &NginxProvisioner{ + cfg: Config{ + GatewayPodConfig: &config.GatewayPodConfig{ + Namespace: ngfNamespace, + Version: "1.0.0", + Image: "ngf-image", + }, + AgentTLSSecretName: agentTLSTestSecretName, + }, + baseLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "nginx", + }, + }, + k8sClient: fakeClient, + } + + gateway := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw", + Namespace: "default", + }, + Spec: gatewayv1.GatewaySpec{ + Listeners: []gatewayv1.Listener{ + { + Port: 80, + }, + { + Port: 8888, + }, + }, + }, + } + + resourceName := "gw-nginx" + nProxyCfg := &graph.EffectiveNginxProxy{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{ + WAFContainers: &ngfAPIv1alpha2.WAFContainerSpec{ + Enforcer: &ngfAPIv1alpha2.WAFContainerConfig{ + Image: &ngfAPIv1alpha2.Image{ + Repository: helpers.GetPointer("custom-registry/waf-enforcer"), + Tag: helpers.GetPointer("custom-tag"), + }, + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("512Mi"), + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "waf-logs", + MountPath: "/var/log/waf", + }, + }, + }, + ConfigManager: &ngfAPIv1alpha2.WAFContainerConfig{ + Image: &ngfAPIv1alpha2.Image{ + Repository: helpers.GetPointer("custom-registry/waf-config-mgr"), + Tag: helpers.GetPointer("config-tag"), + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("300m"), + }, + }, + }, + }, + }, + }, + } + + objects, err := provisioner.buildNginxResourceObjects(resourceName, gateway, nProxyCfg) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(objects).To(HaveLen(6)) + + // WAF-specific validations on the deployment + depObj := objects[5] + dep, ok := depObj.(*appsv1.Deployment) + g.Expect(ok).To(BeTrue()) + + template := dep.Spec.Template + + // Should have 3 containers: nginx + waf-enforcer + waf-config-mgr + g.Expect(template.Spec.Containers).To(HaveLen(3)) + + // Validate NGINX container (first container) + nginxContainer := template.Spec.Containers[0] + g.Expect(nginxContainer.Name).To(Equal("nginx")) + g.Expect(nginxContainer.Image).To(Equal(fmt.Sprintf("%s:1.0.0", defaultNginxImagePath))) + + // Check NGINX container has WAF volume mounts + wafVolumeMountNames := []string{ + "app-protect-bundles", + "app-protect-config", + "app-protect-bd-config", + } + + expectedNginxWAFMounts := map[string]string{ + "app-protect-bundles": "/etc/app_protect/bundles", + "app-protect-config": "/opt/app_protect/config", + "app-protect-bd-config": "/opt/app_protect/bd_config", + } + + for _, expectedMount := range wafVolumeMountNames { + found := false + for _, mount := range nginxContainer.VolumeMounts { + if mount.Name == expectedMount { + found = true + g.Expect(mount.MountPath).To(Equal(expectedNginxWAFMounts[expectedMount])) + break + } + } + g.Expect(found).To(BeTrue(), "NGINX container missing WAF volume mount: %s", expectedMount) + } + + // Validate WAF Enforcer container (second container) + enforcerContainer := template.Spec.Containers[1] + g.Expect(enforcerContainer.Name).To(Equal("waf-enforcer")) + g.Expect(enforcerContainer.Image).To(Equal("custom-registry/waf-enforcer:custom-tag")) + g.Expect(enforcerContainer.ImagePullPolicy).To(Equal(defaultImagePullPolicy)) + + // Check enforcer resources + g.Expect(enforcerContainer.Resources.Requests).To(HaveKey(corev1.ResourceMemory)) + g.Expect(enforcerContainer.Resources.Requests[corev1.ResourceMemory]).To(Equal(resource.MustParse("512Mi"))) + g.Expect(enforcerContainer.Resources.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("200m"))) + + // Check enforcer volume mounts (should have default + custom) + g.Expect(enforcerContainer.VolumeMounts).To(HaveLen(2)) + + // Default mount + defaultMount := false + customMount := false + for _, mount := range enforcerContainer.VolumeMounts { + if mount.Name == "app-protect-bd-config" && mount.MountPath == "/opt/app_protect/bd_config" { + defaultMount = true + } + if mount.Name == "waf-logs" && mount.MountPath == "/var/log/waf" { + customMount = true + } + } + g.Expect(defaultMount).To(BeTrue()) + g.Expect(customMount).To(BeTrue()) + + // Validate WAF Config Manager container (third container) + configMgrContainer := template.Spec.Containers[2] + g.Expect(configMgrContainer.Name).To(Equal("waf-config-mgr")) + g.Expect(configMgrContainer.Image).To(Equal("custom-registry/waf-config-mgr:config-tag")) + + // Check config manager security context + g.Expect(configMgrContainer.SecurityContext).ToNot(BeNil()) + g.Expect(configMgrContainer.SecurityContext.AllowPrivilegeEscalation).To(Equal(helpers.GetPointer(false))) + g.Expect(configMgrContainer.SecurityContext.RunAsNonRoot).To(Equal(helpers.GetPointer(false))) + g.Expect(configMgrContainer.SecurityContext.RunAsUser).To(Equal(helpers.GetPointer[int64](101))) + g.Expect(configMgrContainer.SecurityContext.Capabilities.Drop).To(ContainElement(corev1.Capability("all"))) + + // Check config manager resources + g.Expect(configMgrContainer.Resources.Limits).To(HaveKey(corev1.ResourceCPU)) + g.Expect(configMgrContainer.Resources.Limits[corev1.ResourceCPU]).To(Equal(resource.MustParse("300m"))) + + // Check config manager volume mounts (should have all 3 WAF volumes) + g.Expect(configMgrContainer.VolumeMounts).To(HaveLen(3)) + + expectedConfigMgrMounts := map[string]string{ + "app-protect-bd-config": "/opt/app_protect/bd_config", + "app-protect-config": "/opt/app_protect/config", + "app-protect-bundles": "/etc/app_protect/bundles", + } + + for _, mount := range configMgrContainer.VolumeMounts { + expectedPath, exists := expectedConfigMgrMounts[mount.Name] + g.Expect(exists).To(BeTrue(), "Unexpected volume mount in config manager: %s", mount.Name) + g.Expect(mount.MountPath).To(Equal(expectedPath)) + } + + // Validate WAF volumes are present in pod spec + volumeNames := make([]string, len(template.Spec.Volumes)) + for i, volume := range template.Spec.Volumes { + volumeNames[i] = volume.Name + } + + for _, expectedVolume := range wafVolumeMountNames { + g.Expect(volumeNames).To(ContainElement(expectedVolume)) + } +} diff --git a/internal/controller/state/dataplane/configuration.go b/internal/controller/state/dataplane/configuration.go index 6bded6cebb..7a1cd01dfe 100644 --- a/internal/controller/state/dataplane/configuration.go +++ b/internal/controller/state/dataplane/configuration.go @@ -79,6 +79,7 @@ func BuildConfiguration( NginxPlus: nginxPlus, MainSnippets: buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextMain), AuxiliarySecrets: buildAuxiliarySecrets(g.PlusSecrets), + WAF: WAFConfig{Enabled: graph.WAFEnabledForNginxProxy(gateway.EffectiveNginxProxy)}, } return config diff --git a/internal/controller/state/dataplane/types.go b/internal/controller/state/dataplane/types.go index 5f4d3c2d9b..706f7a127f 100644 --- a/internal/controller/state/dataplane/types.go +++ b/internal/controller/state/dataplane/types.go @@ -54,6 +54,8 @@ type Configuration struct { NginxPlus NginxPlus // BaseHTTPConfig holds the configuration options at the http context. BaseHTTPConfig BaseHTTPConfig + // WAF holds the WAF configuration. + WAF WAFConfig } // SSLKeyPairID is a unique identifier for a SSLKeyPair. @@ -445,3 +447,10 @@ type DeploymentContext struct { // Integration is "ngf". Integration string `json:"integration"` } + +// WAFConfig holds the WAF configuration for the dataplane. +// It is used to determine whether WAF is enabled and to load the WAF module. +type WAFConfig struct { + // Enabled indicates whether WAF is enabled. + Enabled bool +} diff --git a/internal/controller/state/graph/nginxproxy.go b/internal/controller/state/graph/nginxproxy.go index 77c50934ef..74dd59fdbb 100644 --- a/internal/controller/state/graph/nginxproxy.go +++ b/internal/controller/state/graph/nginxproxy.go @@ -123,6 +123,11 @@ func MetricsEnabledForNginxProxy(np *EffectiveNginxProxy) (*int32, bool) { return nil, true } +// WAFEnabledForNginxProxy returns whether WAF is enabled for the given NginxProxy configuration. +func WAFEnabledForNginxProxy(np *EffectiveNginxProxy) bool { + return np != nil && np.WAF != nil && *np.WAF == ngfAPIv1alpha2.WAFEnabled +} + func processNginxProxies( nps map[types.NamespacedName]*ngfAPIv1alpha2.NginxProxy, validator validation.GenericValidator, diff --git a/internal/controller/state/graph/nginxproxy_test.go b/internal/controller/state/graph/nginxproxy_test.go index c8713ea116..4658b6f046 100644 --- a/internal/controller/state/graph/nginxproxy_test.go +++ b/internal/controller/state/graph/nginxproxy_test.go @@ -460,6 +460,112 @@ func TestMetricsEnabledForNginxProxy(t *testing.T) { } } +// Add test cases for WAF merging in TestBuildEffectiveNginxProxy. +func TestBuildEffectiveNginxProxy_WAF(t *testing.T) { + t.Parallel() + + tests := []struct { + gcNp *NginxProxy + gwNp *NginxProxy + exp *EffectiveNginxProxy + name string + }{ + { + name: "gateway nginx proxy overrides WAF setting", + gcNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFDisabled), + }, + }, + }, + gwNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), + }, + }, + }, + exp: &EffectiveNginxProxy{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), + }, + }, + { + name: "gateway class WAF setting when gateway has no WAF config", + gcNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), + }, + }, + }, + gwNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + // No WAF field set + }, + }, + }, + exp: &EffectiveNginxProxy{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFEnabled), + }, + }, + { + name: "both have WAF disabled", + gcNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFDisabled), + }, + }, + }, + gwNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFDisabled), + }, + }, + }, + exp: &EffectiveNginxProxy{ + WAF: helpers.GetPointer(ngfAPIv1alpha2.WAFDisabled), + }, + }, + { + name: "both have WAF unset", + gcNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{}, + }, + }, + gwNp: &NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha2.NginxProxy{ + Spec: ngfAPIv1alpha2.NginxProxySpec{}, + }, + }, + exp: &EffectiveNginxProxy{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + enp := buildEffectiveNginxProxy(test.gcNp, test.gwNp) + g.Expect(enp).ToNot(BeNil()) + g.Expect(enp.WAF).To(Equal(test.exp.WAF)) + }) + } +} + func TestProcessNginxProxies(t *testing.T) { t.Parallel()