Repository: Kong/kubernetes-ingress-controller
Pull request #5188 (2023-11-16 19:48:28)

fix: Ingress with default backend targeting the same Service
<!-- Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, read our contributor guidelines https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#the-pull-request-submit-process and developer guide https://github.com/kubernetes/community/blob/master/contributors/devel/development.md
2. If you want *faster* PR reviews, read how: https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
3. Follow the instructions for writing a release note: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md and ensure your changes are being reflected in CHANGELOG.md for the next upcoming release
-->

**What this PR does / why we need it**:

It covers Ingress's default backend with integration test and fixes behavior when the same Service is used in a routing rule and as the default backend. Although rather such config doesn't have much practical application, when someone configures something like that it should work as expected.  

<!-- Please describe why this particular PR is necessary or why you see it as a nice addition -->

**Which issue this PR fixes**:

Spotted during work on https://github.com/Kong/kubernetes-ingress-controller/pull/5171

<!--
Here you can add any links to issues that this PR is relevant for.
You can use Github keywords (like: closes, fixes or resolves) to auto-resolve
the linked issue(s) when this PR gets merged.

For example: fixes #<issue number>
-->

**Special notes for your reviewer**:

For a default backend `konghq.com/rewrite` annotation does not have meaning, because it works on the level of capture groups specified in the URL path used in an ingress rule. [See the docs](https://docs.konghq.com/kubernetes-ingress-controller/latest/reference/annotations/#konghqcomrewrite).

<!-- Here you can add any open questions or notes that you might have for reviewers -->

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [x] the `CHANGELOG.md` release notes have been updated to reflect any significant (and particularly user-facing) changes introduced by this PR


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5188?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`2bcd01a`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/2bcd01a6a69dfeaff9d23d8445cd5d6c28329626?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) 77.9% compared to head [(`ef91412`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5188?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) 77.9%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##            main   #5188     +/-   ##
=======================================
- Coverage   77.9%   77.9%   -0.1%     
=======================================
  Files        168     168             
  Lines      18899   18903      +4     
=======================================
- Hits       14731   14727      -4     
- Misses      3339    3346      +7     
- Partials     829     830      +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5188?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index caffe3874..82472af2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,11 +79,25 @@ Adding a new version? You'll need three changes:
 
 ## Unreleased
 
+### Added
+
+- Added `configPatches` field to KongPlugin and KongClusterPlugin to 
+  support populating configuration fields from Secret values. An item in
+  `configPatches` defines a JSON patch to add a field on the path in its `path`
+  and value from the value in the secret given in `valueFrom`. The JSON patches
+  are applied to the raw JSON in `config`. Can only be specified when
+  `configFrom` is not present.
+  [#5158](https://github.com/Kong/kubernetes-ingress-controller/pull/5158)
+
 ### Fixed
 
 - Using an Ingress with annotation `konghq.com/rewrite` and another Ingress without it pointing to the same Service,
   will no longer cause synchronization loop and random request failures due to incorrect routing.
   [#5171](https://github.com/Kong/kubernetes-ingress-controller/pull/5171)
+- Using the same Service in one Ingress as a target for ingress rule and default backend works without issues.
+  [#5188](https://github.com/Kong/kubernetes-ingress-controller/pull/5188)
+
+
 
 ### Changed
 
diff --git a/config/crd/bases/configuration.konghq.com_kongclusterplugins.yaml b/config/crd/bases/configuration.konghq.com_kongclusterplugins.yaml
index 613253b19..d48cb59d9 100644
--- a/config/crd/bases/configuration.konghq.com_kongclusterplugins.yaml
+++ b/config/crd/bases/configuration.konghq.com_kongclusterplugins.yaml
@@ -87,6 +87,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -249,9 +292,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/config/crd/bases/configuration.konghq.com_kongplugins.yaml b/config/crd/bases/configuration.konghq.com_kongplugins.yaml
index d714ed4b7..112d13cbd 100644
--- a/config/crd/bases/configuration.konghq.com_kongplugins.yaml
+++ b/config/crd/bases/configuration.konghq.com_kongplugins.yaml
@@ -83,6 +83,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -244,9 +285,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 168075eaf..6defb2b58 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -32,6 +32,7 @@ KongClusterPlugin is the Schema for the kongclusterplugins API.
 | `disabled` _boolean_ | Disabled set if the plugin is disabled or not. |
 | `config` _[JSON](#json)_ | Config contains the plugin configuration. It's a list of keys and values required to configure the plugin. Please read the documentation of the plugin being configured to set values in here. For any plugin in Kong, anything that goes in the `config` JSON key in the Admin API request, goes into this property. Only one of `config` or `configFrom` may be used in a KongClusterPlugin, not both at once. |
 | `configFrom` _[NamespacedConfigSource](#namespacedconfigsource)_ | ConfigFrom references a secret containing the plugin configuration. This should be used when the plugin configuration contains sensitive information, such as AWS credentials in the Lambda plugin or the client secret in the OIDC plugin. Only one of `config` or `configFrom` may be used in a KongClusterPlugin, not both at once. |
+| `configPatches` _[NamespacedConfigPatch](#namespacedconfigpatch) array_ | ConfigPatches represents JSON patches to the configuration of the plugin. Each item means a JSON patch to add something in the configuration, where path is specified in `path` and value is in `valueFrom` referencing a key in a secret. Could only be specified when Config specified. |
 | `plugin` _string_ | PluginName is the name of the plugin to which to apply the config. |
 | `run_on` _string_ | RunOn configures the plugin to run on the first or the second or both nodes in case of a service mesh deployment. |
 | `protocols` _[KongProtocol](#kongprotocol) array_ | Protocols configures plugin to run on requests received on specific protocols. |
@@ -99,6 +100,7 @@ KongPlugin is the Schema for the kongplugins API.
 | `disabled` _boolean_ | Disabled set if the plugin is disabled or not. |
 | `config` _[JSON](#json)_ | Config contains the plugin configuration. It's a list of keys and values required to configure the plugin. Please read the documentation of the plugin being configured to set values in here. For any plugin in Kong, anything that goes in the `config` JSON key in the Admin API request, goes into this property. Only one of `config` or `configFrom` may be used in a KongPlugin, not both at once. |
 | `configFrom` _[ConfigSource](#configsource)_ | ConfigFrom references a secret containing the plugin configuration. This should be used when the plugin configuration contains sensitive information, such as AWS credentials in the Lambda plugin or the client secret in the OIDC plugin. Only one of `config` or `configFrom` may be used in a KongPlugin, not both at once. |
+| `configPatches` _[ConfigPatch](#configpatch) array_ | ConfigPatches represents JSON patches to the configuration of the plugin. Each item means a JSON patch to add something in the configuration, where path is specified in `path` and value is in `valueFrom` referencing a key in a secret. Could only be specified when Config specified. |
 | `plugin` _string_ | PluginName is the name of the plugin to which to apply the config. |
 | `run_on` _string_ | RunOn configures the plugin to run on the first or the second or both nodes in case of a service mesh deployment. |
 | `protocols` _[KongProtocol](#kongprotocol) array_ | Protocols configures plugin to run on requests received on specific protocols. |
@@ -112,6 +114,23 @@ KongPlugin is the Schema for the kongplugins API.
 
 
 
+### ConfigPatch
+
+
+
+ConfigPatch is a JSON patch (RFC6902) to add values from Secret to the generated configuration. It is an equivalent of the following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.
+
+
+
+| Field | Description |
+| --- | --- |
+| `path` _string_ | Path is the JSON-Pointer value (RFC6901) that references a location within the target configuration. |
+| `valueFrom` _[ConfigSource](#configsource)_ | ValueFrom is the reference to a key of a secret where the patched value comes from. |
+
+
+_Appears in:_
+- [KongPlugin](#kongplugin)
+
 ### ConfigSource
 
 
@@ -126,6 +145,7 @@ ConfigSource is a wrapper around SecretValueFromSource.
 
 
 _Appears in:_
+- [ConfigPatch](#configpatch)
 - [KongPlugin](#kongplugin)
 
 
@@ -225,6 +245,23 @@ _Appears in:_
 - [KongIngressRoute](#kongingressroute)
 - [KongPlugin](#kongplugin)
 
+### NamespacedConfigPatch
+
+
+
+NamespacedConfigPatch is a JSON patch to add values from secrets to KongClusterPlugin to the generated configuration of plugin in Kong.
+
+
+
+| Field | Description |
+| --- | --- |
+| `path` _string_ | Path is the JSON path to add the patch. |
+| `valueFrom` _[NamespacedConfigSource](#namespacedconfigsource)_ | ValueFrom is the reference to a key of a secret where the patched value comes from. |
+
+
+_Appears in:_
+- [KongClusterPlugin](#kongclusterplugin)
+
 ### NamespacedConfigSource
 
 
@@ -240,6 +277,7 @@ NamespacedConfigSource is a wrapper around NamespacedSecretValueFromSource.
 
 _Appears in:_
 - [KongClusterPlugin](#kongclusterplugin)
+- [NamespacedConfigPatch](#namespacedconfigpatch)
 
 ### NamespacedSecretValueFromSource
 
diff --git a/go.mod b/go.mod
index e29173174..4f415cffc 100644
--- a/go.mod
+++ b/go.mod
@@ -117,7 +117,7 @@ require (
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/evanphx/json-patch v5.7.0+incompatible // indirect
-	github.com/evanphx/json-patch/v5 v5.7.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.7.0
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
 	github.com/fatih/camelcase v1.0.0 // indirect
 	github.com/fatih/color v1.15.0 // indirect
diff --git a/internal/admission/validator.go b/internal/admission/validator.go
index 5a8d57d4e..cb93e0d36 100644
--- a/internal/admission/validator.go
+++ b/internal/admission/validator.go
@@ -284,7 +284,13 @@ func (validator KongHTTPValidator) ValidatePlugin(
 	var plugin kong.Plugin
 	plugin.Name = kong.String(k8sPlugin.PluginName)
 	var err error
-	plugin.Config, err = kongstate.RawConfigToConfiguration(k8sPlugin.Config)
+
+	plugin.Config, err = kongstate.RawConfigurationWithPatchesToConfiguration(
+		validator.SecretGetter,
+		k8sPlugin.Namespace,
+		k8sPlugin.Config,
+		k8sPlugin.ConfigPatches,
+	)
 	if err != nil {
 		return false, ErrTextPluginConfigInvalid, err
 	}
@@ -306,6 +312,11 @@ func (validator KongHTTPValidator) ValidatePlugin(
 	}
 	errText, err := validator.validatePluginAgainstGatewaySchema(ctx, plugin)
 	if err != nil || errText != "" {
+		validator.Logger.Info("validate KongPlugin on Kong gateway failed",
+			"plugin", fmt.Sprintf("%s/%s", k8sPlugin.Namespace, k8sPlugin.Name),
+			"config", plugin.Config,
+			"error", err,
+		)
 		return false, errText, err
 	}
 
diff --git a/internal/controllers/configuration/object_references.go b/internal/controllers/configuration/object_references.go
index 7565203d2..4ce5d3368 100644
--- a/internal/controllers/configuration/object_references.go
+++ b/internal/controllers/configuration/object_references.go
@@ -3,6 +3,7 @@ package configuration
 import (
 	"context"
 
+	"github.com/samber/lo"
 	corev1 "k8s.io/api/core/v1"
 	netv1 "k8s.io/api/networking/v1"
 	k8stypes "k8s.io/apimachinery/pkg/types"
@@ -81,7 +82,7 @@ func listNetV1IngressReferredSecrets(ingress *netv1.Ingress) []k8stypes.Namespac
 }
 
 func listKongPluginReferredSecrets(plugin *kongv1.KongPlugin) []k8stypes.NamespacedName {
-	referredSecretNames := make([]k8stypes.NamespacedName, 0, 1)
+	referredSecretNames := make([]k8stypes.NamespacedName, 0, len(plugin.ConfigPatches)+1)
 	if plugin.ConfigFrom != nil {
 		nsName := k8stypes.NamespacedName{
 			Namespace: plugin.Namespace,
@@ -89,7 +90,16 @@ func listKongPluginReferredSecrets(plugin *kongv1.KongPlugin) []k8stypes.Namespa
 		}
 		referredSecretNames = append(referredSecretNames, nsName)
 	}
-	return referredSecretNames
+
+	for _, patch := range plugin.ConfigPatches {
+		nsName := k8stypes.NamespacedName{
+			Namespace: plugin.Namespace,
+			Name:      patch.ValueFrom.SecretValue.Secret,
+		}
+		referredSecretNames = append(referredSecretNames, nsName)
+	}
+
+	return lo.Uniq(referredSecretNames)
 }
 
 func listKongClusterPluginReferredSecrets(plugin *kongv1.KongClusterPlugin) []k8stypes.NamespacedName {
@@ -101,7 +111,16 @@ func listKongClusterPluginReferredSecrets(plugin *kongv1.KongClusterPlugin) []k8
 		}
 		referredSecretNames = append(referredSecretNames, nsName)
 	}
-	return referredSecretNames
+
+	for _, patch := range plugin.ConfigPatches {
+		nsName := k8stypes.NamespacedName{
+			Namespace: plugin.Namespace,
+			Name:      patch.ValueFrom.SecretValue.Secret,
+		}
+		referredSecretNames = append(referredSecretNames, nsName)
+	}
+
+	return lo.Uniq(referredSecretNames)
 }
 
 func listKongConsumerReferredSecrets(consumer *kongv1.KongConsumer) []k8stypes.NamespacedName {
diff --git a/internal/dataplane/kongstate/plugin.go b/internal/dataplane/kongstate/plugin.go
new file mode 100644
index 000000000..c1579bdec
--- /dev/null
+++ b/internal/dataplane/kongstate/plugin.go
@@ -0,0 +1,351 @@
+package kongstate
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	jsonpatch "github.com/evanphx/json-patch/v5"
+	"github.com/kong/go-kong/kong"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/yaml"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+)
+
+// getKongPluginOrKongClusterPlugin fetches a KongPlugin or KongClusterPlugin (as fallback) from the store.
+// If both are not found, an error is returned.
+func getKongPluginOrKongClusterPlugin(s store.Storer, namespace, name string) (
+	*kongv1.KongPlugin,
+	*kongv1.KongClusterPlugin,
+	error,
+) {
+	plugin, pluginErr := s.GetKongPlugin(namespace, name)
+	if pluginErr != nil {
+		if !errors.As(pluginErr, &store.NotFoundError{}) {
+			return nil, nil, fmt.Errorf("failed fetching KongPlugin: %w", pluginErr)
+		}
+
+		// If KongPlugin is not found, try to fetch KongClusterPlugin.
+		clusterPlugin, err := s.GetKongClusterPlugin(name)
+		if err != nil {
+			if !errors.As(err, &store.NotFoundError{}) {
+				return nil, nil, fmt.Errorf("failed fetching KongClusterPlugin: %w", err)
+			}
+
+			// Both KongPlugin and KongClusterPlugin are not found.
+			return nil, nil, fmt.Errorf("no KongPlugin or KongClusterPlugin was found for %s/%s", namespace, name)
+		}
+
+		return nil, clusterPlugin, nil
+	}
+
+	return plugin, nil, nil
+}
+
+func kongPluginFromK8SClusterPlugin(
+	s store.Storer,
+	k8sPlugin kongv1.KongClusterPlugin,
+) (Plugin, error) {
+	var config kong.Configuration
+	config, err := RawConfigurationWithNamespacedPatchesToConfiguration(
+		s,
+		k8sPlugin.Config,
+		k8sPlugin.ConfigPatches,
+	)
+	if err != nil {
+		return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w",
+			k8sPlugin.Namespace, k8sPlugin.Name, err)
+	}
+	if k8sPlugin.ConfigFrom != nil && len(config) > 0 {
+		return Plugin{},
+			fmt.Errorf("KongClusterPlugin '/%v' has both "+
+				"Config and ConfigFrom set", k8sPlugin.Name)
+	}
+	if k8sPlugin.ConfigFrom != nil {
+		var err error
+		config, err = namespacedSecretToConfiguration(
+			s,
+			(*k8sPlugin.ConfigFrom).SecretValue)
+		if err != nil {
+			return Plugin{},
+				fmt.Errorf("error parsing config for KongClusterPlugin %s: %w",
+					k8sPlugin.Name, err)
+		}
+	}
+
+	return Plugin{
+		Plugin: plugin{
+			Name:   k8sPlugin.PluginName,
+			Config: config,
+
+			RunOn:        k8sPlugin.RunOn,
+			Ordering:     k8sPlugin.Ordering,
+			InstanceName: k8sPlugin.InstanceName,
+			Disabled:     k8sPlugin.Disabled,
+			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
+			Tags:         util.GenerateTagsForObject(&k8sPlugin),
+		}.toKongPlugin(),
+		K8sParent: &k8sPlugin,
+	}, nil
+}
+
+func protocolsToStrings(protocols []kongv1.KongProtocol) (res []string) {
+	for _, protocol := range protocols {
+		res = append(res, string(protocol))
+	}
+	return
+}
+
+func kongPluginFromK8SPlugin(
+	s store.Storer,
+	k8sPlugin kongv1.KongPlugin,
+) (Plugin, error) {
+	var config kong.Configuration
+	config, err := RawConfigurationWithPatchesToConfiguration(
+		s,
+		k8sPlugin.Namespace,
+		k8sPlugin.Config,
+		k8sPlugin.ConfigPatches,
+	)
+	if err != nil {
+		return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w",
+			k8sPlugin.Namespace, k8sPlugin.Name, err)
+	}
+	if k8sPlugin.ConfigFrom != nil && len(config) > 0 {
+		return Plugin{},
+			fmt.Errorf("KongPlugin '%s/%s' has both Config and ConfigFrom set",
+				k8sPlugin.Namespace, k8sPlugin.Name)
+	}
+	if k8sPlugin.ConfigFrom != nil {
+		var err error
+		config, err = SecretToConfiguration(s,
+			(*k8sPlugin.ConfigFrom).SecretValue, k8sPlugin.Namespace)
+		if err != nil {
+			return Plugin{},
+				fmt.Errorf("error parsing config for KongPlugin '%s/%s': %w",
+					k8sPlugin.Name, k8sPlugin.Namespace, err)
+		}
+	}
+
+	return Plugin{
+		Plugin: plugin{
+			Name:   k8sPlugin.PluginName,
+			Config: config,
+
+			RunOn:        k8sPlugin.RunOn,
+			Ordering:     k8sPlugin.Ordering,
+			InstanceName: k8sPlugin.InstanceName,
+			Disabled:     k8sPlugin.Disabled,
+			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
+			Tags:         util.GenerateTagsForObject(&k8sPlugin),
+		}.toKongPlugin(),
+		K8sParent: &k8sPlugin,
+	}, nil
+}
+
+var rawPatchPattern = `[{"op":"%s","path":"%s","value":%s}]`
+
+type JSONPatchOp string
+
+var (
+	JSONPatchOpAdd     JSONPatchOp = "add"
+	JSONPatchOpReplace JSONPatchOp = "replace"
+)
+
+func applyJSONPatchFromNamespacedSecretRef(s SecretGetter, raw []byte, path string, namespace string, secretName string, key string) ([]byte, error) {
+	secret, err := s.GetSecret(namespace, secretName)
+	if err != nil {
+		return nil, err
+	}
+	secretVal, ok := secret.Data[key]
+	if !ok {
+		return nil,
+			fmt.Errorf("no key '%v' in secret '%v/%v'",
+				key, namespace, secretName)
+	}
+
+	// JSON patch (RFC6902) specifies the behavior of applying "add" on root,
+	// but because the jsonpatch package could not do "add" on root path (path=""),
+	// we have to use "replace" op on root to set the entire content of document if patch is on root path.
+	// https://github.com/evanphx/json-patch/issues/188
+	op := JSONPatchOpAdd
+	if path == "" {
+		op = JSONPatchOpReplace
+	}
+
+	rawPatch := fmt.Sprintf(rawPatchPattern, op, path, string(secretVal))
+	p, err := jsonpatch.DecodePatch([]byte(rawPatch))
+	if err != nil {
+		return nil, err
+	}
+
+	// Set EnsurePathExistsOnAdd to true for adding to subpaths to a non-existing path, e.g:
+	// Apply {"op":"add","path":"/add/headers","value":[{"h1":"v1"},{"h2":"v2"}]} on `{}`.
+	opts := jsonpatch.NewApplyOptions()
+	opts.EnsurePathExistsOnAdd = true
+	raw, err = p.ApplyWithOptions(raw, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return raw, nil
+}
+
+// RawConfigurationWithPatchesToConfiguration converts config and add patches from configPatches of KongPlugin.
+func RawConfigurationWithPatchesToConfiguration(
+	s SecretGetter, namespace string,
+	rawConfig apiextensionsv1.JSON,
+	patches []kongv1.ConfigPatch,
+) (kong.Configuration, error) {
+	raw := rawConfig.Raw
+	if raw == nil {
+		// In case the config is empty, we need to initialize it to an empty
+		// JSON object so that the patches can be applied.
+		raw = []byte("{}")
+	}
+
+	// apply patches
+	for _, patch := range patches {
+		var err error
+		raw, err = applyJSONPatchFromNamespacedSecretRef(
+			s,
+			raw,
+			patch.Path,
+			namespace,
+			patch.ValueFrom.SecretValue.Secret,
+			patch.ValueFrom.SecretValue.Key,
+		)
+		if err != nil {
+			return kong.Configuration{}, err
+		}
+	}
+	return rawConfigToConfiguration(raw)
+}
+
+// RawConfigurationWithNamespacedPatchesToConfiguration converts config and add patches from configPatches of KongClusterPlugin.
+func RawConfigurationWithNamespacedPatchesToConfiguration(
+	s SecretGetter,
+	rawConfig apiextensionsv1.JSON,
+	patches []kongv1.NamespacedConfigPatch,
+) (kong.Configuration, error) {
+	raw := rawConfig.Raw
+
+	if raw == nil {
+		// In case the config is empty, we need to initialize it to an empty
+		// JSON object so that the patches can be applied.
+		raw = []byte("{}")
+	}
+	for _, patch := range patches {
+		var err error
+		raw, err = applyJSONPatchFromNamespacedSecretRef(
+			s,
+			raw,
+			patch.Path,
+			patch.ValueFrom.SecretValue.Namespace,
+			patch.ValueFrom.SecretValue.Secret,
+			patch.ValueFrom.SecretValue.Key,
+		)
+		if err != nil {
+			return kong.Configuration{}, err
+		}
+	}
+	return rawConfigToConfiguration(raw)
+}
+
+// rawConfigToConfiguration decodes raw JSON to the format of Kong configuration.
+// it is run after all patches applied to the initial config.
+func rawConfigToConfiguration(raw []byte) (kong.Configuration, error) {
+	if len(raw) == 0 {
+		return kong.Configuration{}, nil
+	}
+	var kongConfig kong.Configuration
+	err := json.Unmarshal(raw, &kongConfig)
+	if err != nil {
+		return kong.Configuration{}, err
+	}
+	return kongConfig, nil
+}
+
+func namespacedSecretToConfiguration(
+	s store.Storer,
+	reference kongv1.NamespacedSecretValueFromSource) (
+	kong.Configuration, error,
+) {
+	bareReference := kongv1.SecretValueFromSource{
+		Secret: reference.Secret,
+		Key:    reference.Key,
+	}
+	return SecretToConfiguration(s, bareReference, reference.Namespace)
+}
+
+type SecretGetter interface {
+	GetSecret(namespace, name string) (*corev1.Secret, error)
+}
+
+func SecretToConfiguration(
+	s SecretGetter,
+	reference kongv1.SecretValueFromSource, namespace string) (
+	kong.Configuration, error,
+) {
+	secret, err := s.GetSecret(namespace, reference.Secret)
+	if err != nil {
+		return kong.Configuration{}, fmt.Errorf(
+			"error fetching plugin configuration secret '%v/%v': %w",
+			namespace, reference.Secret, err)
+	}
+	secretVal, ok := secret.Data[reference.Key]
+	if !ok {
+		return kong.Configuration{},
+			fmt.Errorf("no key '%v' in secret '%v/%v'",
+				reference.Key, namespace, reference.Secret)
+	}
+	var config kong.Configuration
+	if err := json.Unmarshal(secretVal, &config); err != nil {
+		if err := yaml.Unmarshal(secretVal, &config); err != nil {
+			return kong.Configuration{},
+				fmt.Errorf("key '%v' in secret '%v/%v' contains neither "+
+					"valid JSON nor valid YAML)",
+					reference.Key, namespace, reference.Secret)
+		}
+	}
+	return config, nil
+}
+
+// plugin is a intermediate type to hold plugin related configuration.
+type plugin struct {
+	Name   string
+	Config kong.Configuration
+
+	RunOn        string
+	Ordering     *kong.PluginOrdering
+	InstanceName string
+	Disabled     bool
+	Protocols    []string
+	Tags         []*string
+}
+
+func (p plugin) toKongPlugin() kong.Plugin {
+	result := kong.Plugin{
+		Name:   kong.String(p.Name),
+		Config: p.Config.DeepCopy(),
+		Tags:   p.Tags,
+	}
+	if p.RunOn != "" {
+		result.RunOn = kong.String(p.RunOn)
+	}
+	if p.Disabled {
+		result.Enabled = kong.Bool(false)
+	}
+	result.Ordering = p.Ordering
+	if len(p.Protocols) > 0 {
+		result.Protocols = kong.StringSlice(p.Protocols...)
+	}
+	if p.InstanceName != "" {
+		result.InstanceName = kong.String(p.InstanceName)
+	}
+	return result
+}
diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go
index a60762c01..471fde2e6 100644
--- a/internal/dataplane/kongstate/util.go
+++ b/internal/dataplane/kongstate/util.go
@@ -1,15 +1,10 @@
 package kongstate
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 	"strings"
 
-	"github.com/kong/go-kong/kong"
 	corev1 "k8s.io/api/core/v1"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	"sigs.k8s.io/yaml"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
@@ -79,185 +74,6 @@ func getKongIngressFromObjAnnotations(
 	return nil, nil
 }
 
-// getKongPluginOrKongClusterPlugin fetches a KongPlugin or KongClusterPlugin (as fallback) from the store.
-// If both are not found, an error is returned.
-func getKongPluginOrKongClusterPlugin(s store.Storer, namespace, name string) (
-	*kongv1.KongPlugin,
-	*kongv1.KongClusterPlugin,
-	error,
-) {
-	plugin, pluginErr := s.GetKongPlugin(namespace, name)
-	if pluginErr != nil {
-		if !errors.As(pluginErr, &store.NotFoundError{}) {
-			return nil, nil, fmt.Errorf("failed fetching KongPlugin: %w", pluginErr)
-		}
-
-		// If KongPlugin is not found, try to fetch KongClusterPlugin.
-		clusterPlugin, err := s.GetKongClusterPlugin(name)
-		if err != nil {
-			if !errors.As(err, &store.NotFoundError{}) {
-				return nil, nil, fmt.Errorf("failed fetching KongClusterPlugin: %w", err)
-			}
-
-			// Both KongPlugin and KongClusterPlugin are not found.
-			return nil, nil, fmt.Errorf("no KongPlugin or KongClusterPlugin was found for %s/%s", namespace, name)
-		}
-
-		return nil, clusterPlugin, nil
-	}
-
-	return plugin, nil, nil
-}
-
-func kongPluginFromK8SClusterPlugin(
-	s store.Storer,
-	k8sPlugin kongv1.KongClusterPlugin,
-) (Plugin, error) {
-	var config kong.Configuration
-	config, err := RawConfigToConfiguration(k8sPlugin.Config)
-	if err != nil {
-		return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w",
-			k8sPlugin.Namespace, k8sPlugin.Name, err)
-	}
-	if k8sPlugin.ConfigFrom != nil && len(config) > 0 {
-		return Plugin{},
-			fmt.Errorf("KongClusterPlugin '/%v' has both "+
-				"Config and ConfigFrom set", k8sPlugin.Name)
-	}
-	if k8sPlugin.ConfigFrom != nil {
-		var err error
-		config, err = namespacedSecretToConfiguration(
-			s,
-			(*k8sPlugin.ConfigFrom).SecretValue)
-		if err != nil {
-			return Plugin{},
-				fmt.Errorf("error parsing config for KongClusterPlugin %s: %w",
-					k8sPlugin.Name, err)
-		}
-	}
-
-	return Plugin{
-		Plugin: plugin{
-			Name:   k8sPlugin.PluginName,
-			Config: config,
-
-			RunOn:        k8sPlugin.RunOn,
-			Ordering:     k8sPlugin.Ordering,
-			InstanceName: k8sPlugin.InstanceName,
-			Disabled:     k8sPlugin.Disabled,
-			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
-			Tags:         util.GenerateTagsForObject(&k8sPlugin),
-		}.toKongPlugin(),
-		K8sParent: &k8sPlugin,
-	}, nil
-}
-
-func protocolsToStrings(protocols []kongv1.KongProtocol) (res []string) {
-	for _, protocol := range protocols {
-		res = append(res, string(protocol))
-	}
-	return
-}
-
-func kongPluginFromK8SPlugin(
-	s store.Storer,
-	k8sPlugin kongv1.KongPlugin,
-) (Plugin, error) {
-	var config kong.Configuration
-	config, err := RawConfigToConfiguration(k8sPlugin.Config)
-	if err != nil {
-		return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w",
-			k8sPlugin.Namespace, k8sPlugin.Name, err)
-	}
-	if k8sPlugin.ConfigFrom != nil && len(config) > 0 {
-		return Plugin{},
-			fmt.Errorf("KongPlugin '%s/%s' has both Config and ConfigFrom set",
-				k8sPlugin.Namespace, k8sPlugin.Name)
-	}
-	if k8sPlugin.ConfigFrom != nil {
-		var err error
-		config, err = SecretToConfiguration(s,
-			(*k8sPlugin.ConfigFrom).SecretValue, k8sPlugin.Namespace)
-		if err != nil {
-			return Plugin{},
-				fmt.Errorf("error parsing config for KongPlugin '%s/%s': %w",
-					k8sPlugin.Name, k8sPlugin.Namespace, err)
-		}
-	}
-
-	return Plugin{
-		Plugin: plugin{
-			Name:   k8sPlugin.PluginName,
-			Config: config,
-
-			RunOn:        k8sPlugin.RunOn,
-			Ordering:     k8sPlugin.Ordering,
-			InstanceName: k8sPlugin.InstanceName,
-			Disabled:     k8sPlugin.Disabled,
-			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
-			Tags:         util.GenerateTagsForObject(&k8sPlugin),
-		}.toKongPlugin(),
-		K8sParent: &k8sPlugin,
-	}, nil
-}
-
-func RawConfigToConfiguration(config apiextensionsv1.JSON) (kong.Configuration, error) {
-	if len(config.Raw) == 0 {
-		return kong.Configuration{}, nil
-	}
-	var kongConfig kong.Configuration
-	err := json.Unmarshal(config.Raw, &kongConfig)
-	if err != nil {
-		return kong.Configuration{}, err
-	}
-	return kongConfig, nil
-}
-
-func namespacedSecretToConfiguration(
-	s store.Storer,
-	reference kongv1.NamespacedSecretValueFromSource) (
-	kong.Configuration, error,
-) {
-	bareReference := kongv1.SecretValueFromSource{
-		Secret: reference.Secret,
-		Key:    reference.Key,
-	}
-	return SecretToConfiguration(s, bareReference, reference.Namespace)
-}
-
-type SecretGetter interface {
-	GetSecret(namespace, name string) (*corev1.Secret, error)
-}
-
-func SecretToConfiguration(
-	s SecretGetter,
-	reference kongv1.SecretValueFromSource, namespace string) (
-	kong.Configuration, error,
-) {
-	secret, err := s.GetSecret(namespace, reference.Secret)
-	if err != nil {
-		return kong.Configuration{}, fmt.Errorf(
-			"error fetching plugin configuration secret '%v/%v': %w",
-			namespace, reference.Secret, err)
-	}
-	secretVal, ok := secret.Data[reference.Key]
-	if !ok {
-		return kong.Configuration{},
-			fmt.Errorf("no key '%v' in secret '%v/%v'",
-				reference.Key, namespace, reference.Secret)
-	}
-	var config kong.Configuration
-	if err := json.Unmarshal(secretVal, &config); err != nil {
-		if err := yaml.Unmarshal(secretVal, &config); err != nil {
-			return kong.Configuration{},
-				fmt.Errorf("key '%v' in secret '%v/%v' contains neither "+
-					"valid JSON nor valid YAML)",
-					reference.Key, namespace, reference.Secret)
-		}
-	}
-	return config, nil
-}
-
 // prettyPrintServiceList makes a clean printable list of Kubernetes
 // services for the purpose of logging (errors, info, etc.).
 func prettyPrintServiceList(services []*corev1.Service) string {
@@ -267,38 +83,3 @@ func prettyPrintServiceList(services []*corev1.Service) string {
 	}
 	return strings.Join(serviceList, ", ")
 }
-
-// plugin is a intermediate type to hold plugin related configuration.
-type plugin struct {
-	Name   string
-	Config kong.Configuration
-
-	RunOn        string
-	Ordering     *kong.PluginOrdering
-	InstanceName string
-	Disabled     bool
-	Protocols    []string
-	Tags         []*string
-}
-
-func (p plugin) toKongPlugin() kong.Plugin {
-	result := kong.Plugin{
-		Name:   kong.String(p.Name),
-		Config: p.Config.DeepCopy(),
-		Tags:   p.Tags,
-	}
-	if p.RunOn != "" {
-		result.RunOn = kong.String(p.RunOn)
-	}
-	if p.Disabled {
-		result.Enabled = kong.Bool(false)
-	}
-	result.Ordering = p.Ordering
-	if len(p.Protocols) > 0 {
-		result.Protocols = kong.StringSlice(p.Protocols...)
-	}
-	if p.InstanceName != "" {
-		result.InstanceName = kong.String(p.InstanceName)
-	}
-	return result
-}
diff --git a/internal/dataplane/translator/translate_ingress.go b/internal/dataplane/translator/translate_ingress.go
index a363a9890..f855917e8 100644
--- a/internal/dataplane/translator/translate_ingress.go
+++ b/internal/dataplane/translator/translate_ingress.go
@@ -64,6 +64,11 @@ func (t *Translator) ingressRulesFromIngressV1() ingressRules {
 	// Add a default backend if it exists.
 	defaultBackendService, ok := getDefaultBackendService(allDefaultBackends, t.featureFlags.ExpressionRoutes)
 	if ok {
+		// When such service would overwrite an existing service, merge the routes.
+		if svc, ok := result.ServiceNameToServices[*defaultBackendService.Name]; ok {
+			svc.Routes = append(svc.Routes, defaultBackendService.Routes...)
+			defaultBackendService = svc
+		}
 		result.ServiceNameToServices[*defaultBackendService.Name] = defaultBackendService
 		result.ServiceNameToParent[*defaultBackendService.Name] = defaultBackendService.Parent
 	}
diff --git a/pkg/apis/configuration/v1/configsource.go b/pkg/apis/configuration/v1/configsource.go
index d4fc0efe6..c6ef4b945 100644
--- a/pkg/apis/configuration/v1/configsource.go
+++ b/pkg/apis/configuration/v1/configsource.go
@@ -7,6 +7,17 @@ type ConfigSource struct {
 	SecretValue SecretValueFromSource `json:"secretKeyRef"`
 }
 
+// ConfigPatch is a JSON patch (RFC6902) to add values from Secret to the generated configuration.
+// It is an equivalent of the following patch:
+// `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.
+// +kubebuilder:object:generate=true
+type ConfigPatch struct {
+	// Path is the JSON-Pointer value (RFC6901) that references a location within the target configuration.
+	Path string `json:"path"`
+	// ValueFrom is the reference to a key of a secret where the patched value comes from.
+	ValueFrom ConfigSource `json:"valueFrom"`
+}
+
 // NamespacedConfigSource is a wrapper around NamespacedSecretValueFromSource.
 // +kubebuilder:object:generate=true
 type NamespacedConfigSource struct {
@@ -14,6 +25,16 @@ type NamespacedConfigSource struct {
 	SecretValue NamespacedSecretValueFromSource `json:"secretKeyRef"`
 }
 
+// NamespacedConfigPatch is a JSON patch to add values from secrets to KongClusterPlugin
+// to the generated configuration of plugin in Kong.
+// +kubebuilder:object:generate=true
+type NamespacedConfigPatch struct {
+	// Path is the JSON path to add the patch.
+	Path string `json:"path"`
+	// ValueFrom is the reference to a key of a secret where the patched value comes from.
+	ValueFrom NamespacedConfigSource `json:"valueFrom"`
+}
+
 // SecretValueFromSource represents the source of a secret value.
 // +kubebuilder:object:generate=true
 type SecretValueFromSource struct {
diff --git a/pkg/apis/configuration/v1/kongclusterplugin_types.go b/pkg/apis/configuration/v1/kongclusterplugin_types.go
index a53aff6ad..3a4a31dca 100644
--- a/pkg/apis/configuration/v1/kongclusterplugin_types.go
+++ b/pkg/apis/configuration/v1/kongclusterplugin_types.go
@@ -34,7 +34,8 @@ import (
 // +kubebuilder:printcolumn:name="Disabled",type=boolean,JSONPath=`.disabled`,description="Indicates if the plugin is disabled",priority=1
 // +kubebuilder:printcolumn:name="Config",type=string,JSONPath=`.config`,description="Configuration of the plugin",priority=1
 // +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=`.status.conditions[?(@.type=="Programmed")].status`
-// +kubebuilder:validation:XValidation:rule="(has(self.config) || has(self.configFrom)) ? ((!has(self.config) && has(self.configFrom)) || (has(self.config) && !has(self.configFrom))) : true", message="Using both config and configFrom fields is not allowed."
+// +kubebuilder:validation:XValidation:rule="!(has(self.config) && has(self.configFrom))", message="Using both config and configFrom fields is not allowed."
+// +kubebuilder:validation:XValidation:rule="!(has(self.configFrom) && has(self.configPatches))", message="Using both configFrom and configPatches fields is not allowed."
 // +kubebuilder:validation:XValidation:rule="self.plugin == oldSelf.plugin", message="The plugin field is immutable"
 
 // KongClusterPlugin is the Schema for the kongclusterplugins API.
@@ -63,6 +64,13 @@ type KongClusterPlugin struct {
 	// Only one of `config` or `configFrom` may be used in a KongClusterPlugin, not both at once.
 	ConfigFrom *NamespacedConfigSource `json:"configFrom,omitempty"`
 
+	// ConfigPatches represents JSON patches to the configuration of the plugin.
+	// Each item means a JSON patch to add something in the configuration,
+	// where path is specified in `path` and value is in `valueFrom` referencing
+	// a key in a secret.
+	// Could only be specified when Config specified.
+	ConfigPatches []NamespacedConfigPatch `json:"configPatches,omitempty"`
+
 	// PluginName is the name of the plugin to which to apply the config.
 	// +kubebuilder:validation:Required
 	PluginName string `json:"plugin"`
diff --git a/pkg/apis/configuration/v1/kongplugin_types.go b/pkg/apis/configuration/v1/kongplugin_types.go
index ac6b92d5b..0c2866e6c 100644
--- a/pkg/apis/configuration/v1/kongplugin_types.go
+++ b/pkg/apis/configuration/v1/kongplugin_types.go
@@ -33,7 +33,8 @@ import (
 // +kubebuilder:printcolumn:name="Disabled",type=boolean,JSONPath=`.disabled`,description="Indicates if the plugin is disabled",priority=1
 // +kubebuilder:printcolumn:name="Config",type=string,JSONPath=`.config`,description="Configuration of the plugin",priority=1
 // +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=`.status.conditions[?(@.type=="Programmed")].status`
-// +kubebuilder:validation:XValidation:rule="(has(self.config) || has(self.configFrom)) ? ((!has(self.config) && has(self.configFrom)) || (has(self.config) && !has(self.configFrom))) : true", message="Using both config and configFrom fields is not allowed."
+// +kubebuilder:validation:XValidation:rule="!(has(self.config) && has(self.configFrom))", message="Using both config and configFrom fields is not allowed."
+// +kubebuilder:validation:XValidation:rule="!(has(self.configFrom) && has(self.configPatches))", message="Using both configFrom and configPatches fields is not allowed."
 // +kubebuilder:validation:XValidation:rule="self.plugin == oldSelf.plugin", message="The plugin field is immutable"
 
 // KongPlugin is the Schema for the kongplugins API.
@@ -63,6 +64,13 @@ type KongPlugin struct {
 	// Only one of `config` or `configFrom` may be used in a KongPlugin, not both at once.
 	ConfigFrom *ConfigSource `json:"configFrom,omitempty"`
 
+	// ConfigPatches represents JSON patches to the configuration of the plugin.
+	// Each item means a JSON patch to add something in the configuration,
+	// where path is specified in `path` and value is in `valueFrom` referencing
+	// a key in a secret.
+	// Could only be specified when Config specified.
+	ConfigPatches []ConfigPatch `json:"configPatches,omitempty"`
+
 	// PluginName is the name of the plugin to which to apply the config.
 	// +kubebuilder:validation:Required
 	PluginName string `json:"plugin"`
diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go
index 7adaa1738..4b8767c16 100644
--- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go
@@ -26,6 +26,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConfigPatch) DeepCopyInto(out *ConfigPatch) {
+	*out = *in
+	out.ValueFrom = in.ValueFrom
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigPatch.
+func (in *ConfigPatch) DeepCopy() *ConfigPatch {
+	if in == nil {
+		return nil
+	}
+	out := new(ConfigPatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ConfigSource) DeepCopyInto(out *ConfigSource) {
 	*out = *in
@@ -53,6 +69,11 @@ func (in *KongClusterPlugin) DeepCopyInto(out *KongClusterPlugin) {
 		*out = new(NamespacedConfigSource)
 		**out = **in
 	}
+	if in.ConfigPatches != nil {
+		in, out := &in.ConfigPatches, &out.ConfigPatches
+		*out = make([]NamespacedConfigPatch, len(*in))
+		copy(*out, *in)
+	}
 	if in.Protocols != nil {
 		in, out := &in.Protocols, &out.Protocols
 		*out = make([]KongProtocol, len(*in))
@@ -540,6 +561,11 @@ func (in *KongPlugin) DeepCopyInto(out *KongPlugin) {
 		*out = new(ConfigSource)
 		**out = **in
 	}
+	if in.ConfigPatches != nil {
+		in, out := &in.ConfigPatches, &out.ConfigPatches
+		*out = make([]ConfigPatch, len(*in))
+		copy(*out, *in)
+	}
 	if in.Protocols != nil {
 		in, out := &in.Protocols, &out.Protocols
 		*out = make([]KongProtocol, len(*in))
@@ -625,6 +651,22 @@ func (in *KongPluginStatus) DeepCopy() *KongPluginStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedConfigPatch) DeepCopyInto(out *NamespacedConfigPatch) {
+	*out = *in
+	out.ValueFrom = in.ValueFrom
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedConfigPatch.
+func (in *NamespacedConfigPatch) DeepCopy() *NamespacedConfigPatch {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedConfigPatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NamespacedConfigSource) DeepCopyInto(out *NamespacedConfigSource) {
 	*out = *in


Test patch:
diff --git a/internal/dataplane/kongstate/plugin_test.go b/internal/dataplane/kongstate/plugin_test.go
new file mode 100644
index 000000000..fed7b3f1c
--- /dev/null
+++ b/internal/dataplane/kongstate/plugin_test.go
@@ -0,0 +1,766 @@
+package kongstate
+
+import (
+	"testing"
+
+	"github.com/kong/go-kong/kong"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+)
+
+func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
+	assert := assert.New(t)
+	store, _ := store.NewFakeStore(store.FakeObjects{
+		Secrets: []*corev1.Secret{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "conf-secret",
+					Namespace: "default",
+				},
+				Data: map[string][]byte{
+					"correlation-id-config":            []byte(`{"header_name": "foo"}`),
+					"correlation-id-headername":        []byte(`"foo"`),
+					"correlation-id-generator":         []byte(`"uuid"`),
+					"correlation-id-invalid":           []byte(`"aaa`),
+					"response-transformer-add-headers": []byte(`["h1:v1","h2:v2"]`),
+				},
+			},
+		},
+	})
+	type args struct {
+		plugin kongv1.KongClusterPlugin
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    kong.Plugin
+		wantErr bool
+	}{
+		{
+			name: "basic configuration",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:    []kongv1.KongProtocol{"http"},
+					PluginName:   "correlation-id",
+					InstanceName: "example",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols:    kong.StringSlice("http"),
+				InstanceName: kong.String("example"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "secret configuration",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "conf-secret",
+							Namespace: "default",
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "missing secret configuration",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "missing",
+							Namespace: "default",
+						},
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "non-JSON configuration",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{{}`),
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "both Config and ConfigFrom set",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "conf-secret",
+							Namespace: "default",
+						},
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "Config and ConfigPatches set",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Key:       "correlation-id-generator",
+									Secret:    "conf-secret",
+									Namespace: "default",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "configPatch on subpath of non-exist path",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "response-transformer",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/add/headers",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "response-transformer-add-headers",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("response-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]interface{}{
+						"headers": []interface{}{
+							"foo:bar",
+						},
+					},
+					"add": map[string]interface{}{
+						"headers": []interface{}{
+							"h1:v1",
+							"h2:v2",
+						},
+					},
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "empty config and configPatch for particular paths",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/header_name",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-headername",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-generator",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "empty config and configPatch for whole object",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-config",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "missing secret in configPatches",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-generator",
+									Secret:    "missing-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing key of secret in cofigPatches",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-missing",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid value in configPatches",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-invalid",
+									Secret:    "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := kongPluginFromK8SClusterPlugin(store, tt.args.plugin)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("kongPluginFromK8SClusterPlugin error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			assert.Equal(tt.want, got.Plugin)
+			assert.NotEmpty(t, got.K8sParent)
+		})
+	}
+}
+
+func TestKongPluginFromK8SPlugin(t *testing.T) {
+	assert := assert.New(t)
+	store, _ := store.NewFakeStore(store.FakeObjects{
+		Secrets: []*corev1.Secret{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "conf-secret",
+					Namespace: "default",
+				},
+				Data: map[string][]byte{
+					"correlation-id-config":            []byte(`{"header_name": "foo"}`),
+					"correlation-id-headername":        []byte(`"foo"`),
+					"correlation-id-generator":         []byte(`"uuid"`),
+					"correlation-id-invalid":           []byte(`"aaa`),
+					"response-transformer-add-headers": []byte(`["h1:v1","h2:v2"]`),
+				},
+			},
+		},
+	})
+	type args struct {
+		plugin kongv1.KongPlugin
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    kong.Plugin
+		wantErr bool
+	}{
+		{
+			name: "basic configuration",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:    []kongv1.KongProtocol{"http"},
+					PluginName:   "correlation-id",
+					InstanceName: "example",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols:    kong.StringSlice("http"),
+				InstanceName: kong.String("example"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "secret configuration",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foo",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "conf-secret",
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "missing secret configuration",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foo",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "missing",
+						},
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "non-JSON configuration",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{{}`),
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "both Config and ConfigFrom set",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "conf-secret",
+						},
+					},
+				},
+			},
+			want:    kong.Plugin{},
+			wantErr: true,
+		},
+		{
+			name: "config and configPatches set",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "configPatch on subpath of non-exist path",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "response-transformer",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/add/headers",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "response-transformer-add-headers",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("response-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]interface{}{
+						"headers": []interface{}{
+							"foo:bar",
+						},
+					},
+					"add": map[string]interface{}{
+						"headers": []interface{}{
+							"h1:v1",
+							"h2:v2",
+						},
+					},
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "empty config and configPatch for particular paths",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/header_name",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-headername",
+									Secret: "conf-secret",
+								},
+							},
+						},
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "empty config and configPatch for whole object",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-config",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+				},
+				Protocols: kong.StringSlice("http"),
+			},
+		},
+		{
+			name: "missing secret in configPatches",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "missing-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing key of secret in configPatches",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-missing",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid value in configPatches",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-invalid",
+									Secret: "conf-secret",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := kongPluginFromK8SPlugin(store, tt.args.plugin)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("kongPluginFromK8SPlugin error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			// don't care about tags in this test
+			got.Tags = nil
+			assert.Equal(tt.want, got.Plugin)
+			assert.NotEmpty(t, got.K8sParent)
+		})
+	}
+}
diff --git a/internal/dataplane/kongstate/util_test.go b/internal/dataplane/kongstate/util_test.go
index 49a483d9d..5c24252ad 100644
--- a/internal/dataplane/kongstate/util_test.go
+++ b/internal/dataplane/kongstate/util_test.go
@@ -4,11 +4,8 @@ import (
 	"regexp"
 	"testing"
 
-	"github.com/kong/go-kong/kong"
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -19,287 +16,6 @@ import (
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 )
 
-func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
-	assert := assert.New(t)
-	store, _ := store.NewFakeStore(store.FakeObjects{
-		Secrets: []*corev1.Secret{
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "conf-secret",
-					Namespace: "default",
-				},
-				Data: map[string][]byte{
-					"correlation-id-config": []byte(`{"header_name": "foo"}`),
-				},
-			},
-		},
-	})
-	type args struct {
-		plugin kongv1.KongClusterPlugin
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    kong.Plugin
-		wantErr bool
-	}{
-		{
-			name: "basic configuration",
-			args: args{
-				plugin: kongv1.KongClusterPlugin{
-					Protocols:    []kongv1.KongProtocol{"http"},
-					PluginName:   "correlation-id",
-					InstanceName: "example",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{"header_name": "foo"}`),
-					},
-				},
-			},
-			want: kong.Plugin{
-				Name: kong.String("correlation-id"),
-				Config: kong.Configuration{
-					"header_name": "foo",
-				},
-				Protocols:    kong.StringSlice("http"),
-				InstanceName: kong.String("example"),
-			},
-			wantErr: false,
-		},
-		{
-			name: "secret configuration",
-			args: args{
-				plugin: kongv1.KongClusterPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					ConfigFrom: &kongv1.NamespacedConfigSource{
-						SecretValue: kongv1.NamespacedSecretValueFromSource{
-							Key:       "correlation-id-config",
-							Secret:    "conf-secret",
-							Namespace: "default",
-						},
-					},
-				},
-			},
-			want: kong.Plugin{
-				Name: kong.String("correlation-id"),
-				Config: kong.Configuration{
-					"header_name": "foo",
-				},
-				Protocols: kong.StringSlice("http"),
-			},
-			wantErr: false,
-		},
-		{
-			name: "missing secret configuration",
-			args: args{
-				plugin: kongv1.KongClusterPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					ConfigFrom: &kongv1.NamespacedConfigSource{
-						SecretValue: kongv1.NamespacedSecretValueFromSource{
-							Key:       "correlation-id-config",
-							Secret:    "missing",
-							Namespace: "default",
-						},
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-		{
-			name: "non-JSON configuration",
-			args: args{
-				plugin: kongv1.KongClusterPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{{}`),
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-		{
-			name: "both Config and ConfigFrom set",
-			args: args{
-				plugin: kongv1.KongClusterPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{"header_name": "foo"}`),
-					},
-					ConfigFrom: &kongv1.NamespacedConfigSource{
-						SecretValue: kongv1.NamespacedSecretValueFromSource{
-							Key:       "correlation-id-config",
-							Secret:    "conf-secret",
-							Namespace: "default",
-						},
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := kongPluginFromK8SClusterPlugin(store, tt.args.plugin)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("kongPluginFromK8SClusterPlugin error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			assert.Equal(tt.want, got.Plugin)
-			assert.NotEmpty(t, got.K8sParent)
-		})
-	}
-}
-
-func TestKongPluginFromK8SPlugin(t *testing.T) {
-	assert := assert.New(t)
-	store, _ := store.NewFakeStore(store.FakeObjects{
-		Secrets: []*corev1.Secret{
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "conf-secret",
-					Namespace: "default",
-				},
-				Data: map[string][]byte{
-					"correlation-id-config": []byte(`{"header_name": "foo"}`),
-				},
-			},
-		},
-	})
-	type args struct {
-		plugin kongv1.KongPlugin
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    kong.Plugin
-		wantErr bool
-	}{
-		{
-			name: "basic configuration",
-			args: args{
-				plugin: kongv1.KongPlugin{
-					Protocols:    []kongv1.KongProtocol{"http"},
-					PluginName:   "correlation-id",
-					InstanceName: "example",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{"header_name": "foo"}`),
-					},
-				},
-			},
-			want: kong.Plugin{
-				Name: kong.String("correlation-id"),
-				Config: kong.Configuration{
-					"header_name": "foo",
-				},
-				Protocols:    kong.StringSlice("http"),
-				InstanceName: kong.String("example"),
-			},
-			wantErr: false,
-		},
-		{
-			name: "secret configuration",
-			args: args{
-				plugin: kongv1.KongPlugin{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "foo",
-						Namespace: "default",
-					},
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					ConfigFrom: &kongv1.ConfigSource{
-						SecretValue: kongv1.SecretValueFromSource{
-							Key:    "correlation-id-config",
-							Secret: "conf-secret",
-						},
-					},
-				},
-			},
-			want: kong.Plugin{
-				Name: kong.String("correlation-id"),
-				Config: kong.Configuration{
-					"header_name": "foo",
-				},
-				Protocols: kong.StringSlice("http"),
-			},
-			wantErr: false,
-		},
-		{
-			name: "missing secret configuration",
-			args: args{
-				plugin: kongv1.KongPlugin{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "foo",
-						Namespace: "default",
-					},
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					ConfigFrom: &kongv1.ConfigSource{
-						SecretValue: kongv1.SecretValueFromSource{
-							Key:    "correlation-id-config",
-							Secret: "missing",
-						},
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-		{
-			name: "non-JSON configuration",
-			args: args{
-				plugin: kongv1.KongPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{{}`),
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-		{
-			name: "both Config and ConfigFrom set",
-			args: args{
-				plugin: kongv1.KongPlugin{
-					Protocols:  []kongv1.KongProtocol{"http"},
-					PluginName: "correlation-id",
-					Config: apiextensionsv1.JSON{
-						Raw: []byte(`{"header_name": "foo"}`),
-					},
-					ConfigFrom: &kongv1.ConfigSource{
-						SecretValue: kongv1.SecretValueFromSource{
-							Key:    "correlation-id-config",
-							Secret: "conf-secret",
-						},
-					},
-				},
-			},
-			want:    kong.Plugin{},
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := kongPluginFromK8SPlugin(store, tt.args.plugin)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("kongPluginFromK8SPlugin error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			// don't care about tags in this test
-			got.Tags = nil
-			assert.Equal(tt.want, got.Plugin)
-			assert.NotEmpty(t, got.K8sParent)
-		})
-	}
-}
-
 func TestGetKongIngressForServices(t *testing.T) {
 	for _, tt := range []struct {
 		name                string
diff --git a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
index dad59cb6f..87d18d261 100644
--- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
index d401dfee2..a129497db 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
index 62716367f..b696af9d7 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml
index f5ce84671..3f452463b 100644
--- a/test/e2e/manifests/all-in-one-dbless.yaml
+++ b/test/e2e/manifests/all-in-one-dbless.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
index 8dc544612..b3deba6b2 100644
--- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
index 3c78fb43d..87b327561 100644
--- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml
index 8b1b7ff84..bef7a1316 100644
--- a/test/e2e/manifests/all-in-one-postgres.yaml
+++ b/test/e2e/manifests/all-in-one-postgres.yaml
@@ -145,6 +145,49 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: NamespacedConfigPatch is a JSON patch to add values from
+                secrets to KongClusterPlugin to the generated configuration of plugin
+                in Kong.
+              properties:
+                path:
+                  description: Path is the JSON path to add the patch.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name, a namespace, and a key of a secret
+                        to refer to.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                        namespace:
+                          description: The namespace containing the secret.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      - namespace
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -307,9 +350,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
@@ -1061,6 +1104,47 @@ spec:
             required:
             - secretKeyRef
             type: object
+          configPatches:
+            description: ConfigPatches represents JSON patches to the configuration
+              of the plugin. Each item means a JSON patch to add something in the
+              configuration, where path is specified in `path` and value is in `valueFrom`
+              referencing a key in a secret. Could only be specified when Config specified.
+            items:
+              description: 'ConfigPatch is a JSON patch (RFC6902) to add values from
+                Secret to the generated configuration. It is an equivalent of the
+                following patch: `{"op": "add", "path": {.Path}, "value": {.ComputedValueFrom}}`.'
+              properties:
+                path:
+                  description: Path is the JSON-Pointer value (RFC6901) that references
+                    a location within the target configuration.
+                  type: string
+                valueFrom:
+                  description: ValueFrom is the reference to a key of a secret where
+                    the patched value comes from.
+                  properties:
+                    secretKeyRef:
+                      description: Specifies a name and a key of a secret to refer
+                        to. The namespace is implicitly set to the one of referring
+                        object.
+                      properties:
+                        key:
+                          description: The key containing the value.
+                          type: string
+                        name:
+                          description: The secret containing the key.
+                          type: string
+                      required:
+                      - key
+                      - name
+                      type: object
+                  required:
+                  - secretKeyRef
+                  type: object
+              required:
+              - path
+              - valueFrom
+              type: object
+            type: array
           consumerRef:
             description: ConsumerRef is a reference to a particular consumer.
             type: string
@@ -1222,9 +1306,9 @@ spec:
         type: object
         x-kubernetes-validations:
         - message: Using both config and configFrom fields is not allowed.
-          rule: '(has(self.config) || has(self.configFrom)) ? ((!has(self.config)
-            && has(self.configFrom)) || (has(self.config) && !has(self.configFrom)))
-            : true'
+          rule: '!(has(self.config) && has(self.configFrom))'
+        - message: Using both configFrom and configPatches fields is not allowed.
+          rule: '!(has(self.configFrom) && has(self.configPatches))'
         - message: The plugin field is immutable
           rule: self.plugin == oldSelf.plugin
     served: true
diff --git a/test/envtest/crds_envtest_test.go b/test/envtest/crds_envtest_test.go
index b7fb9ae01..fe6a6fbbb 100644
--- a/test/envtest/crds_envtest_test.go
+++ b/test/envtest/crds_envtest_test.go
@@ -550,6 +550,76 @@ func TestCRDValidations(t *testing.T) {
 				assert.ErrorContains(t, err, "The plugin field is immutable")
 			},
 		},
+		{
+			name: "KongPlugin - using configPatches is allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongPlugin{
+					PluginName: "key-auth",
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Secret: "secret-name",
+									Key:    "key-name",
+								},
+							},
+						},
+					},
+				}
+				require.NoError(t, createKongPlugin(ctx, ctrlClient, ns, plugin))
+			},
+		},
+		{
+			name: "KongPlugin - using config and configPatches is allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongPlugin{
+					PluginName: "key-auth",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"key_name":"apikey"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Secret: "secret-name",
+									Key:    "key-name",
+								},
+							},
+						},
+					},
+				}
+				require.NoError(t, createKongPlugin(ctx, ctrlClient, ns, plugin))
+			},
+		},
+		{
+			name: "KongPlugin - using configFrom and configPatches is not allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongPlugin{
+					PluginName: "key-auth",
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Secret: "secret-name",
+							Key:    "key-name",
+						},
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Secret: "secret-name",
+									Key:    "key-name",
+								},
+							},
+						},
+					},
+				}
+				assert.ErrorContains(t, createKongPlugin(ctx, ctrlClient, ns, plugin),
+					"Using both configFrom and configPatches fields is not allowed.")
+			},
+		},
 		{
 			name: "KongClusterPlugin - no config is allowed",
 			scenario: func(ctx context.Context, t *testing.T, ns string) {
@@ -621,6 +691,79 @@ func TestCRDValidations(t *testing.T) {
 				assert.ErrorContains(t, err, "The plugin field is immutable")
 			},
 		},
+		{
+			name: "KongClusterPlugin - using configPatches is allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongClusterPlugin{
+					PluginName: "key-auth",
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: ns,
+									Secret:    "secret-name",
+									Key:       "key-name",
+								},
+							},
+						},
+					},
+				}
+				require.NoError(t, createKongClusterPlugin(ctx, ctrlClient, ns, plugin))
+			},
+		},
+		{
+			name: "KongClusterPlugin - using config and configPatches is allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongClusterPlugin{
+					PluginName: "key-auth",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"key_name":"apikey"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: ns,
+									Secret:    "secret-name",
+									Key:       "key-name",
+								},
+							},
+						},
+					},
+				}
+				require.NoError(t, createKongClusterPlugin(ctx, ctrlClient, ns, plugin))
+			},
+		},
+		{
+			name: "KongClusterPlugin - using configFrom and configPatches is not allowed",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				plugin := &kongv1.KongClusterPlugin{
+					PluginName: "key-auth",
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Secret: "secret-name",
+							Key:    "key-name",
+						},
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/key_names",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: ns,
+									Secret:    "secret-name",
+									Key:       "key-name",
+								},
+							},
+						},
+					},
+				}
+				assert.ErrorContains(t, createKongClusterPlugin(ctx, ctrlClient, ns, plugin),
+					"Using both configFrom and configPatches fields is not allowed.")
+			},
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/test/integration/ingress_test.go b/test/integration/ingress_test.go
index 292e29b55..c018e4205 100644
--- a/test/integration/ingress_test.go
+++ b/test/integration/ingress_test.go
@@ -151,6 +151,48 @@ func TestIngressEssentials(t *testing.T) {
 	helpers.EventuallyExpectHTTP404WithNoRoute(t, proxyURL, proxyURL.Host, "/test_ingress_essentials", ingressWait, waitTick, nil)
 }
 
