diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d79025472a..647e9f4f40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: token_format: access_token workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - if: ${{ github.event_name != 'pull_request' && contains(inputs.image, 'plus') }} + if: ${{ github.event_name != 'pull_request' && (contains(inputs.image, 'plus') || inputs.image == 'plus-waf') }} - name: Login to GAR uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 @@ -95,7 +95,7 @@ jobs: registry: us-docker.pkg.dev username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - if: ${{ github.event_name != 'pull_request' && contains(inputs.image, 'plus') }} + if: ${{ github.event_name != 'pull_request' && (contains(inputs.image, 'plus') || inputs.image == 'plus-waf') }} - name: Docker meta id: meta @@ -106,7 +106,9 @@ jobs: name=ghcr.io/${{ github.repository_owner }}/nginx-gateway-fabric,enable=${{ inputs.image == 'ngf' && github.event_name != 'pull_request' }} name=ghcr.io/${{ github.repository_owner }}/nginx-gateway-fabric/nginx,enable=${{ inputs.image == 'nginx' && github.event_name != 'pull_request' }} name=docker-mgmt.nginx.com/nginx-gateway-fabric/nginx-plus,enable=${{ inputs.image == 'plus' && github.event_name != 'pull_request' }} + name=docker-mgmt.nginx.com/nginx-gateway-fabric/nginx-plus-nap-waf,enable=${{ inputs.image == 'plus-waf' && github.event_name != 'pull_request' }} name=us-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/nginx-gateway-fabric/nginx-plus,enable=${{ inputs.image == 'plus' && github.event_name != 'pull_request' }} + name=us-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/nginx-gateway-fabric/nginx-plus-nap-waf,enable=${{ inputs.image == 'plus-waf' && github.event_name != 'pull_request' }} name=localhost:5000/nginx-gateway-fabric/${{ inputs.image }} flavor: | latest=${{ (inputs.tag != '' && 'true') || 'auto' }} @@ -134,7 +136,7 @@ jobs: - name: Build Docker Image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: - file: build/Dockerfile${{ inputs.image == 'nginx' && '.nginx' || '' }}${{ inputs.image == 'plus' && '.nginxplus' || '' }} + file: build/Dockerfile${{ inputs.image == 'nginx' && '.nginx' || '' }}${{ (inputs.image == 'plus' || inputs.image == 'plus-waf') && '.nginxplus' || '' }} context: "." target: ${{ inputs.image == 'ngf' && 'goreleaser' || '' }} tags: ${{ steps.meta.outputs.tags }} @@ -152,13 +154,30 @@ jobs: NJS_DIR=internal/controller/nginx/modules/src NGINX_CONF_DIR=internal/controller/nginx/conf BUILD_AGENT=gha + ${{ inputs.image == 'plus-waf' && 'ALPINE_VERSION=3.19' || '' }} + ${{ inputs.image == 'plus-waf' && 'INCLUDE_NAP_WAF=true' || '' }} secrets: | ${{ contains(inputs.image, 'plus') && format('"nginx-repo.crt={0}"', secrets.NGINX_CRT) || '' }} ${{ contains(inputs.image, 'plus') && format('"nginx-repo.key={0}"', secrets.NGINX_KEY) || '' }} - name: Inspect SBOM and output manifest run: | + if [[ "${{ inputs.image }}" == "plus-waf" ]]; then + # For plus-waf, use syft directly + echo "Generating SBOM for plus-waf using syft..." + + # Install syft if not available + if ! command -v syft >/dev/null 2>&1; then + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + fi + + # Generate SBOM using syft directly for plus-waf (known to work with NAP WAF) + syft localhost:5000/nginx-gateway-fabric/${{ inputs.image }}:${{ steps.meta.outputs.version }} -o spdx-json > sbom-${{ inputs.image }}.json + echo "Generated SBOM using syft for plus-waf" + else + # For other images, use the standard Docker buildx approach docker buildx imagetools inspect localhost:5000/nginx-gateway-fabric/${{ inputs.image }}:${{ steps.meta.outputs.version }} --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sbom-${{ inputs.image }}.json + fi docker buildx imagetools inspect localhost:5000/nginx-gateway-fabric/${{ inputs.image }}:${{ steps.meta.outputs.version }} --raw - name: Scan SBOM @@ -176,4 +195,4 @@ jobs: with: sarif_file: ${{ steps.scan.outputs.sarif }} category: build-${{ inputs.image }} - if: always() + if: always() && steps.scan.conclusion == 'success' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfdc580e66..3e195639e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -222,6 +222,20 @@ jobs: id-token: write # for docker/login to login to NGINX registry secrets: inherit + build-plus-waf: + name: Build Plus WAF images + needs: [vars, binary] + uses: ./.github/workflows/build.yml + with: + image: plus-waf + platforms: "linux/amd64" + permissions: + contents: read # for docker/build-push-action to read repo content + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + packages: write # for docker/build-push-action to push to GHCR + id-token: write # for docker/login to login to NGINX registry + secrets: inherit + functional-tests: name: Functional tests needs: [vars, build-oss, build-plus] 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/v1alpha1/clientsettingspolicy_types.go b/apis/v1alpha1/clientsettingspolicy_types.go index 6948f3339a..a03df366e6 100644 --- a/apis/v1alpha1/clientsettingspolicy_types.go +++ b/apis/v1alpha1/clientsettingspolicy_types.go @@ -5,7 +5,6 @@ import ( gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status diff --git a/apis/v1alpha1/nginxgateway_types.go b/apis/v1alpha1/nginxgateway_types.go index f6c16f6a42..ad1e57b4c0 100644 --- a/apis/v1alpha1/nginxgateway_types.go +++ b/apis/v1alpha1/nginxgateway_types.go @@ -2,7 +2,6 @@ package v1alpha1 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status diff --git a/apis/v1alpha1/observabilitypolicy_types.go b/apis/v1alpha1/observabilitypolicy_types.go index 2366a1fff5..d43403f10f 100644 --- a/apis/v1alpha1/observabilitypolicy_types.go +++ b/apis/v1alpha1/observabilitypolicy_types.go @@ -5,7 +5,6 @@ import ( gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:deprecatedversion:warning="The 'v1alpha1' version of ObservabilityPolicy API is deprecated, please migrate to 'v1alpha2'." // +kubebuilder:subresource:status diff --git a/apis/v1alpha1/policy_methods.go b/apis/v1alpha1/policy_methods.go index 71cc93ede8..3b291cfa07 100644 --- a/apis/v1alpha1/policy_methods.go +++ b/apis/v1alpha1/policy_methods.go @@ -43,3 +43,15 @@ func (p *UpstreamSettingsPolicy) GetPolicyStatus() v1alpha2.PolicyStatus { func (p *UpstreamSettingsPolicy) SetPolicyStatus(status v1alpha2.PolicyStatus) { p.Status = status } + +func (p *WAFPolicy) GetTargetRefs() []v1alpha2.LocalPolicyTargetReference { + return []v1alpha2.LocalPolicyTargetReference{p.Spec.TargetRef} +} + +func (p *WAFPolicy) GetPolicyStatus() v1alpha2.PolicyStatus { + return p.Status +} + +func (p *WAFPolicy) SetPolicyStatus(status v1alpha2.PolicyStatus) { + p.Status = status +} diff --git a/apis/v1alpha1/register.go b/apis/v1alpha1/register.go index 7deb5bfb5c..6967ab3271 100644 --- a/apis/v1alpha1/register.go +++ b/apis/v1alpha1/register.go @@ -42,6 +42,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &SnippetsFilterList{}, &UpstreamSettingsPolicy{}, &UpstreamSettingsPolicyList{}, + &WAFPolicy{}, + &WAFPolicyList{}, ) // AddToGroupVersion allows the serialization of client types like ListOptions. metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/apis/v1alpha1/snippetsfilter_types.go b/apis/v1alpha1/snippetsfilter_types.go index decb6190a4..e58b15ea21 100644 --- a/apis/v1alpha1/snippetsfilter_types.go +++ b/apis/v1alpha1/snippetsfilter_types.go @@ -5,7 +5,6 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status diff --git a/apis/v1alpha1/upstreamsettingspolicy_types.go b/apis/v1alpha1/upstreamsettingspolicy_types.go index 158776491e..eaac7b45fd 100644 --- a/apis/v1alpha1/upstreamsettingspolicy_types.go +++ b/apis/v1alpha1/upstreamsettingspolicy_types.go @@ -5,7 +5,6 @@ import ( gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status diff --git a/apis/v1alpha1/wafpolicy_types.go b/apis/v1alpha1/wafpolicy_types.go new file mode 100644 index 0000000000..638b6c440e --- /dev/null +++ b/apis/v1alpha1/wafpolicy_types.go @@ -0,0 +1,307 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories=nginx-gateway-fabric +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" + +// WAFPolicy is an Inherited Attached Policy. It provides a way to configure NGINX App Protect Web Application Firewall +// for Gateways and Routes. +type WAFPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of the WAFPolicy. + Spec WAFPolicySpec `json:"spec"` + + // Status defines the state of the WAFPolicy. + Status gatewayv1alpha2.PolicyStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// WAFPolicyList contains a list of WAFPolicies. +type WAFPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []WAFPolicy `json:"items"` +} + +// WAFPolicySpec defines the desired state of a WAFPolicy. +// +// +kubebuilder:validation:XValidation:message="policySource is required when securityLogs are specified",rule="!has(self.securityLogs) || has(self.policySource)" +// +//nolint:lll +type WAFPolicySpec struct { + // PolicySource defines the source location and configuration for the compiled WAF policy bundle. + // + // +optional + PolicySource *WAFPolicySource `json:"policySource,omitempty"` + + // TargetRef identifies an API object to apply the policy to. + // Object must be in the same namespace as the policy. + // Support: Gateway, HTTPRoute, GRPCRoute. + // + // +kubebuilder:validation:XValidation:message="TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute",rule="(self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute')" + // +kubebuilder:validation:XValidation:message="TargetRef Group must be gateway.networking.k8s.io.",rule="(self.group=='gateway.networking.k8s.io')" + //nolint:lll + TargetRef gatewayv1alpha2.LocalPolicyTargetReference `json:"targetRef"` + + // SecurityLogs defines the security logging configuration for app_protect_security_log directives. + // Multiple logging configurations can be specified to send logs to different destinations. + // + // +optional + // +kubebuilder:validation:MaxItems=32 + SecurityLogs []WAFSecurityLog `json:"securityLogs,omitempty"` +} + +// WAFPolicySource defines the source location and configuration for fetching WAF policy bundles. +type WAFPolicySource struct { + // AuthSecret is the Secret containing authentication credentials for the WAF policy source. + // + // +optional + AuthSecret *WAFPolicyAuthSecret `json:"authSecret,omitempty"` + + // Validation defines the validation methods for policy integrity verification. + // + // +optional + Validation *WAFPolicyValidation `json:"validation,omitempty"` + + // Polling defines the polling configuration for automatic WAF policy change detection. + // + // +optional + Polling *WAFPolicyPolling `json:"polling,omitempty"` + + // Retry defines the retry configuration for WAF policy fetch failures. + // + // +optional + Retry *WAFPolicyRetry `json:"retry,omitempty"` + + // Timeout for policy downloads. + // + // +optional + Timeout *Duration `json:"timeout,omitempty"` + + // FileLocation defines the location of the WAF policy file. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + FileLocation string `json:"fileLocation"` +} + +// WAFPolicyAuthSecret is the Secret containing authentication credentials for the WAF policy source. +// It must live in the same Namespace as the policy. +type WAFPolicyAuthSecret struct { + // Name is the name of the Secret containing authentication credentials for the WAF policy source. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9_-]+$` + Name string `json:"name"` +} + +// WAFPolicyValidation defines the validation methods for policy integrity verification. +type WAFPolicyValidation struct { + // Methods specifies the validation methods to use for policy integrity verification. + // Currently supported: ["checksum"] + // + // +optional + // +listType=set + Methods []WAFPolicyValidationMethod `json:"methods,omitempty"` +} + +// WAFPolicyValidationMethod defines the supported validation methods. +// +// +kubebuilder:validation:Enum=checksum +type WAFPolicyValidationMethod string + +const ( + // WAFPolicyValidationChecksum validates policy integrity using checksum verification. + WAFPolicyValidationChecksum WAFPolicyValidationMethod = "checksum" +) + +// WAFPolicyPolling defines the polling configuration for automatic WAF policy change detection. +type WAFPolicyPolling struct { + // Enabled indicates whether polling is enabled for automatic WAF policy change detection. + // When enabled, NGINX Gateway Fabric will periodically check for policy changes using checksum validation. + // + // +optional + // +kubebuilder:default=false + Enabled *bool `json:"enabled,omitempty"` + + // Interval is the polling interval to check for WAF policy changes. + // Must be a valid duration string (e.g., "5m", "30s", "1h"). + // Defaults to "5m" if polling is enabled. + // + // +optional + // +kubebuilder:default="5m" + Interval *Duration `json:"interval,omitempty"` + + // ChecksumLocation specifies the location of the checksum file for the policy bundle. + // If not specified, defaults to .sha256 + // + // +optional + // +kubebuilder:validation:MaxLength=2048 + ChecksumLocation *string `json:"checksumLocation,omitempty"` +} + +// WAFPolicyRetry defines the retry configuration for WAF policy fetch failures. +type WAFPolicyRetry struct { + // Attempts is the number of retry attempts for fetching the WAF policy. + // Set to 0 to disable retries. + // + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=3 + Attempts *int32 `json:"attempts,omitempty"` + + // Backoff defines the backoff strategy for retry attempts. + // Supported values: "exponential", "linear" + // + // +optional + // +kubebuilder:default="exponential" + Backoff *WAFPolicyRetryBackoff `json:"backoff,omitempty"` + + // MaxDelay is the maximum delay between retry attempts. + // Must be a valid duration string (e.g., "5m", "30s"). + // + // +optional + // +kubebuilder:default="5m" + MaxDelay *Duration `json:"maxDelay,omitempty"` +} + +// WAFPolicyRetryBackoff defines the supported backoff strategies. +// +// +kubebuilder:validation:Enum=exponential;linear +type WAFPolicyRetryBackoff string + +const ( + // WAFPolicyRetryBackoffExponential uses exponential backoff for retry delays. + WAFPolicyRetryBackoffExponential WAFPolicyRetryBackoff = "exponential" + // WAFPolicyRetryBackoffLinear uses linear backoff for retry delays. + WAFPolicyRetryBackoffLinear WAFPolicyRetryBackoff = "linear" +) + +// WAFSecurityLog defines the security logging configuration for app_protect_security_log directives. +// LogProfile and LogProfileBundle are mutually exclusive per security log entry. +// +// +kubebuilder:validation:XValidation:message="only one of logProfile or logProfileBundle may be set",rule="!(has(self.logProfile) && has(self.logProfileBundle))" +// +kubebuilder:validation:XValidation:message="at least one of logProfile or logProfileBundle must be set",rule="has(self.logProfile) || has(self.logProfileBundle)" +// +//nolint:lll +type WAFSecurityLog struct { + // Name is the name of the security log configuration. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$` + Name *string `json:"name,omitempty"` + + // LogProfile defines the built-in logging profile to use. + // + // +optional + LogProfile *LogProfile `json:"logProfile,omitempty"` + + // LogProfileBundle defines a custom logging profile bundle, similar to policy bundle. + // + // +optional + LogProfileBundle *WAFPolicySource `json:"logProfileBundle,omitempty"` + + // Destination defines where the security logs should be sent. + Destination SecurityLogDestination `json:"destination"` +} + +// SecurityLogDestination defines the destination for security logs. +// +// +kubebuilder:validation:XValidation:message="destination.file must be nil if the destination.type is not file",rule="!(has(self.file) && self.type != 'file')" +// +kubebuilder:validation:XValidation:message="destination.file must be specified for file destination.type",rule="!(!has(self.file) && self.type == 'file')" +// +kubebuilder:validation:XValidation:message="destination.syslog must be nil if the destination.type is not syslog",rule="!(has(self.syslog) && self.type != 'syslog')" +// +kubebuilder:validation:XValidation:message="destination.syslog must be specified for syslog destination.type",rule="!(!has(self.syslog) && self.type == 'syslog')" +// +//nolint:lll +type SecurityLogDestination struct { + // File defines the file destination configuration. + // Only valid when type is "file". + // + // +optional + File *SecurityLogFile `json:"file,omitempty"` + + // Syslog defines the syslog destination configuration. + // Only valid when type is "syslog". + // + // +optional + Syslog *SecurityLogSyslog `json:"syslog,omitempty"` + + // Type identifies the type of security log destination. + // + // +unionDiscriminator + // +kubebuilder:default=stderr + Type SecurityLogDestinationType `json:"type"` +} + +// SecurityLogDestinationType defines the supported security log destination types. +// +// +kubebuilder:validation:Enum=stderr;file;syslog +type SecurityLogDestinationType string + +const ( + // SecurityLogDestinationTypeStderr outputs logs to container stderr. + SecurityLogDestinationTypeStderr SecurityLogDestinationType = "stderr" + // SecurityLogDestinationTypeFile writes logs to a specified file path. + SecurityLogDestinationTypeFile SecurityLogDestinationType = "file" + // SecurityLogDestinationTypeSyslog sends logs to a syslog server via TCP. + SecurityLogDestinationTypeSyslog SecurityLogDestinationType = "syslog" +) + +// SecurityLogFile defines the file destination configuration for security logs. +type SecurityLogFile struct { + // Path is the file path where security logs will be written. + // Must be accessible to the waf-enforcer container. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:Pattern=`^/.*$` + Path string `json:"path"` +} + +// SecurityLogSyslog defines the syslog destination configuration for security logs. +type SecurityLogSyslog struct { + // Server is the syslog server address in the format "host:port". + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9.-]+:[0-9]+$` + Server string `json:"server"` +} + +// LogProfile defines the built-in logging profiles available in NGINX App Protect. +// +// +kubebuilder:validation:Enum=log_default;log_all;log_illegal;log_blocked;log_grpc_all;log_grpc_blocked;log_grpc_illegal +// +//nolint:lll +type LogProfile string + +const ( + // LogProfileDefault is the default logging profile. + LogProfileDefault LogProfile = "log_default" + // LogProfileAll logs all requests (blocked and passed). + LogProfileAll LogProfile = "log_all" + // LogProfileIllegal logs illegal requests. + LogProfileIllegal LogProfile = "log_illegal" + // LogProfileBlocked logs only blocked requests. + LogProfileBlocked LogProfile = "log_blocked" + // LogProfileGRPCAll logs all gRPC requests. + LogProfileGRPCAll LogProfile = "log_grpc_all" + // LogProfileGRPCBlocked logs blocked gRPC requests. + LogProfileGRPCBlocked LogProfile = "log_grpc_blocked" + // LogProfileGRPCIllegal logs illegal gRPC requests. + LogProfileGRPCIllegal LogProfile = "log_grpc_illegal" +) diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 65b3b76c30..c4a8b430ac 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -404,6 +404,61 @@ func (in *ObservabilityPolicySpec) DeepCopy() *ObservabilityPolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityLogDestination) DeepCopyInto(out *SecurityLogDestination) { + *out = *in + if in.File != nil { + in, out := &in.File, &out.File + *out = new(SecurityLogFile) + **out = **in + } + if in.Syslog != nil { + in, out := &in.Syslog, &out.Syslog + *out = new(SecurityLogSyslog) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityLogDestination. +func (in *SecurityLogDestination) DeepCopy() *SecurityLogDestination { + if in == nil { + return nil + } + out := new(SecurityLogDestination) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityLogFile) DeepCopyInto(out *SecurityLogFile) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityLogFile. +func (in *SecurityLogFile) DeepCopy() *SecurityLogFile { + if in == nil { + return nil + } + out := new(SecurityLogFile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityLogSyslog) DeepCopyInto(out *SecurityLogSyslog) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityLogSyslog. +func (in *SecurityLogSyslog) DeepCopy() *SecurityLogSyslog { + if in == nil { + return nil + } + out := new(SecurityLogSyslog) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Snippet) DeepCopyInto(out *Snippet) { *out = *in @@ -695,3 +750,256 @@ func (in *UpstreamSettingsPolicySpec) DeepCopy() *UpstreamSettingsPolicySpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicy) DeepCopyInto(out *WAFPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicy. +func (in *WAFPolicy) DeepCopy() *WAFPolicy { + if in == nil { + return nil + } + out := new(WAFPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WAFPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicyAuthSecret) DeepCopyInto(out *WAFPolicyAuthSecret) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicyAuthSecret. +func (in *WAFPolicyAuthSecret) DeepCopy() *WAFPolicyAuthSecret { + if in == nil { + return nil + } + out := new(WAFPolicyAuthSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicyList) DeepCopyInto(out *WAFPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WAFPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicyList. +func (in *WAFPolicyList) DeepCopy() *WAFPolicyList { + if in == nil { + return nil + } + out := new(WAFPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WAFPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicyPolling) DeepCopyInto(out *WAFPolicyPolling) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(Duration) + **out = **in + } + if in.ChecksumLocation != nil { + in, out := &in.ChecksumLocation, &out.ChecksumLocation + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicyPolling. +func (in *WAFPolicyPolling) DeepCopy() *WAFPolicyPolling { + if in == nil { + return nil + } + out := new(WAFPolicyPolling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicyRetry) DeepCopyInto(out *WAFPolicyRetry) { + *out = *in + if in.Attempts != nil { + in, out := &in.Attempts, &out.Attempts + *out = new(int32) + **out = **in + } + if in.Backoff != nil { + in, out := &in.Backoff, &out.Backoff + *out = new(WAFPolicyRetryBackoff) + **out = **in + } + if in.MaxDelay != nil { + in, out := &in.MaxDelay, &out.MaxDelay + *out = new(Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicyRetry. +func (in *WAFPolicyRetry) DeepCopy() *WAFPolicyRetry { + if in == nil { + return nil + } + out := new(WAFPolicyRetry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicySource) DeepCopyInto(out *WAFPolicySource) { + *out = *in + if in.AuthSecret != nil { + in, out := &in.AuthSecret, &out.AuthSecret + *out = new(WAFPolicyAuthSecret) + **out = **in + } + if in.Validation != nil { + in, out := &in.Validation, &out.Validation + *out = new(WAFPolicyValidation) + (*in).DeepCopyInto(*out) + } + if in.Polling != nil { + in, out := &in.Polling, &out.Polling + *out = new(WAFPolicyPolling) + (*in).DeepCopyInto(*out) + } + if in.Retry != nil { + in, out := &in.Retry, &out.Retry + *out = new(WAFPolicyRetry) + (*in).DeepCopyInto(*out) + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicySource. +func (in *WAFPolicySource) DeepCopy() *WAFPolicySource { + if in == nil { + return nil + } + out := new(WAFPolicySource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicySpec) DeepCopyInto(out *WAFPolicySpec) { + *out = *in + if in.PolicySource != nil { + in, out := &in.PolicySource, &out.PolicySource + *out = new(WAFPolicySource) + (*in).DeepCopyInto(*out) + } + in.TargetRef.DeepCopyInto(&out.TargetRef) + if in.SecurityLogs != nil { + in, out := &in.SecurityLogs, &out.SecurityLogs + *out = make([]WAFSecurityLog, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicySpec. +func (in *WAFPolicySpec) DeepCopy() *WAFPolicySpec { + if in == nil { + return nil + } + out := new(WAFPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFPolicyValidation) DeepCopyInto(out *WAFPolicyValidation) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]WAFPolicyValidationMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFPolicyValidation. +func (in *WAFPolicyValidation) DeepCopy() *WAFPolicyValidation { + if in == nil { + return nil + } + out := new(WAFPolicyValidation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFSecurityLog) DeepCopyInto(out *WAFSecurityLog) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.LogProfile != nil { + in, out := &in.LogProfile, &out.LogProfile + *out = new(LogProfile) + **out = **in + } + if in.LogProfileBundle != nil { + in, out := &in.LogProfileBundle, &out.LogProfileBundle + *out = new(WAFPolicySource) + (*in).DeepCopyInto(*out) + } + in.Destination.DeepCopyInto(&out.Destination) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFSecurityLog. +func (in *WAFSecurityLog) DeepCopy() *WAFSecurityLog { + if in == nil { + return nil + } + out := new(WAFSecurityLog) + in.DeepCopyInto(out) + return out +} diff --git a/apis/v1alpha2/nginxproxy_types.go b/apis/v1alpha2/nginxproxy_types.go index 43b509d06d..ee6b64d03a 100644 --- a/apis/v1alpha2/nginxproxy_types.go +++ b/apis/v1alpha2/nginxproxy_types.go @@ -7,7 +7,6 @@ import ( "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:resource:categories=nginx-gateway-fabric,scope=Namespaced @@ -42,7 +41,7 @@ type NginxProxySpec struct { // Default is "dual", meaning the server will use both IPv4 and IPv6. // // +optional - // +kubebuilder:default:=dual + // +kubebuilder:default=dual IPFamily *IPFamilyType `json:"ipFamily,omitempty"` // Telemetry specifies the OpenTelemetry configuration. // @@ -72,12 +71,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 +410,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 +429,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 +519,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. @@ -499,7 +567,7 @@ type Image struct { // PullPolicy describes a policy for if/when to pull a container image. // // +optional - // +kubebuilder:default:=IfNotPresent + // +kubebuilder:default=IfNotPresent PullPolicy *PullPolicy `json:"pullPolicy,omitempty"` } @@ -523,7 +591,7 @@ type ServiceSpec struct { // ServiceType describes ingress method for the Service. // // +optional - // +kubebuilder:default:=LoadBalancer + // +kubebuilder:default=LoadBalancer ServiceType *ServiceType `json:"type,omitempty"` // ExternalTrafficPolicy describes how nodes distribute service traffic they @@ -531,7 +599,7 @@ type ServiceSpec struct { // and LoadBalancer IPs. // // +optional - // +kubebuilder:default:=Local + // +kubebuilder:default=Local ExternalTrafficPolicy *ExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty"` // LoadBalancerIP is a static IP address for the load balancer. Requires service type to be LoadBalancer. diff --git a/apis/v1alpha2/observabilitypolicy_types.go b/apis/v1alpha2/observabilitypolicy_types.go index d46b64ddd3..258ca5c92f 100644 --- a/apis/v1alpha2/observabilitypolicy_types.go +++ b/apis/v1alpha2/observabilitypolicy_types.go @@ -7,7 +7,6 @@ import ( ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1" ) -// +genclient // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status 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 63334f029f..bf68260ae1 100644 --- a/build/Dockerfile.nginxplus +++ b/build/Dockerfile.nginxplus @@ -1,14 +1,20 @@ # syntax=docker/dockerfile:1.17 + +# 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 ARG NGINX_AGENT_VERSION=v3.0.2 +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#v} 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 3fb3f6230b..8f765c44e4 100644 --- a/charts/nginx-gateway-fabric/README.md +++ b/charts/nginx-gateway-fabric/README.md @@ -264,7 +264,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `certGenerator.ttlSecondsAfterFinished` | How long to wait after the cert generator job has finished before it is removed by the job controller. | int | `30` | | `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` | @@ -288,6 +288,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},"name":"","nodeSelector":{},"podAnnotations":{},"productTelemetry":{"enable":true},"readinessProbe":{"enable":true,"initialDelaySeconds":3,"port":8081},"replicas":1,"resources":{},"service":{"annotations":{},"labels":{}},"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/clusterrole.yaml b/charts/nginx-gateway-fabric/templates/clusterrole.yaml index 1205570535..65619ac983 100644 --- a/charts/nginx-gateway-fabric/templates/clusterrole.yaml +++ b/charts/nginx-gateway-fabric/templates/clusterrole.yaml @@ -109,6 +109,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies {{- if .Values.nginxGateway.snippetsFilters.enable }} - snippetsfilters {{- end }} @@ -122,6 +123,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status {{- if .Values.nginxGateway.snippetsFilters.enable }} - snippetsfilters/status {{- end }} 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 15582052e5..936bfddea4 100644 --- a/charts/nginx-gateway-fabric/values.schema.json +++ b/charts/nginx-gateway-fabric/values.schema.json @@ -305,6 +305,15 @@ }, "required": [], "type": "object" + }, + "waf": { + "description": "WAF enables NGINX App Protect WAF functionality.", + "enum": [ + "enabled", + "disabled" + ], + "required": [], + "type": "string" } }, "required": [], @@ -525,6 +534,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 82c427fd6b..f2c22d24ad 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -373,6 +373,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. @@ -412,6 +418,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/config/crd/bases/gateway.nginx.org_wafpolicies.yaml b/config/crd/bases/gateway.nginx.org_wafpolicies.yaml new file mode 100644 index 0000000000..3961df2f2d --- /dev/null +++ b/config/crd/bases/gateway.nginx.org_wafpolicies.yaml @@ -0,0 +1,692 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + gateway.networking.k8s.io/policy: inherited + name: wafpolicies.gateway.nginx.org +spec: + group: gateway.nginx.org + names: + categories: + - nginx-gateway-fabric + kind: WAFPolicy + listKind: WAFPolicyList + plural: wafpolicies + singular: wafpolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + WAFPolicy is an Inherited Attached Policy. It provides a way to configure NGINX App Protect Web Application Firewall + for Gateways and Routes. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the WAFPolicy. + properties: + policySource: + description: PolicySource defines the source location and configuration + for the compiled WAF policy bundle. + properties: + authSecret: + description: AuthSecret is the Secret containing authentication + credentials for the WAF policy source. + properties: + name: + description: Name is the name of the Secret containing authentication + credentials for the WAF policy source. + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + required: + - name + type: object + fileLocation: + description: FileLocation defines the location of the WAF policy + file. + maxLength: 256 + minLength: 1 + type: string + polling: + description: Polling defines the polling configuration for automatic + WAF policy change detection. + properties: + checksumLocation: + description: |- + ChecksumLocation specifies the location of the checksum file for the policy bundle. + If not specified, defaults to .sha256 + maxLength: 2048 + type: string + enabled: + default: false + description: |- + Enabled indicates whether polling is enabled for automatic WAF policy change detection. + When enabled, NGINX Gateway Fabric will periodically check for policy changes using checksum validation. + type: boolean + interval: + default: 5m + description: |- + Interval is the polling interval to check for WAF policy changes. + Must be a valid duration string (e.g., "5m", "30s", "1h"). + Defaults to "5m" if polling is enabled. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + retry: + description: Retry defines the retry configuration for WAF policy + fetch failures. + properties: + attempts: + default: 3 + description: |- + Attempts is the number of retry attempts for fetching the WAF policy. + Set to 0 to disable retries. + format: int32 + minimum: 0 + type: integer + backoff: + default: exponential + description: |- + Backoff defines the backoff strategy for retry attempts. + Supported values: "exponential", "linear" + enum: + - exponential + - linear + type: string + maxDelay: + default: 5m + description: |- + MaxDelay is the maximum delay between retry attempts. + Must be a valid duration string (e.g., "5m", "30s"). + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + timeout: + description: Timeout for policy downloads. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + validation: + description: Validation defines the validation methods for policy + integrity verification. + properties: + methods: + description: |- + Methods specifies the validation methods to use for policy integrity verification. + Currently supported: ["checksum"] + items: + description: WAFPolicyValidationMethod defines the supported + validation methods. + enum: + - checksum + type: string + type: array + x-kubernetes-list-type: set + type: object + required: + - fileLocation + type: object + securityLogs: + description: |- + SecurityLogs defines the security logging configuration for app_protect_security_log directives. + Multiple logging configurations can be specified to send logs to different destinations. + items: + description: |- + WAFSecurityLog defines the security logging configuration for app_protect_security_log directives. + LogProfile and LogProfileBundle are mutually exclusive per security log entry. + properties: + destination: + description: Destination defines where the security logs should + be sent. + properties: + file: + description: |- + File defines the file destination configuration. + Only valid when type is "file". + properties: + path: + description: |- + Path is the file path where security logs will be written. + Must be accessible to the waf-enforcer container. + maxLength: 256 + minLength: 1 + pattern: ^/.*$ + type: string + required: + - path + type: object + syslog: + description: |- + Syslog defines the syslog destination configuration. + Only valid when type is "syslog". + properties: + server: + description: Server is the syslog server address in + the format "host:port". + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9.-]+:[0-9]+$ + type: string + required: + - server + type: object + type: + default: stderr + description: Type identifies the type of security log destination. + enum: + - stderr + - file + - syslog + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: destination.file must be nil if the destination.type + is not file + rule: '!(has(self.file) && self.type != ''file'')' + - message: destination.file must be specified for file destination.type + rule: '!(!has(self.file) && self.type == ''file'')' + - message: destination.syslog must be nil if the destination.type + is not syslog + rule: '!(has(self.syslog) && self.type != ''syslog'')' + - message: destination.syslog must be specified for syslog destination.type + rule: '!(!has(self.syslog) && self.type == ''syslog'')' + logProfile: + description: LogProfile defines the built-in logging profile + to use. + enum: + - log_default + - log_all + - log_illegal + - log_blocked + - log_grpc_all + - log_grpc_blocked + - log_grpc_illegal + type: string + logProfileBundle: + description: LogProfileBundle defines a custom logging profile + bundle, similar to policy bundle. + properties: + authSecret: + description: AuthSecret is the Secret containing authentication + credentials for the WAF policy source. + properties: + name: + description: Name is the name of the Secret containing + authentication credentials for the WAF policy source. + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + required: + - name + type: object + fileLocation: + description: FileLocation defines the location of the WAF + policy file. + maxLength: 256 + minLength: 1 + type: string + polling: + description: Polling defines the polling configuration for + automatic WAF policy change detection. + properties: + checksumLocation: + description: |- + ChecksumLocation specifies the location of the checksum file for the policy bundle. + If not specified, defaults to .sha256 + maxLength: 2048 + type: string + enabled: + default: false + description: |- + Enabled indicates whether polling is enabled for automatic WAF policy change detection. + When enabled, NGINX Gateway Fabric will periodically check for policy changes using checksum validation. + type: boolean + interval: + default: 5m + description: |- + Interval is the polling interval to check for WAF policy changes. + Must be a valid duration string (e.g., "5m", "30s", "1h"). + Defaults to "5m" if polling is enabled. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + retry: + description: Retry defines the retry configuration for WAF + policy fetch failures. + properties: + attempts: + default: 3 + description: |- + Attempts is the number of retry attempts for fetching the WAF policy. + Set to 0 to disable retries. + format: int32 + minimum: 0 + type: integer + backoff: + default: exponential + description: |- + Backoff defines the backoff strategy for retry attempts. + Supported values: "exponential", "linear" + enum: + - exponential + - linear + type: string + maxDelay: + default: 5m + description: |- + MaxDelay is the maximum delay between retry attempts. + Must be a valid duration string (e.g., "5m", "30s"). + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + timeout: + description: Timeout for policy downloads. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + validation: + description: Validation defines the validation methods for + policy integrity verification. + properties: + methods: + description: |- + Methods specifies the validation methods to use for policy integrity verification. + Currently supported: ["checksum"] + items: + description: WAFPolicyValidationMethod defines the + supported validation methods. + enum: + - checksum + type: string + type: array + x-kubernetes-list-type: set + type: object + required: + - fileLocation + type: object + name: + description: Name is the name of the security log configuration. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$ + type: string + required: + - destination + type: object + x-kubernetes-validations: + - message: only one of logProfile or logProfileBundle may be set + rule: '!(has(self.logProfile) && has(self.logProfileBundle))' + - message: at least one of logProfile or logProfileBundle must be + set + rule: has(self.logProfile) || has(self.logProfileBundle) + maxItems: 32 + type: array + targetRef: + description: |- + TargetRef identifies an API object to apply the policy to. + Object must be in the same namespace as the policy. + Support: Gateway, HTTPRoute, GRPCRoute. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + x-kubernetes-validations: + - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or + GRPCRoute' + rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute') + - message: TargetRef Group must be gateway.networking.k8s.io. + rule: (self.group=='gateway.networking.k8s.io') + required: + - targetRef + type: object + x-kubernetes-validations: + - message: policySource is required when securityLogs are specified + rule: '!has(self.securityLogs) || has(self.policySource)' + status: + description: Status defines the state of the WAFPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - controllerName + type: object + maxItems: 16 + type: array + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b067d64141..ff57c6299e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,3 +7,4 @@ resources: - bases/gateway.nginx.org_observabilitypolicies.yaml - bases/gateway.nginx.org_snippetsfilters.yaml - bases/gateway.nginx.org_upstreamsettingspolicies.yaml + - bases/gateway.nginx.org_wafpolicies.yaml diff --git a/deploy/azure/deploy.yaml b/deploy/azure/deploy.yaml index 7e29ea1c66..6652fc6b99 100644 --- a/deploy/azure/deploy.yaml +++ b/deploy/azure/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -155,6 +156,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 7517ce1c4a..56a3344fd9 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,132 +7744,448 @@ spec: description: Number of desired Pods. format: int32 type: integer - type: object - service: - description: Service is the configuration for the NGINX Service. - properties: - externalTrafficPolicy: - default: Local - description: |- - ExternalTrafficPolicy describes how nodes distribute service traffic they - receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, - and LoadBalancer IPs. - enum: - - Cluster - - Local - type: string - loadBalancerClass: - description: |- - LoadBalancerClass is the class of the load balancer implementation this Service belongs to. - Requires service type to be LoadBalancer. - type: string - loadBalancerIP: - description: LoadBalancerIP is a static IP address for the - load balancer. Requires service type to be LoadBalancer. - type: string - loadBalancerSourceRanges: - description: |- - LoadBalancerSourceRanges are the IP ranges (CIDR) that are allowed to access the load balancer. - Requires service type to be LoadBalancer. - items: - type: string - type: array - nodePorts: + wafContainers: description: |- - NodePorts are the list of NodePorts to expose on the NGINX data plane service. - Each NodePort MUST map to a Gateway listener port, otherwise it will be ignored. - The default NodePort range enforced by Kubernetes is 30000-32767. - items: - description: |- - NodePort creates a port on each node on which the NGINX data plane service is exposed. The NodePort MUST - map to a Gateway listener port, otherwise it will be ignored. If not specified, Kubernetes allocates a NodePort - automatically if required. The default NodePort range enforced by Kubernetes is 30000-32767. - properties: - listenerPort: - description: |- - ListenerPort is the Gateway listener port that this NodePort maps to. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 - format: int32 - type: integer - port: - description: |- - Port is the NodePort to expose. - kubebuilder:validation:Minimum=1 - kubebuilder:validation:Maximum=65535 - format: int32 - type: integer - required: - - listenerPort - - port - type: object - type: array - type: - default: LoadBalancer - description: ServiceType describes ingress method for the - Service. - enum: - - ClusterIP - - LoadBalancer - - NodePort - type: string - type: object - type: object - x-kubernetes-validations: - - message: only one of deployment or daemonSet can be set - rule: (!has(self.deployment) && !has(self.daemonSet)) || ((has(self.deployment) - && !has(self.daemonSet)) || (!has(self.deployment) && has(self.daemonSet))) - logging: - description: Logging defines logging related settings for NGINX. - properties: - agentLevel: - default: info - description: |- - AgentLevel defines the log level of the NGINX agent process. Changing this value results in a - re-roll of the NGINX deployment. - enum: - - debug - - info - - error - - panic - - fatal - type: string - errorLevel: - default: info - description: |- - ErrorLevel defines the error log level. Possible log levels listed in order of increasing severity are - debug, info, notice, warn, error, crit, alert, and emerg. Setting a certain log level will cause all messages - of the specified and more severe log levels to be logged. For example, the log level 'error' will cause error, - crit, alert, and emerg messages to be logged. https://nginx.org/en/docs/ngx_core_module.html#error_log - enum: - - debug - - info - - notice - - warn - - error - - crit - - alert - - emerg - type: string - type: object - metrics: - description: |- - Metrics defines the configuration for Prometheus scraping metrics. Changing this value results in a - re-roll of the NGINX deployment. - properties: - disable: - description: Disable serving Prometheus metrics on the listen - port. - type: boolean - port: - description: Port where the Prometheus metrics are exposed. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - type: object - nginxPlus: - description: NginxPlus specifies NGINX Plus additional settings. + 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. + properties: + externalTrafficPolicy: + default: Local + description: |- + ExternalTrafficPolicy describes how nodes distribute service traffic they + receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, + and LoadBalancer IPs. + enum: + - Cluster + - Local + type: string + loadBalancerClass: + description: |- + LoadBalancerClass is the class of the load balancer implementation this Service belongs to. + Requires service type to be LoadBalancer. + type: string + loadBalancerIP: + description: LoadBalancerIP is a static IP address for the + load balancer. Requires service type to be LoadBalancer. + type: string + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges are the IP ranges (CIDR) that are allowed to access the load balancer. + Requires service type to be LoadBalancer. + items: + type: string + type: array + nodePorts: + description: |- + NodePorts are the list of NodePorts to expose on the NGINX data plane service. + Each NodePort MUST map to a Gateway listener port, otherwise it will be ignored. + The default NodePort range enforced by Kubernetes is 30000-32767. + items: + description: |- + NodePort creates a port on each node on which the NGINX data plane service is exposed. The NodePort MUST + map to a Gateway listener port, otherwise it will be ignored. If not specified, Kubernetes allocates a NodePort + automatically if required. The default NodePort range enforced by Kubernetes is 30000-32767. + properties: + listenerPort: + description: |- + ListenerPort is the Gateway listener port that this NodePort maps to. + kubebuilder:validation:Minimum=1 + kubebuilder:validation:Maximum=65535 + format: int32 + type: integer + port: + description: |- + Port is the NodePort to expose. + kubebuilder:validation:Minimum=1 + kubebuilder:validation:Maximum=65535 + format: int32 + type: integer + required: + - listenerPort + - port + type: object + type: array + type: + default: LoadBalancer + description: ServiceType describes ingress method for the + Service. + enum: + - ClusterIP + - LoadBalancer + - NodePort + type: string + type: object + type: object + x-kubernetes-validations: + - message: only one of deployment or daemonSet can be set + rule: (!has(self.deployment) && !has(self.daemonSet)) || ((has(self.deployment) + && !has(self.daemonSet)) || (!has(self.deployment) && has(self.daemonSet))) + logging: + description: Logging defines logging related settings for NGINX. + properties: + agentLevel: + default: info + description: |- + AgentLevel defines the log level of the NGINX agent process. Changing this value results in a + re-roll of the NGINX deployment. + enum: + - debug + - info + - error + - panic + - fatal + type: string + errorLevel: + default: info + description: |- + ErrorLevel defines the error log level. Possible log levels listed in order of increasing severity are + debug, info, notice, warn, error, crit, alert, and emerg. Setting a certain log level will cause all messages + of the specified and more severe log levels to be logged. For example, the log level 'error' will cause error, + crit, alert, and emerg messages to be logged. https://nginx.org/en/docs/ngx_core_module.html#error_log + enum: + - debug + - info + - notice + - warn + - error + - crit + - alert + - emerg + type: string + type: object + metrics: + description: |- + Metrics defines the configuration for Prometheus scraping metrics. Changing this value results in a + re-roll of the NGINX deployment. + properties: + disable: + description: Disable serving Prometheus metrics on the listen + port. + type: boolean + port: + description: Port where the Prometheus metrics are exposed. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + type: object + nginxPlus: + description: NginxPlus specifies NGINX Plus additional settings. properties: allowedAddresses: description: AllowedAddresses specifies IPAddresses or CIDR blocks @@ -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 @@ -9308,3 +9951,695 @@ spec: storage: true subresources: status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + gateway.networking.k8s.io/policy: inherited + name: wafpolicies.gateway.nginx.org +spec: + group: gateway.nginx.org + names: + categories: + - nginx-gateway-fabric + kind: WAFPolicy + listKind: WAFPolicyList + plural: wafpolicies + singular: wafpolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + WAFPolicy is an Inherited Attached Policy. It provides a way to configure NGINX App Protect Web Application Firewall + for Gateways and Routes. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the WAFPolicy. + properties: + policySource: + description: PolicySource defines the source location and configuration + for the compiled WAF policy bundle. + properties: + authSecret: + description: AuthSecret is the Secret containing authentication + credentials for the WAF policy source. + properties: + name: + description: Name is the name of the Secret containing authentication + credentials for the WAF policy source. + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + required: + - name + type: object + fileLocation: + description: FileLocation defines the location of the WAF policy + file. + maxLength: 256 + minLength: 1 + type: string + polling: + description: Polling defines the polling configuration for automatic + WAF policy change detection. + properties: + checksumLocation: + description: |- + ChecksumLocation specifies the location of the checksum file for the policy bundle. + If not specified, defaults to .sha256 + maxLength: 2048 + type: string + enabled: + default: false + description: |- + Enabled indicates whether polling is enabled for automatic WAF policy change detection. + When enabled, NGINX Gateway Fabric will periodically check for policy changes using checksum validation. + type: boolean + interval: + default: 5m + description: |- + Interval is the polling interval to check for WAF policy changes. + Must be a valid duration string (e.g., "5m", "30s", "1h"). + Defaults to "5m" if polling is enabled. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + retry: + description: Retry defines the retry configuration for WAF policy + fetch failures. + properties: + attempts: + default: 3 + description: |- + Attempts is the number of retry attempts for fetching the WAF policy. + Set to 0 to disable retries. + format: int32 + minimum: 0 + type: integer + backoff: + default: exponential + description: |- + Backoff defines the backoff strategy for retry attempts. + Supported values: "exponential", "linear" + enum: + - exponential + - linear + type: string + maxDelay: + default: 5m + description: |- + MaxDelay is the maximum delay between retry attempts. + Must be a valid duration string (e.g., "5m", "30s"). + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + timeout: + description: Timeout for policy downloads. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + validation: + description: Validation defines the validation methods for policy + integrity verification. + properties: + methods: + description: |- + Methods specifies the validation methods to use for policy integrity verification. + Currently supported: ["checksum"] + items: + description: WAFPolicyValidationMethod defines the supported + validation methods. + enum: + - checksum + type: string + type: array + x-kubernetes-list-type: set + type: object + required: + - fileLocation + type: object + securityLogs: + description: |- + SecurityLogs defines the security logging configuration for app_protect_security_log directives. + Multiple logging configurations can be specified to send logs to different destinations. + items: + description: |- + WAFSecurityLog defines the security logging configuration for app_protect_security_log directives. + LogProfile and LogProfileBundle are mutually exclusive per security log entry. + properties: + destination: + description: Destination defines where the security logs should + be sent. + properties: + file: + description: |- + File defines the file destination configuration. + Only valid when type is "file". + properties: + path: + description: |- + Path is the file path where security logs will be written. + Must be accessible to the waf-enforcer container. + maxLength: 256 + minLength: 1 + pattern: ^/.*$ + type: string + required: + - path + type: object + syslog: + description: |- + Syslog defines the syslog destination configuration. + Only valid when type is "syslog". + properties: + server: + description: Server is the syslog server address in + the format "host:port". + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9.-]+:[0-9]+$ + type: string + required: + - server + type: object + type: + default: stderr + description: Type identifies the type of security log destination. + enum: + - stderr + - file + - syslog + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: destination.file must be nil if the destination.type + is not file + rule: '!(has(self.file) && self.type != ''file'')' + - message: destination.file must be specified for file destination.type + rule: '!(!has(self.file) && self.type == ''file'')' + - message: destination.syslog must be nil if the destination.type + is not syslog + rule: '!(has(self.syslog) && self.type != ''syslog'')' + - message: destination.syslog must be specified for syslog destination.type + rule: '!(!has(self.syslog) && self.type == ''syslog'')' + logProfile: + description: LogProfile defines the built-in logging profile + to use. + enum: + - log_default + - log_all + - log_illegal + - log_blocked + - log_grpc_all + - log_grpc_blocked + - log_grpc_illegal + type: string + logProfileBundle: + description: LogProfileBundle defines a custom logging profile + bundle, similar to policy bundle. + properties: + authSecret: + description: AuthSecret is the Secret containing authentication + credentials for the WAF policy source. + properties: + name: + description: Name is the name of the Secret containing + authentication credentials for the WAF policy source. + maxLength: 253 + minLength: 1 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + required: + - name + type: object + fileLocation: + description: FileLocation defines the location of the WAF + policy file. + maxLength: 256 + minLength: 1 + type: string + polling: + description: Polling defines the polling configuration for + automatic WAF policy change detection. + properties: + checksumLocation: + description: |- + ChecksumLocation specifies the location of the checksum file for the policy bundle. + If not specified, defaults to .sha256 + maxLength: 2048 + type: string + enabled: + default: false + description: |- + Enabled indicates whether polling is enabled for automatic WAF policy change detection. + When enabled, NGINX Gateway Fabric will periodically check for policy changes using checksum validation. + type: boolean + interval: + default: 5m + description: |- + Interval is the polling interval to check for WAF policy changes. + Must be a valid duration string (e.g., "5m", "30s", "1h"). + Defaults to "5m" if polling is enabled. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + retry: + description: Retry defines the retry configuration for WAF + policy fetch failures. + properties: + attempts: + default: 3 + description: |- + Attempts is the number of retry attempts for fetching the WAF policy. + Set to 0 to disable retries. + format: int32 + minimum: 0 + type: integer + backoff: + default: exponential + description: |- + Backoff defines the backoff strategy for retry attempts. + Supported values: "exponential", "linear" + enum: + - exponential + - linear + type: string + maxDelay: + default: 5m + description: |- + MaxDelay is the maximum delay between retry attempts. + Must be a valid duration string (e.g., "5m", "30s"). + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + type: object + timeout: + description: Timeout for policy downloads. + pattern: ^[0-9]{1,4}(ms|s|m|h)?$ + type: string + validation: + description: Validation defines the validation methods for + policy integrity verification. + properties: + methods: + description: |- + Methods specifies the validation methods to use for policy integrity verification. + Currently supported: ["checksum"] + items: + description: WAFPolicyValidationMethod defines the + supported validation methods. + enum: + - checksum + type: string + type: array + x-kubernetes-list-type: set + type: object + required: + - fileLocation + type: object + name: + description: Name is the name of the security log configuration. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$ + type: string + required: + - destination + type: object + x-kubernetes-validations: + - message: only one of logProfile or logProfileBundle may be set + rule: '!(has(self.logProfile) && has(self.logProfileBundle))' + - message: at least one of logProfile or logProfileBundle must be + set + rule: has(self.logProfile) || has(self.logProfileBundle) + maxItems: 32 + type: array + targetRef: + description: |- + TargetRef identifies an API object to apply the policy to. + Object must be in the same namespace as the policy. + Support: Gateway, HTTPRoute, GRPCRoute. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + x-kubernetes-validations: + - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or + GRPCRoute' + rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute') + - message: TargetRef Group must be gateway.networking.k8s.io. + rule: (self.group=='gateway.networking.k8s.io') + required: + - targetRef + type: object + x-kubernetes-validations: + - message: policySource is required when securityLogs are specified + rule: '!has(self.securityLogs) || has(self.policySource)' + status: + description: Status defines the state of the WAFPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - controllerName + type: object + maxItems: 16 + type: array + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/default/deploy.yaml b/deploy/default/deploy.yaml index 199131b2a4..31f1d5c9f3 100644 --- a/deploy/default/deploy.yaml +++ b/deploy/default/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -155,6 +156,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/experimental-nginx-plus/deploy.yaml b/deploy/experimental-nginx-plus/deploy.yaml index 46844c4e47..1d59b626c8 100644 --- a/deploy/experimental-nginx-plus/deploy.yaml +++ b/deploy/experimental-nginx-plus/deploy.yaml @@ -149,6 +149,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -159,6 +160,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/experimental/deploy.yaml b/deploy/experimental/deploy.yaml index 0dbeac7329..150ffd946b 100644 --- a/deploy/experimental/deploy.yaml +++ b/deploy/experimental/deploy.yaml @@ -149,6 +149,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -159,6 +160,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/nginx-plus/deploy.yaml b/deploy/nginx-plus/deploy.yaml index 73e985ebc2..dd973bcf0e 100644 --- a/deploy/nginx-plus/deploy.yaml +++ b/deploy/nginx-plus/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -155,6 +156,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/nodeport/deploy.yaml b/deploy/nodeport/deploy.yaml index a2725a6473..a0c9decc54 100644 --- a/deploy/nodeport/deploy.yaml +++ b/deploy/nodeport/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -155,6 +156,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/openshift/deploy.yaml b/deploy/openshift/deploy.yaml index 99485c69bd..f047ca8763 100644 --- a/deploy/openshift/deploy.yaml +++ b/deploy/openshift/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies verbs: - list - watch @@ -155,6 +156,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status verbs: - update - apiGroups: diff --git a/deploy/snippets-filters-nginx-plus/deploy.yaml b/deploy/snippets-filters-nginx-plus/deploy.yaml index 6cc0026877..47a36dac99 100644 --- a/deploy/snippets-filters-nginx-plus/deploy.yaml +++ b/deploy/snippets-filters-nginx-plus/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies - snippetsfilters verbs: - list @@ -156,6 +157,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status - snippetsfilters/status verbs: - update diff --git a/deploy/snippets-filters/deploy.yaml b/deploy/snippets-filters/deploy.yaml index 9bb597289d..19fd4d9f69 100644 --- a/deploy/snippets-filters/deploy.yaml +++ b/deploy/snippets-filters/deploy.yaml @@ -145,6 +145,7 @@ rules: - clientsettingspolicies - observabilitypolicies - upstreamsettingspolicies + - wafpolicies - snippetsfilters verbs: - list @@ -156,6 +157,7 @@ rules: - clientsettingspolicies/status - observabilitypolicies/status - upstreamsettingspolicies/status + - wafpolicies/status - snippetsfilters/status verbs: - update 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..8590a779b0 100644 --- a/internal/controller/provisioner/objects.go +++ b/internal/controller/provisioner/objects.go @@ -33,9 +33,21 @@ const ( defaultServiceType = corev1.ServiceTypeLoadBalancer defaultServicePolicy = corev1.ServiceExternalTrafficPolicyLocal - defaultNginxImagePath = "ghcr.io/nginx/nginx-gateway-fabric/nginx" - defaultNginxPlusImagePath = "private-registry.nginx.com/nginx-gateway-fabric/nginx-plus" - defaultImagePullPolicy = corev1.PullIfNotPresent + defaultNginxImagePath = "ghcr.io/nginx/nginx-gateway-fabric/nginx" + defaultNginxPlusImagePath = "private-registry.nginx.com/nginx-gateway-fabric/nginx-plus" + defaultNginxPlusWafImagePath = "private-registry.nginx.com/nginx-gateway-fabric/nginx-plus-nap-waf" + 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 +565,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 +578,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 +654,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 +900,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 +985,14 @@ func (p *NginxProvisioner) buildImage(nProxyCfg *graph.EffectiveNginxProxy) (str tag := p.cfg.GatewayPodConfig.Version pullPolicy := defaultImagePullPolicy + if p.cfg.Plus { + image = defaultNginxPlusImagePath + } + + if graph.WAFEnabledForNginxProxy(nProxyCfg) { + image = defaultNginxPlusWafImagePath + } + getImageAndPullPolicy := func(container ngfAPIv1alpha2.ContainerSpec) (string, string, corev1.PullPolicy) { if container.Image != nil { if container.Image.Repository != nil { @@ -895,6 +1020,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()