Repository: nginx/nginx-gateway-fabric

Issue:
Allow users to submit custom patches to nginx deployment/service specs
<!--
WARNING: Prior to submitting an enhancement request, we ask that you create a discussion. If you have not yet
created a discussion related to your request, please do so now: https://github.com/nginx/nginx-gateway-fabric/discussions/new?category=ideas
-->

**Is your enhancement request related to a problem? Please describe.**
There are many Deployment and Service fields that can be customized, and having to expose all of them in the NginxProxy resource is tedious and unnecessary for uncommonly used fields.

**What would you like to be added**:
Add a way to submit custom patches to unblock users who need to set fields that are not yet exposed in the NginxProxy CRD.

**Why this is needed**:
Reduce blockages for fields that users need to have set.

### Acceptance
- User has a method to patch deployment and service objects provisioned by NGF with Kubernetes patch JSON
- Default patch type is StrategicMerge
- The [data plane configuration doc](https://docs.nginx.com/nginx-gateway-fabric/how-to/data-plane-configuration/#configure-infrastructure-related-settings) is updated to demonstrate how patches can be applied to provisioned objects.

### Dev notes
- Patch should accept a Patch Type and raw JSON
- We can use marshaling and patch merging to copy the user patch on top of the deployment/service objects before provisioning.


Discussion:

Solution patch:
diff --git a/apis/v1alpha2/nginxproxy_types.go b/apis/v1alpha2/nginxproxy_types.go
index c27b9a7e..9ebffa7f 100644
--- a/apis/v1alpha2/nginxproxy_types.go
+++ b/apis/v1alpha2/nginxproxy_types.go
@@ -2,6 +2,7 @@ package v1alpha2
 
 import (
 	corev1 "k8s.io/api/core/v1"
+	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
@@ -388,6 +389,36 @@ type KubernetesSpec struct {
 	Service *ServiceSpec `json:"service,omitempty"`
 }
 
+// Patch defines a patch to apply to a Kubernetes object.
+type Patch struct {
+	// Type is the type of patch. Defaults to StrategicMerge.
+	//
+	// +optional
+	// +kubebuilder:default:=StrategicMerge
+	Type *PatchType `json:"type,omitempty"`
+
+	// Value is the patch data as raw JSON.
+	// For StrategicMerge and Merge patches, this should be a JSON object.
+	// For JSONPatch patches, this should be a JSON array of patch operations.
+	//
+	// +optional
+	// +kubebuilder:validation:XPreserveUnknownFields
+	Value *apiextv1.JSON `json:"value,omitempty"`
+}
+
+// PatchType specifies the type of patch.
+// +kubebuilder:validation:Enum=StrategicMerge;Merge;JSONPatch
+type PatchType string
+
+const (
+	// PatchTypeStrategicMerge uses strategic merge patch.
+	PatchTypeStrategicMerge PatchType = "StrategicMerge"
+	// PatchTypeMerge uses merge patch (RFC 7386).
+	PatchTypeMerge PatchType = "Merge"
+	// PatchTypeJSONPatch uses JSON patch (RFC 6902).
+	PatchTypeJSONPatch PatchType = "JSONPatch"
+)
+
 // Deployment is the configuration for the NGINX Deployment.
 type DeploymentSpec struct {
 	// Container defines container fields for the NGINX container.
@@ -404,6 +435,11 @@ type DeploymentSpec struct {
 	//
 	// +optional
 	Pod PodSpec `json:"pod"`
+
+	// Patches are custom patches to apply to the NGINX Deployment.
+	//
+	// +optional
+	Patches []Patch `json:"patches,omitempty"`
 }
 
 // DaemonSet is the configuration for the NGINX DaemonSet.
@@ -417,6 +453,11 @@ type DaemonSetSpec struct {
 	//
 	// +optional
 	Pod PodSpec `json:"pod"`
+
+	// Patches are custom patches to apply to the NGINX DaemonSet.
+	//
+	// +optional
+	Patches []Patch `json:"patches,omitempty"`
 }
 
 // PodSpec defines Pod-specific fields.
@@ -594,6 +635,11 @@ type ServiceSpec struct {
 	//
 	// +optional
 	NodePorts []NodePort `json:"nodePorts,omitempty"`
+
+	// Patches are custom patches to apply to the NGINX Service.
+	//
+	// +optional
+	Patches []Patch `json:"patches,omitempty"`
 }
 
 // ServiceType describes ingress method for the Service.
diff --git a/apis/v1alpha2/zz_generated.deepcopy.go b/apis/v1alpha2/zz_generated.deepcopy.go
index cd523929..1e4f5955 100644
--- a/apis/v1alpha2/zz_generated.deepcopy.go
+++ b/apis/v1alpha2/zz_generated.deepcopy.go
@@ -6,7 +6,8 @@ package v1alpha2
 
 import (
 	"github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
-	"k8s.io/api/core/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	apisv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 )
@@ -26,12 +27,12 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) {
 	}
 	if in.Resources != nil {
 		in, out := &in.Resources, &out.Resources
-		*out = new(v1.ResourceRequirements)
+		*out = new(corev1.ResourceRequirements)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.Lifecycle != nil {
 		in, out := &in.Lifecycle, &out.Lifecycle
-		*out = new(v1.Lifecycle)
+		*out = new(corev1.Lifecycle)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.ReadinessProbe != nil {
@@ -46,7 +47,7 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) {
 	}
 	if in.VolumeMounts != nil {
 		in, out := &in.VolumeMounts, &out.VolumeMounts
-		*out = make([]v1.VolumeMount, len(*in))
+		*out = make([]corev1.VolumeMount, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -68,6 +69,13 @@ func (in *DaemonSetSpec) DeepCopyInto(out *DaemonSetSpec) {
 	*out = *in
 	in.Container.DeepCopyInto(&out.Container)
 	in.Pod.DeepCopyInto(&out.Pod)
+	if in.Patches != nil {
+		in, out := &in.Patches, &out.Patches
+		*out = make([]Patch, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DaemonSetSpec.
@@ -90,6 +98,13 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
 		**out = **in
 	}
 	in.Pod.DeepCopyInto(&out.Pod)
+	if in.Patches != nil {
+		in, out := &in.Patches, &out.Patches
+		*out = make([]Patch, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentSpec.
@@ -481,6 +496,31 @@ 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 *Patch) DeepCopyInto(out *Patch) {
+	*out = *in
+	if in.Type != nil {
+		in, out := &in.Type, &out.Type
+		*out = new(PatchType)
+		**out = **in
+	}
+	if in.Value != nil {
+		in, out := &in.Value, &out.Value
+		*out = new(v1.JSON)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Patch.
+func (in *Patch) DeepCopy() *Patch {
+	if in == nil {
+		return nil
+	}
+	out := new(Patch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PodSpec) DeepCopyInto(out *PodSpec) {
 	*out = *in
@@ -491,7 +531,7 @@ func (in *PodSpec) DeepCopyInto(out *PodSpec) {
 	}
 	if in.Affinity != nil {
 		in, out := &in.Affinity, &out.Affinity
-		*out = new(v1.Affinity)
+		*out = new(corev1.Affinity)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.NodeSelector != nil {
@@ -503,21 +543,21 @@ func (in *PodSpec) DeepCopyInto(out *PodSpec) {
 	}
 	if in.Tolerations != nil {
 		in, out := &in.Tolerations, &out.Tolerations
-		*out = make([]v1.Toleration, len(*in))
+		*out = make([]corev1.Toleration, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 	if in.Volumes != nil {
 		in, out := &in.Volumes, &out.Volumes
-		*out = make([]v1.Volume, len(*in))
+		*out = make([]corev1.Volume, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 	if in.TopologySpreadConstraints != nil {
 		in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints
-		*out = make([]v1.TopologySpreadConstraint, len(*in))
+		*out = make([]corev1.TopologySpreadConstraint, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -637,6 +677,13 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
 		*out = make([]NodePort, len(*in))
 		copy(*out, *in)
 	}
+	if in.Patches != nil {
+		in, out := &in.Patches, &out.Patches
+		*out = make([]Patch, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml
index 3bcaf309..14def1da 100644
--- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml
+++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml
@@ -511,6 +511,30 @@ spec:
                               type: object
                             type: array
                         type: object
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          DaemonSet.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       pod:
                         description: Pod defines Pod-specific fields.
                         properties:
@@ -3944,6 +3968,30 @@ spec:
                               type: object
                             type: array
                         type: object
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          Deployment.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       pod:
                         description: Pod defines Pod-specific fields.
                         properties:
@@ -6996,6 +7044,30 @@ spec:
                           - port
                           type: object
                         type: array
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          Service.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       type:
                         default: LoadBalancer
                         description: ServiceType describes ingress method for the
diff --git a/deploy/crds.yaml b/deploy/crds.yaml
index 555934b6..db7e8498 100644
--- a/deploy/crds.yaml
+++ b/deploy/crds.yaml
@@ -1096,6 +1096,30 @@ spec:
                               type: object
                             type: array
                         type: object
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          DaemonSet.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       pod:
                         description: Pod defines Pod-specific fields.
                         properties:
@@ -4529,6 +4553,30 @@ spec:
                               type: object
                             type: array
                         type: object
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          Deployment.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       pod:
                         description: Pod defines Pod-specific fields.
                         properties:
@@ -7581,6 +7629,30 @@ spec:
                           - port
                           type: object
                         type: array
+                      patches:
+                        description: Patches are custom patches to apply to the NGINX
+                          Service.
+                        items:
+                          description: Patch defines a patch to apply to a Kubernetes
+                            object.
+                          properties:
+                            type:
+                              default: StrategicMerge
+                              description: Type is the type of patch. Defaults to
+                                StrategicMerge.
+                              enum:
+                              - StrategicMerge
+                              - Merge
+                              - JSONPatch
+                              type: string
+                            value:
+                              description: |-
+                                Value is the patch data as raw JSON.
+                                For StrategicMerge and Merge patches, this should be a JSON object.
+                                For JSONPatch patches, this should be a JSON array of patch operations.
+                              x-kubernetes-preserve-unknown-fields: true
+                          type: object
+                        type: array
                       type:
                         default: LoadBalancer
                         description: ServiceType describes ingress method for the
diff --git a/go.mod b/go.mod
index 9b93ffeb..25bff444 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
 	golang.org/x/text v0.27.0
 	google.golang.org/grpc v1.74.0
 	google.golang.org/protobuf v1.36.6
+	gopkg.in/evanphx/json-patch.v4 v4.12.0
 	k8s.io/api v0.33.3
 	k8s.io/apiextensions-apiserver v0.33.3
 	k8s.io/apimachinery v0.33.3
@@ -81,7 +82,6 @@ require (
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
diff --git a/internal/controller/provisioner/objects.go b/internal/controller/provisioner/objects.go
index d24a8971..8a9d39c0 100644
--- a/internal/controller/provisioner/objects.go
+++ b/internal/controller/provisioner/objects.go
@@ -2,6 +2,7 @@ package provisioner
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"maps"
@@ -9,12 +10,14 @@ import (
 	"strconv"
 	"time"
 
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/apimachinery/pkg/util/strategicpatch"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
@@ -47,6 +50,8 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 	gateway *gatewayv1.Gateway,
 	nProxyCfg *graph.EffectiveNginxProxy,
 ) ([]client.Object, error) {
+	var errs []error
+
 	// Need to ensure nginx resource objects are generated deterministically. Specifically when generating
 	// an object's field by ranging over a map, since ranging over a map is done in random order, we need to
 	// do some processing to ensure the generated results are the same each time.
@@ -108,6 +113,9 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		caSecretName,
 		clientSSLSecretName,
 	)
+	if err != nil {
+		errs = append(errs, err)
+	}
 
 	configmaps := p.buildNginxConfigMaps(
 		objectMeta,
@@ -133,8 +141,12 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		ports[int32(listener.Port)] = struct{}{}
 	}
 
-	service := buildNginxService(objectMeta, nProxyCfg, ports, selectorLabels)
-	deployment := p.buildNginxDeployment(
+	service, err := buildNginxService(objectMeta, nProxyCfg, ports, selectorLabels)
+	if err != nil {
+		errs = append(errs, err)
+	}
+
+	deployment, err := p.buildNginxDeployment(
 		objectMeta,
 		nProxyCfg,
 		ngxIncludesConfigMapName,
@@ -147,6 +159,9 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		caSecretName,
 		clientSSLSecretName,
 	)
+	if err != nil {
+		errs = append(errs, err)
+	}
 
 	// order to install resources:
 	// secrets
@@ -165,7 +180,7 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 	}
 	objects = append(objects, service, deployment)
 
-	return objects, err
+	return objects, errors.Join(errs...)
 }
 
 func (p *NginxProvisioner) buildNginxSecrets(
@@ -421,7 +436,7 @@ func buildNginxService(
 	nProxyCfg *graph.EffectiveNginxProxy,
 	ports map[int32]struct{},
 	selectorLabels map[string]string,
-) *corev1.Service {
+) (*corev1.Service, error) {
 	var serviceCfg ngfAPIv1alpha2.ServiceSpec
 	if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.Service != nil {
 		serviceCfg = *nProxyCfg.Kubernetes.Service
@@ -478,17 +493,16 @@ func buildNginxService(
 
 	setIPFamily(nProxyCfg, svc)
 
-	if serviceCfg.LoadBalancerIP != nil {
-		svc.Spec.LoadBalancerIP = *serviceCfg.LoadBalancerIP
-	}
-	if serviceCfg.LoadBalancerClass != nil {
-		svc.Spec.LoadBalancerClass = serviceCfg.LoadBalancerClass
-	}
-	if serviceCfg.LoadBalancerSourceRanges != nil {
-		svc.Spec.LoadBalancerSourceRanges = serviceCfg.LoadBalancerSourceRanges
+	setSvcLoadBalancerSettings(serviceCfg, &svc.Spec)
+
+	// Apply service patches
+	if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.Service != nil {
+		if err := applyPatches(svc, nProxyCfg.Kubernetes.Service.Patches); err != nil {
+			return svc, fmt.Errorf("failed to apply service patches: %w", err)
+		}
 	}
 
-	return svc
+	return svc, nil
 }
 
 func setIPFamily(nProxyCfg *graph.EffectiveNginxProxy, svc *corev1.Service) {
@@ -502,6 +516,18 @@ func setIPFamily(nProxyCfg *graph.EffectiveNginxProxy, svc *corev1.Service) {
 	}
 }
 
+func setSvcLoadBalancerSettings(svcCfg ngfAPIv1alpha2.ServiceSpec, svcSpec *corev1.ServiceSpec) {
+	if svcCfg.LoadBalancerIP != nil {
+		svcSpec.LoadBalancerIP = *svcCfg.LoadBalancerIP
+	}
+	if svcCfg.LoadBalancerClass != nil {
+		svcSpec.LoadBalancerClass = svcCfg.LoadBalancerClass
+	}
+	if svcCfg.LoadBalancerSourceRanges != nil {
+		svcSpec.LoadBalancerSourceRanges = svcCfg.LoadBalancerSourceRanges
+	}
+}
+
 func (p *NginxProvisioner) buildNginxDeployment(
 	objectMeta metav1.ObjectMeta,
 	nProxyCfg *graph.EffectiveNginxProxy,
@@ -514,7 +540,7 @@ func (p *NginxProvisioner) buildNginxDeployment(
 	jwtSecretName string,
 	caSecretName string,
 	clientSSLSecretName string,
-) client.Object {
+) (client.Object, error) {
 	podTemplateSpec := p.buildNginxPodTemplateSpec(
 		objectMeta,
 		nProxyCfg,
@@ -529,7 +555,7 @@ func (p *NginxProvisioner) buildNginxDeployment(
 	)
 
 	if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.DaemonSet != nil {
-		return &appsv1.DaemonSet{
+		daemonSet := &appsv1.DaemonSet{
 			ObjectMeta: objectMeta,
 			Spec: appsv1.DaemonSetSpec{
 				Selector: &metav1.LabelSelector{
@@ -538,6 +564,13 @@ func (p *NginxProvisioner) buildNginxDeployment(
 				Template: podTemplateSpec,
 			},
 		}
+
+		// Apply DaemonSet patches
+		if err := applyPatches(daemonSet, nProxyCfg.Kubernetes.DaemonSet.Patches); err != nil {
+			return daemonSet, fmt.Errorf("failed to apply daemonset patches: %w", err)
+		}
+
+		return daemonSet, nil
 	}
 
 	deployment := &appsv1.Deployment{
@@ -553,13 +586,77 @@ func (p *NginxProvisioner) buildNginxDeployment(
 	var deploymentCfg ngfAPIv1alpha2.DeploymentSpec
 	if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.Deployment != nil {
 		deploymentCfg = *nProxyCfg.Kubernetes.Deployment
+		// Apply Deployment patches
+		if err := applyPatches(deployment, nProxyCfg.Kubernetes.Deployment.Patches); err != nil {
+			return deployment, fmt.Errorf("failed to apply deployment patches: %w", err)
+		}
 	}
 
 	if deploymentCfg.Replicas != nil {
 		deployment.Spec.Replicas = deploymentCfg.Replicas
 	}
 
-	return deployment
+	return deployment, nil
+}
+
+// applyPatches applies the provided patches to the given object.
+func applyPatches(obj client.Object, patches []ngfAPIv1alpha2.Patch) error {
+	if len(patches) == 0 {
+		return nil
+	}
+
+	// Convert object to JSON
+	objData, err := json.Marshal(obj)
+	if err != nil {
+		return fmt.Errorf("failed to marshal object: %w", err)
+	}
+
+	// Apply each patch in sequence
+	for i, patch := range patches {
+		if patch.Value == nil || len(patch.Value.Raw) == 0 {
+			continue
+		}
+		patchType := ngfAPIv1alpha2.PatchTypeStrategicMerge
+		if patch.Type != nil {
+			patchType = *patch.Type
+		}
+
+		patchData := patch.Value.Raw
+		var patchedData []byte
+
+		switch patchType {
+		case ngfAPIv1alpha2.PatchTypeStrategicMerge:
+			patchedData, err = strategicpatch.StrategicMergePatch(objData, patchData, obj)
+			if err != nil {
+				return fmt.Errorf("failed to apply %s patch %d: %w", patchType, i, err)
+			}
+		case ngfAPIv1alpha2.PatchTypeMerge:
+			patchedData, err = jsonpatch.MergePatch(objData, patchData)
+			if err != nil {
+				return fmt.Errorf("failed to apply %s patch %d: %w", patchType, i, err)
+			}
+		case ngfAPIv1alpha2.PatchTypeJSONPatch:
+			jsonPatch, err := jsonpatch.DecodePatch(patchData)
+			if err != nil {
+				return fmt.Errorf("failed to decode json patch %d: %w", i, err)
+			}
+			patchedData, err = jsonPatch.Apply(objData)
+			if err != nil {
+				return fmt.Errorf("failed to apply %s patch %d: %w", patchType, i, err)
+			}
+		default:
+			return fmt.Errorf("unsupported patch type: %s", patchType)
+		}
+
+		objData = patchedData
+	}
+
+	// Unmarshal back to object
+	if err := json.Unmarshal(objData, obj); err != nil {
+		return fmt.Errorf("failed to unmarshal patched object: %w", err)
+	}
+
+	return nil
 }
 
 //nolint:gocyclo // will refactor at some point



Test patch:
diff --git a/internal/controller/provisioner/objects_test.go b/internal/controller/provisioner/objects_test.go
index 6486e61b..c32310b4 100644
--- a/internal/controller/provisioner/objects_test.go
+++ b/internal/controller/provisioner/objects_test.go
@@ -8,6 +8,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
+	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -1162,3 +1163,268 @@ func TestBuildReadinessProbe(t *testing.T) {
 		})
 	}
 }
+
+func TestBuildNginxResourceObjects_Patches(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},
+			},
+		},
+	}
+
+	// Test successful patches with all three resource types and all patch types
+	nProxyCfg := &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			Service: &ngfAPIv1alpha2.ServiceSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"svc-strategic":"true"}}}`),
+						},
+					},
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"svc-merge":"true"}}}`),
+						},
+					},
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeJSONPatch),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`[{"op": "add", "path": "/metadata/labels/svc-json", "value": "true"}]`),
+						},
+					},
+				},
+			},
+			Deployment: &ngfAPIv1alpha2.DeploymentSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"dep-patched":"true"}},"spec":{"replicas":3}}`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err := provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(objects).To(HaveLen(6))
+
+	// Find and validate service
+	var svc *corev1.Service
+	for _, obj := range objects {
+		if s, ok := obj.(*corev1.Service); ok {
+			svc = s
+			break
+		}
+	}
+	g.Expect(svc).ToNot(BeNil())
+	g.Expect(svc.Labels).To(HaveKeyWithValue("svc-strategic", "true"))
+	g.Expect(svc.Labels).To(HaveKeyWithValue("svc-merge", "true"))
+	g.Expect(svc.Labels).To(HaveKeyWithValue("svc-json", "true"))
+
+	// Find and validate deployment
+	var dep *appsv1.Deployment
+	for _, obj := range objects {
+		if d, ok := obj.(*appsv1.Deployment); ok {
+			dep = d
+			break
+		}
+	}
+	g.Expect(dep).ToNot(BeNil())
+	g.Expect(dep.Labels).To(HaveKeyWithValue("dep-patched", "true"))
+	g.Expect(dep.Spec.Replicas).To(Equal(helpers.GetPointer(int32(3))))
+
+	// Test that a later patch overrides a field set by an earlier patch
+	nProxyCfg = &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			Service: &ngfAPIv1alpha2.ServiceSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"override-label":"first"}}}`),
+						},
+					},
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"override-label":"second"}}}`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err = provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(objects).To(HaveLen(6))
+
+	// Find and validate service label override
+	svc = nil
+	for _, obj := range objects {
+		if s, ok := obj.(*corev1.Service); ok {
+			svc = s
+			break
+		}
+	}
+	g.Expect(svc).ToNot(BeNil())
+	g.Expect(svc.Labels).To(HaveKeyWithValue("override-label", "second"))
+
+	// Test successful daemonset patch
+	nProxyCfg = &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			DaemonSet: &ngfAPIv1alpha2.DaemonSetSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"ds-patched":"true"}}}`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err = provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(objects).To(HaveLen(6))
+
+	// Find and validate daemonset
+	var ds *appsv1.DaemonSet
+	for _, obj := range objects {
+		if d, ok := obj.(*appsv1.DaemonSet); ok {
+			ds = d
+			break
+		}
+	}
+	g.Expect(ds).ToNot(BeNil())
+	g.Expect(ds.Labels).To(HaveKeyWithValue("ds-patched", "true"))
+
+	// Test error cases - invalid patches should return objects and errors
+	nProxyCfg = &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			Service: &ngfAPIv1alpha2.ServiceSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"invalid json":`),
+						},
+					},
+				},
+			},
+			Deployment: &ngfAPIv1alpha2.DeploymentSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeJSONPatch),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`[{"op": "invalid", "path": "/test"}]`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err = provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).To(HaveOccurred())
+	g.Expect(err.Error()).To(ContainSubstring("failed to apply service patches"))
+	g.Expect(err.Error()).To(ContainSubstring("failed to apply deployment patches"))
+	g.Expect(objects).To(HaveLen(6)) // Objects should still be returned
+
+	// Test unsupported patch type
+	nProxyCfg = &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			Service: &ngfAPIv1alpha2.ServiceSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchType("unsupported")),
+						Value: &apiextv1.JSON{
+							Raw: []byte(`{"metadata":{"labels":{"test":"true"}}}`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err = provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).To(HaveOccurred())
+	g.Expect(err.Error()).To(ContainSubstring("unsupported patch type"))
+	g.Expect(objects).To(HaveLen(6))
+
+	// Test edge cases - nil values and empty patches should be ignored
+	nProxyCfg = &graph.EffectiveNginxProxy{
+		Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{
+			Service: &ngfAPIv1alpha2.ServiceSpec{
+				Patches: []ngfAPIv1alpha2.Patch{
+					{
+						Type:  helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: nil, // Should be ignored
+					},
+					{
+						Type: helpers.GetPointer(ngfAPIv1alpha2.PatchTypeStrategicMerge),
+						Value: &apiextv1.JSON{
+							Raw: []byte(""), // Should be ignored
+						},
+					},
+				},
+			},
+		},
+	}
+
+	objects, err = provisioner.buildNginxResourceObjects("gw-nginx", gateway, nProxyCfg)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(objects).To(HaveLen(6))
+
+	// Find service and verify no patches were applied
+	for _, obj := range objects {
+		if s, ok := obj.(*corev1.Service); ok {
+			svc = s
+			break
+		}
+	}
+	g.Expect(svc).ToNot(BeNil())
+	g.Expect(svc.Labels).ToNot(HaveKey("patched")) // Should not have patch-related labels
+}