+func TestIngressDefaultBackend(t *testing.T) {
+	ctx := context.Background()
+	ns, cleaner := helpers.Setup(ctx, t, env)
+
+	t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
+	container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort)
+	deployment := generators.NewDeploymentForContainer(container)
+	deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(deployment)
+
+	t.Logf("exposing deployment %s via service", deployment.Name)
+	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
+	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(service)
+
+	t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
+	ingress := generators.NewIngressForService("/foo", map[string]string{
+		"konghq.com/strip-path": "true",
+	}, service)
+	ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
+	ingress.Spec.DefaultBackend = &netv1.IngressBackend{
+		Service: &netv1.IngressServiceBackend{
+			Name: service.Name,
+			Port: netv1.ServiceBackendPort{
+				Number: service.Spec.Ports[0].Port,
+			},
+		},
+	}
+	require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress))
+	cleaner.Add(ingress)
+
+	t.Log("matching path")
+	helpers.EventuallyGETPath(t, nil, proxyURL.String(), "/foo", http.StatusOK, "<title>httpbin.org</title>", nil, ingressWait, waitTick)
+
+	t.Log("non matching path - use default backend")
+	helpers.EventuallyGETPath(
+		t, nil, proxyURL.String(), fmt.Sprintf("/status/%d", http.StatusTeapot), http.StatusTeapot, "", nil, ingressWait, waitTick,
+	)
+}
+
 func TestGRPCIngressEssentials(t *testing.T) {
 	t.Parallel()
 
diff --git a/test/integration/plugin_test.go b/test/integration/plugin_test.go
index 2d1c28816..ac3105853 100644
--- a/test/integration/plugin_test.go
+++ b/test/integration/plugin_test.go
@@ -6,6 +6,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"strings"
 	"testing"
@@ -156,6 +157,175 @@ func TestPluginEssentials(t *testing.T) {
 	helpers.EventuallyExpectHTTP404WithNoRoute(t, proxyURL, proxyURL.Host, "/test_plugin_essentials", ingressWait, waitTick, nil)
 }
 
+func TestPluginConfigPatch(t *testing.T) {
+	ctx := context.Background()
+
+	t.Parallel()
+	ns, cleaner := helpers.Setup(ctx, t, env)
+
+	t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
+	container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort)
+	deployment := generators.NewDeploymentForContainer(container)
+	deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(deployment)
+
+	t.Logf("exposing deployment %s via service", deployment.Name)
+	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
+	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(service)
+
+	t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
+	ingress := generators.NewIngressForService("/test_plugin_essentials", map[string]string{
+		"konghq.com/strip-path": "true",
+	}, service)
+	ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
+	require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress))
+	cleaner.Add(ingress)
+
+	t.Log("waiting for routes from Ingress to be operational")
+	assert.Eventually(t, func() bool {
+		resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyURL))
+		if err != nil {
+			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
+			return false
+		}
+		defer resp.Body.Close()
+		if resp.StatusCode == http.StatusOK {
+			// now that the ingress backend is routable, make sure the contents we're getting back are what we expect
+			// Expected: "<title>httpbin.org</title>"
+			b := new(bytes.Buffer)
+			n, err := b.ReadFrom(resp.Body)
+			require.NoError(t, err)
+			require.True(t, n > 0)
+			return strings.Contains(b.String(), "<title>httpbin.org</title>")
+		}
+		return false
+	}, ingressWait, waitTick)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ns.Name,
+			Name:      "kongplugin-config",
+		},
+		StringData: map[string]string{
+			"teapot-message":    `"I am a little teapot"`,
+			"forbidden-message": `"This service is forbidden"`,
+		},
+	}
+	secret, err = env.Cluster().Client().CoreV1().Secrets(ns.Name).Create(ctx, secret, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(secret)
+
+	kongplugin := &kongv1.KongPlugin{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ns.Name,
+			Name:      "teapot",
+		},
+		InstanceName: "example",
+		PluginName:   "request-termination",
+		Config: apiextensionsv1.JSON{
+			Raw: []byte(`{"status_code": 418}`),
+		},
+		ConfigPatches: []kongv1.ConfigPatch{
+			{
+				Path: "/message",
+				ValueFrom: kongv1.ConfigSource{
+					SecretValue: kongv1.SecretValueFromSource{
+						Secret: "kongplugin-config",
+						Key:    "teapot-message",
+					},
+				},
+			},
+		},
+	}
+	c, err := clientset.NewForConfig(env.Cluster().Config())
+	require.NoError(t, err)
+	kongplugin, err = c.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, kongplugin, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(kongplugin)
+
+	kongclusterplugin := &kongv1.KongClusterPlugin{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "forbidden",
+			Annotations: map[string]string{
+				annotations.IngressClassKey: consts.IngressClass,
+			},
+		},
+		InstanceName: "example-cluster",
+		PluginName:   "request-termination",
+		Config: apiextensionsv1.JSON{
+			Raw: []byte(`{"status_code": 403}`),
+		},
+		ConfigPatches: []kongv1.NamespacedConfigPatch{
+			{
+				Path: "/message",
+				ValueFrom: kongv1.NamespacedConfigSource{
+					SecretValue: kongv1.NamespacedSecretValueFromSource{
+						Namespace: ns.Name,
+						Secret:    "kongplugin-config",
+						Key:       "forbidden-message",
+					},
+				},
+			},
+		},
+	}
+	kongclusterplugin, err = c.ConfigurationV1().KongClusterPlugins().Create(ctx, kongclusterplugin, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(kongclusterplugin)
+
+	t.Logf("updating Ingress to use plugin %s", kongplugin.Name)
+	require.Eventually(t, func() bool {
+		ingress, err := env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{})
+		if err != nil {
+			return false
+		}
+		ingress.ObjectMeta.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = kongplugin.Name
+		_, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{})
+		return err == nil
+	}, ingressWait, waitTick)
+
+	t.Logf("validating that plugin %s was successfully configured", kongplugin.Name)
+	assert.Eventually(t, func() bool {
+		resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyURL))
+		if err != nil {
+			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
+			return false
+		}
+		defer resp.Body.Close()
+		buf, _ := io.ReadAll(resp.Body)
+		return resp.StatusCode == http.StatusTeapot && strings.Contains(string(buf), "I am a little teapot")
+	}, ingressWait, waitTick)
+
+	t.Logf("updating Ingress to use cluster plugin %s", kongclusterplugin.Name)
+	require.Eventually(t, func() bool {
+		ingress, err := env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{})
+		if err != nil {
+			return false
+		}
+		ingress.ObjectMeta.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = kongclusterplugin.Name
+		_, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{})
+		return err == nil
+	}, ingressWait, waitTick)
+
+	t.Logf("validating that clusterplugin %s was successfully configured", kongclusterplugin.Name)
+	assert.Eventually(t, func() bool {
+		resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyURL))
+		if err != nil {
+			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
+			return false
+		}
+		defer resp.Body.Close()
+		buf, _ := io.ReadAll(resp.Body)
+		return resp.StatusCode == http.StatusForbidden && strings.Contains(string(buf), "This service is forbidden")
+	}, ingressWait, waitTick)
+
+	t.Log("deleting Ingress and waiting for routes to be torn down")
+	require.NoError(t, clusters.DeleteIngress(ctx, env.Cluster(), ns.Name, ingress))
+	helpers.EventuallyExpectHTTP404WithNoRoute(t, proxyURL, proxyURL.Host, "/test_plugin_essentials", ingressWait, waitTick, nil)
+}
+
 func TestPluginOrdering(t *testing.T) {
 	t.Parallel()