Repository: Kong/kubernetes-ingress-controller
Pull request #5964 (2024-05-03 21:25:17)

fix(konnect) avoid collisions when redacting
**What this PR does / why we need it**:

Change the static redacted pretend Vault string to a function that generates a random pretend Vault string.

This avoids collisions for certain types of values when go-database-reconciler builds configuration. It uses an in-memory database that looks up Kong entities by unique fields. If these fields aren't actually unique, go-database-reconciler will detect a conflict and abort.

**Which issue this PR fixes**:

Reported by @mheap in chat.

**Special notes for your reviewer**:

Manual smoke testing: creating a key-auth with `{vault://04a84f95-91f0-4caa-87d1-91a2caa2a6ca}` as the `key` value to validate that UUIDs are valid Vault keys.

Originally did this for all redacted strings, but turns out that the random stuff needs accounting for in tests in less than intuitive ways, so limited to the resources we know have this problem for certain. key-auth is probably alone there, as one of the few (only?) things where the primary key has to be sensitive.

**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/5964?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) Report
Attention: Patch coverage is `0%` with `12 lines` in your changes are missing coverage. Please review.
> :exclamation: No coverage uploaded for pull request base (`main@755e2ce`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#section-missing-base-commit).

> :exclamation: Current head c9f88c3 differs from pull request most recent head 1ce7f48. Consider uploading reports for the commit 1ce7f48 to get more accurate results

| [Files](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) | Patch % | Lines |
|---|---|---|
| [internal/dataplane/kongstate/credentials.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Fcredentials.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9jcmVkZW50aWFscy5nbw==) | 0.0% | [8 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/kongstate/plugin.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Fplugin.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9wbHVnaW4uZ28=) | 0.0% | [2 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/kongstate/license.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Flicense.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9saWNlbnNlLmdv) | 0.0% | [1 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/kongstate/types.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Ftypes.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS90eXBlcy5nbw==) | 0.0% | [1 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |

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


```diff
@@          Coverage Diff           @@
##             main   #5964   +/-   ##
======================================
  Coverage        ?   40.5%           
======================================
  Files           ?     178           
  Lines           ?   18305           
  Branches        ?       0           
======================================
  Hits            ?    7418           
  Misses          ?    9877           
  Partials        ?    1010           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5964?dropdown=coverage&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/.github/workflows/checks.yaml b/.github/workflows/checks.yaml
index d47efe6d9..138dafcc2 100644
--- a/.github/workflows/checks.yaml
+++ b/.github/workflows/checks.yaml
@@ -33,7 +33,7 @@ jobs:
       - name: Check if PR is up to date, if it is skip workflows for this ref
         id: 'up-to-date'
         if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')
-        uses: Kong/public-shared-actions/pr-previews/up-to-date@v2.2.2
+        uses: Kong/public-shared-actions/pr-previews/up-to-date@v2.2.3
         with:
           github_token: ${{ secrets.GITHUB_TOKEN }}
 
diff --git a/.github/workflows/validate_kong_image_trigger_via_label.yaml b/.github/workflows/validate_kong_image_trigger_via_label.yaml
index 70b60f323..52e7131c3 100644
--- a/.github/workflows/validate_kong_image_trigger_via_label.yaml
+++ b/.github/workflows/validate_kong_image_trigger_via_label.yaml
@@ -12,7 +12,10 @@ jobs:
     runs-on: ubuntu-latest
     env:
       ISSUE_NUMBER: ${{ github.event.issue.number }}
+      GH_TOKEN: ${{ secrets.K8S_TEAM_BOT_GH_PAT }}
     steps:
+      - name: checkout repository
+        uses: actions/checkout@v4
       - name: "Remove the label ci/run-validate-kong-image if exist"
         run: gh issue edit ${ISSUE_NUMBER} --remove-label ci/run-validate-kong-image
 
@@ -87,9 +90,11 @@ jobs:
       kic_branch: ${{ steps.choose_kic_branch.outputs.KIC_BRANCH }}
       kic_image_tag: ${{ steps.choose_kic_branch.outputs.KIC_IMAGE_TAG }}
     steps:
-     - name: Choose KIC image and branch
-       id: choose_kic_branch
-       run: |
+      - name: checkout repository
+        uses: actions/checkout@v4
+      - name: Choose KIC image and branch
+        id: choose_kic_branch
+        run: |
           if [[ $TEST_KONG_IMAGE_MAJOR_VERSION -le 2 ]]; then
             echo "KIC_BRANCH=release/2.12.x" >> $GITHUB_OUTPUT
             echo "KIC_IMAGE_TAG=2.12" >> $GITHUB_OUTPUT
@@ -118,8 +123,7 @@ jobs:
       KIC_BRANCH: ${{ needs.decide-kic-branch-and-image.outputs.kic_branch }}
       KIC_IMAGE_TAG: ${{ needs.decide-kic-branch-and-image.outputs.kic_image_tag }} 
     steps:
-      - name: checkout to KIC repo
-        id: checkout_kic_repo
+      - name: checkout repository
         uses: actions/checkout@v4
 
       - name: Run validate Kong image tests
diff --git a/.tools_versions.yaml b/.tools_versions.yaml
index b0d80d095..449e59b28 100644
--- a/.tools_versions.yaml
+++ b/.tools_versions.yaml
@@ -5,11 +5,11 @@ controller-tools: "0.15.0"
 # renovate: datasource=github-releases depName=kubernetes-sigs/kustomize
 kustomize: "5.3.0"
 # renovate: datasource=github-releases depName=golangci/golangci-lint
-golangci-lint: "1.57.2"
+golangci-lint: "1.58.1"
 # renovate: datasource=github-releases depName=GoogleContainerTools/skaffold
 skaffold: "2.11.1"
 # renovate: datasource=github-releases depName=kubernetes-sigs/controller-runtime
-setup-envtest: "0.18.1"
+setup-envtest: "0.18.2"
 # renovate: datasource=github-releases depName=elastic/crd-ref-docs
 crd-ref-docs: "0.0.12"
 # renovate: datasource=github-releases depName=mikefarah/yq
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ce15c555..7f857005b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -118,8 +118,26 @@ Adding a new version? You'll need three changes:
   [#5894](https://github.com/Kong/kubernetes-ingress-controller/pull/5894)
 - Add support in `HTTPRoute`s for `URLRewrite`:
   - `FullPathRewrite` [#5855](https://github.com/Kong/kubernetes-ingress-controller/pull/5855)
+  - `ReplacePrefixMatch` for both router modes:
+    - `traditional_compatible`: [#5895](https://github.com/Kong/kubernetes-ingress-controller/pull/5895)
+    - `expressions`: [#5940](https://github.com/Kong/kubernetes-ingress-controller/pull/5940)
 - DB mode now supports Event reporting for resources that failed to apply.
   [#5785](https://github.com/Kong/kubernetes-ingress-controller/pull/5785)
+- Improve validation - reject `Ingresses`, `Services`, `HTTPRoutes`, `KongConsumers` or `KongConsumerGroups`
+  that have multiple instances of the same type plugin attached.
+  [#5972](https://github.com/Kong/kubernetes-ingress-controller/pull/5972)
+  [#5979](https://github.com/Kong/kubernetes-ingress-controller/pull/5979)
+- Added support for `konghq.com/headers-separator` that allows to set custom separator (instead of default `,`)
+  for headers specified with `konghq.com/headers.*` annotations. Moreover parsing a content of `konghq.com/headers.*`
+  is more robust - leading and trailing whitespace characters are discarded.
+  [#5977](https://github.com/Kong/kubernetes-ingress-controller/pull/5977)
+- Added the CRD `KongCustomEntity` to support custom Kong entities that are not
+  defined in KIC yet.
+  [#5982](https://github.com/Kong/kubernetes-ingress-controller/pull/5982)
+- Added `FallbackConfiguration` feature gate to enable the controller to generate a fallback configuration
+  for Kong when it fails to apply the configuration. The feature gate is disabled by default.
+  [#5993](https://github.com/Kong/kubernetes-ingress-controller/pull/5993)
+
 
 ### Fixed
 
@@ -138,6 +156,8 @@ Adding a new version? You'll need three changes:
   `parentRefs`'s group/kind is not `gateway.network.k8s.io/Gateway`, the item
   is seen as a parent other than the controller and ignored in parentRef check.
   [#5919](https://github.com/Kong/kubernetes-ingress-controller/pull/5919)
+- Redacted values no longer cause collisions in configuration reported to Konnect.
+  [5964](https://github.com/Kong/kubernetes-ingress-controller/pull/5964)
 
 ### Changed
 
diff --git a/Dockerfile b/Dockerfile
index 115c2cb7c..3d66b7465 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 ### Standard binary
 # Build the manager binary
-FROM --platform=$BUILDPLATFORM golang:1.22.2 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.3 as builder
 
 ARG GOPATH
 ARG GOCACHE
diff --git a/Dockerfile.debug b/Dockerfile.debug
index 6cbd70a9a..13dbb328a 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -1,5 +1,5 @@
 # Build a manager binary with debug symbols and download Delve
-FROM --platform=$BUILDPLATFORM golang:1.22.2 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.3 as builder
 
 ARG GOPATH
 ARG GOCACHE
@@ -46,7 +46,7 @@ RUN --mount=type=cache,target=$GOPATH/pkg/mod \
 
 ### Debug
 # Create an image that runs a debug build with Delve installed
-FROM golang:1.22.2 AS debug
+FROM golang:1.22.3 AS debug
 RUN go install github.com/go-delve/delve/cmd/dlv@latest
 # We want all source so Delve file location operations work
 COPY --from=builder /workspace/bin/manager-debug /
diff --git a/Makefile b/Makefile
index b5c731608..120b27ac1 100644
--- a/Makefile
+++ b/Makefile
@@ -289,7 +289,7 @@ manifests.single: kustomize ## Compose single-file deployment manifests from bui
 # ------------------------------------------------------------------------------
 
 .PHONY: generate
-generate: generate.controllers generate.clientsets generate.gateway-api-consts generate.docs fmt
+generate: generate.controllers generate.clientsets generate.gateway-api-consts generate.docs generate.store fmt
 
 .PHONY: generate.controllers
 generate.controllers: controller-gen
@@ -328,6 +328,10 @@ generate.apidocs: crd-ref-docs
 generate.cli-arguments-docs:
 	go run ./scripts/cli-arguments-docs-gen/main.go > ./docs/cli-arguments.md
 
+.PHONY: generate.store
+generate.store:
+	go generate ./internal/store
+
 # ------------------------------------------------------------------------------
 # Build - Container Images
 # ------------------------------------------------------------------------------
diff --git a/config/crd/bases/configuration.konghq.com_kongcustomentities.yaml b/config/crd/bases/configuration.konghq.com_kongcustomentities.yaml
new file mode 100644
index 000000000..91c6da9b5
--- /dev/null
+++ b/config/crd/bases/configuration.konghq.com_kongcustomentities.yaml
@@ -0,0 +1,197 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.15.0
+  name: kongcustomentities.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongCustomEntity
+    listKind: KongCustomEntityList
+    plural: kongcustomentities
+    shortNames:
+    - kce
+    singular: kongcustomentity
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: type of the Kong entity
+      jsonPath: .spec.type
+      name: Entity Type
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: KongCustomEntity defines a "custom" Kong entity that KIC cannot
+          support the entity type directly.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              controllerName:
+                description: ControllerName specifies the controller that should reconcile
+                  it, like ingress class.
+                type: string
+              fields:
+                description: Fields defines the fields of the Kong entity itself.
+                x-kubernetes-preserve-unknown-fields: true
+              parentRef:
+                description: |-
+                  ParentRef references the kubernetes resource it attached to when its scope is "attached".
+                  Currently only KongPlugin/KongClusterPlugin allowed. This will make the custom entity to be attached
+                  to the entity(service/route/consumer) where the plugin is attached.
+                properties:
+                  group:
+                    type: string
+                  kind:
+                    type: string
+                  name:
+                    type: string
+                  namespace:
+                    description: Empty namespace means the same namespace of the owning
+                      object.
+                    type: string
+                required:
+                - name
+                type: object
+              type:
+                description: EntityType is the type of the Kong entity. The type is
+                  used in generating declarative configuration.
+                type: string
+            required:
+            - controllerName
+            - fields
+            - type
+            type: object
+          status:
+            description: Status stores the reconciling status of the resource.
+            properties:
+              conditions:
+                default:
+                - lastTransitionTime: "1970-01-01T00:00:00Z"
+                  message: Waiting for controller
+                  reason: Pending
+                  status: Unknown
+                  type: Programmed
+                description: |-
+                  Conditions describe the current conditions of the KongCustomEntityStatus.
+
+
+                  Known condition types are:
+
+
+                  * "Programmed"
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource.\n---\nThis struct is intended for
+                    direct use as an array at the field path .status.conditions.  For
+                    example,\n\n\n\ttype FooStatus struct{\n\t    // Represents the
+                    observations of a foo's current state.\n\t    // Known .status.conditions.type
+                    are: \"Available\", \"Progressing\", and \"Degraded\"\n\t    //
+                    +patchMergeKey=type\n\t    // +patchStrategy=merge\n\t    // +listType=map\n\t
+                    \   // +listMapKey=type\n\t    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+                    \   // other fields\n\t}"
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        ---
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+                        useful (see .node.status.conditions), the ability to deconflict is important.
+                        The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                maxItems: 8
+                type: array
+                x-kubernetes-list-map-keys:
+                - type
+                x-kubernetes-list-type: map
+            required:
+            - conditions
+            type: object
+        required:
+        - spec
+        type: object
+        x-kubernetes-validations:
+        - message: The spec.type field is immutable
+          rule: self.spec.type == oldSelf.spec.type
+        - message: The spec.type field cannot be known Kong entity types
+          rule: '!(self.spec.type in [''services'',''routes'',''upstreams'',''targets'',''plugins'',''consumers'',''consumer_groups''])'
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 1a9f7a1c8..28bf2b3d5 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -313,6 +313,7 @@ _Appears in:_
 Package v1alpha1 contains API Schema definitions for the configuration.konghq.com v1alpha1 API group.
 
 - [IngressClassParameters](#ingressclassparameters)
+- [KongCustomEntity](#kongcustomentity)
 - [KongLicense](#konglicense)
 - [KongVault](#kongvault)
 ### IngressClassParameters
@@ -331,6 +332,22 @@ IngressClassParameters is the Schema for the IngressClassParameters API.
 
 
 
+### KongCustomEntity
+
+
+KongCustomEntity defines a "custom" Kong entity that KIC cannot support the entity type directly.
+
+<!-- kong_custom_entity description placeholder -->
+
+| Field | Description |
+| --- | --- |
+| `apiVersion` _string_ | `configuration.konghq.com/v1alpha1`
+| `kind` _string_ | `KongCustomEntity`
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. |
+| `spec` _[KongCustomEntitySpec](#kongcustomentityspec)_ |  |
+
+
+
 ### KongLicense
 
 
@@ -428,6 +445,28 @@ Kind refers to a Kubernetes kind.
 _Appears in:_
 - [ControllerReference](#controllerreference)
 
+#### KongCustomEntitySpec
+
+
+
+
+
+
+| Field | Description |
+| --- | --- |
+| `type` _string_ | EntityType is the type of the Kong entity. The type is used in generating declarative configuration. |
+| `fields` _[JSON](#json)_ | Fields defines the fields of the Kong entity itself. |
+| `controllerName` _string_ | ControllerName specifies the controller that should reconcile it, like ingress class. |
+| `parentRef` _[ObjectReference](#objectreference)_ | ParentRef references the kubernetes resource it attached to when its scope is "attached". Currently only KongPlugin/KongClusterPlugin allowed. This will make the custom entity to be attached to the entity(service/route/consumer) where the plugin is attached. |
+
+
+_Appears in:_
+- [KongCustomEntity](#kongcustomentity)
+
+
+
+
+
 #### KongLicenseControllerStatus
 
 
@@ -496,6 +535,24 @@ RFC 1123 labels, or RFC 1035 labels.
 _Appears in:_
 - [ControllerReference](#controllerreference)
 
+#### ObjectReference
+
+
+ObjectReference defines reference of a kubernetes object.
+
+
+
+| Field | Description |
+| --- | --- |
+| `group` _string_ |  |
+| `kind` _string_ |  |
+| `namespace` _string_ | Empty namespace means the same namespace of the owning object. |
+| `name` _string_ |  |
+
+
+_Appears in:_
+- [KongCustomEntitySpec](#kongcustomentityspec)
+
 
 ## configuration.konghq.com/v1beta1
 
diff --git a/examples/gateway-httproute-rewrite-path-full.yaml b/examples/gateway-httproute-rewrite-path.yaml
similarity index 79%
rename from examples/gateway-httproute-rewrite-path-full.yaml
rename to examples/gateway-httproute-rewrite-path.yaml
index 3da79512a..93d7ba499 100644
--- a/examples/gateway-httproute-rewrite-path-full.yaml
+++ b/examples/gateway-httproute-rewrite-path.yaml
@@ -81,7 +81,7 @@ spec:
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
-  name: echo
+  name: rewrite-path-full
 spec:
   parentRefs:
   - name: kong
@@ -89,7 +89,7 @@ spec:
   - matches:
     - path:
         type: PathPrefix
-        value: /
+        value: /full-path-prefix
     filters:
     - type: URLRewrite
       urlRewrite:
@@ -100,3 +100,25 @@ spec:
     - name: echo
       kind: Service
       port: 80
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: rewrite-path-prefix
+spec:
+  parentRefs:
+    - name: kong
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /old-prefix
+      filters:
+        - type: URLRewrite
+          urlRewrite:
+            path:
+              type: ReplacePrefixMatch
+              replacePrefixMatch: /echo
+      backendRefs:
+        - name: echo
+          port: 80
diff --git a/go.mod b/go.mod
index 97e61e76c..40ea2a039 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kong/kubernetes-ingress-controller/v3
 
-go 1.22.2
+go 1.22
 
 // TODO: this is disabled by FOSSA action doesn't support go 1.21's toolchain clause:
 //
@@ -19,7 +19,7 @@ go 1.22.2
 // toolchain go1.21.0
 
 require (
-	cloud.google.com/go/container v1.35.0
+	cloud.google.com/go/container v1.35.1
 	github.com/Masterminds/sprig/v3 v3.2.3
 	github.com/avast/retry-go/v4 v4.6.0
 	github.com/blang/semver/v4 v4.0.0
@@ -38,7 +38,7 @@ require (
 	github.com/moul/pb v0.0.0-20220425114252-bca18df4138c
 	github.com/oapi-codegen/runtime v1.1.1
 	github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
-	github.com/prometheus/client_golang v1.19.0
+	github.com/prometheus/client_golang v1.19.1
 	github.com/prometheus/common v0.53.0
 	github.com/samber/lo v1.39.0
 	github.com/samber/mo v1.11.0
@@ -48,7 +48,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/testcontainers/testcontainers-go v0.30.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/api v0.177.0
+	google.golang.org/api v0.178.0
 	k8s.io/api v0.29.4
 	k8s.io/apiextensions-apiserver v0.29.3
 	k8s.io/apimachinery v0.29.4
@@ -68,7 +68,7 @@ require (
 
 require (
 	cloud.google.com/go/compute/metadata v0.3.0 // indirect
-	dario.cat/mergo v1.0.0 // indirect
+	dario.cat/mergo v1.0.0
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Kong/go-diff v1.2.2 // indirect
 	github.com/Kong/gojsondiff v1.3.2 // indirect
@@ -120,7 +120,7 @@ require (
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
-	github.com/googleapis/gax-go/v2 v2.12.3 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
 	github.com/gorilla/websocket v1.5.1 // indirect
 	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -203,7 +203,7 @@ require (
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
 	golang.org/x/mod v0.16.0 // indirect
 	golang.org/x/net v0.24.0 // indirect
-	golang.org/x/oauth2 v0.19.0 // indirect
+	golang.org/x/oauth2 v0.20.0 // indirect
 	golang.org/x/sync v0.7.0
 	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/term v0.19.0 // indirect
@@ -211,10 +211,10 @@ require (
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.17.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
 	google.golang.org/grpc v1.63.2
-	google.golang.org/protobuf v1.34.0 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index b6151f79e..31a14a2a4 100644
--- a/go.sum
+++ b/go.sum
@@ -7,8 +7,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKF
 cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
 cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
 cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
-cloud.google.com/go/container v1.35.0 h1:y5gmgrMMhTrLnQQdMCw0t/Yis9Ps7jvAG4JYcRWxR8g=
-cloud.google.com/go/container v1.35.0/go.mod h1:02fCocALhTHLw4zwqrRaFrztjoQd53yZWFq0nvr+hQo=
+cloud.google.com/go/container v1.35.1 h1:Vbu/3PZNrgV1Z5DGcRubQdUccX/uMUDNc+NgHNIfbEk=
+cloud.google.com/go/container v1.35.1/go.mod h1:udm8fgLm3TtpnjFN4QLLjZezAIIp/VnMo316yIRVRQU=
 dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
@@ -195,8 +195,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
-github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
@@ -357,8 +357,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
 github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
 github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
@@ -467,8 +467,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCy
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
 go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
 go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
-go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
+go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
+go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
 go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
 go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
@@ -519,8 +519,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
 golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
-golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -592,15 +592,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
 gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk=
-google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw=
+google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok=
+google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
-google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww=
+google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -619,8 +619,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
-google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/hack/generators/cache-stores-snapshot/main.go b/hack/generators/cache-stores-snapshot/main.go
new file mode 100644
index 000000000..834ed2d01
--- /dev/null
+++ b/hack/generators/cache-stores-snapshot/main.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+	"bytes"
+	"os"
+	"reflect"
+	"text/template"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+)
+
+// This code generates `store.CacheStores.TakeSnapshot` method.
+
+const (
+	outputFile = "zz_generated_cache_stores_snapshot.go"
+
+	outputTemplate = `// Code generated by hack/generators/cache/snapshot.go; DO NOT EDIT.
+package store
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/cache"
+)
+
+func (c CacheStores) TakeSnapshot() (CacheStores, error) {
+	// Create a fresh CacheStores instance to store the snapshot.
+	snapshot := NewCacheStores()
+
+	// Gather cache store fields for all kinds.
+	allStores := []cache.Store{
+		{{- range . }}
+		c.{{ . }},
+		{{- end }}
+	}
+
+	c.l.RLock()
+	defer c.l.RUnlock()
+
+	// Iterate over all stores and add a deep copy of each object to the snapshot.
+	for _, store := range allStores {
+		for _, item := range store.List() {
+			obj, ok := item.(runtime.Object)
+			if !ok {
+				return CacheStores{}, fmt.Errorf("expected runtime.Object, got %T", item)
+			}
+
+			copiedObj := obj.DeepCopyObject()
+			if err := snapshot.Add(copiedObj); err != nil {
+				return CacheStores{}, err
+			}
+		}
+	}
+
+	return snapshot, nil
+}
+`
+)
+
+func main() {
+	cacheStoresType := reflect.TypeOf(store.CacheStores{})
+	var storesFieldsNames []string
+	for i := 0; i < cacheStoresType.NumField(); i++ {
+		field := cacheStoresType.Field(i)
+		// Only collect fields of `Store` type as `CacheStores` may include other ones (e.g. a mutex).
+		if field.Type.Name() == "Store" {
+			storesFieldsNames = append(storesFieldsNames, field.Name)
+		}
+	}
+
+	tpl, err := template.New("output").Parse(outputTemplate)
+	if err != nil {
+		panic(err)
+	}
+
+	contents := &bytes.Buffer{}
+	if err := tpl.Execute(contents, storesFieldsNames); err != nil {
+		panic(err)
+	}
+
+	if err := os.WriteFile(outputFile, contents.Bytes(), 0o600); err != nil {
+		panic(err)
+	}
+}
diff --git a/internal/admission/handler.go b/internal/admission/handler.go
index 9273f186a..9c77afe37 100644
--- a/internal/admission/handler.go
+++ b/internal/admission/handler.go
@@ -439,16 +439,13 @@ const (
 
 // +kubebuilder:webhook:verbs=create;update,groups=core,resources=services,versions=v1,name=services.validation.ingress-controller.konghq.com,path=/,webhookVersions=v1,matchPolicy=equivalent,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1
 
-func (h RequestHandler) handleService(_ context.Context, request admissionv1.AdmissionRequest, responseBuilder *ResponseBuilder) (*admissionv1.AdmissionResponse, error) {
+func (h RequestHandler) handleService(ctx context.Context, request admissionv1.AdmissionRequest, responseBuilder *ResponseBuilder) (*admissionv1.AdmissionResponse, error) {
 	service := corev1.Service{}
 	_, _, err := codecs.UniversalDeserializer().Decode(request.Object.Raw, nil, &service)
 	if err != nil {
 		return nil, err
 	}
 
-	// Service is always allowed.
-	responseBuilder = responseBuilder.Allowed(true)
-
 	if annotations.ExtractConfigurationName(service.Annotations) != "" {
 		warning := fmt.Sprintf(serviceWarning, annotations.AnnotationPrefix+annotations.ConfigurationKey,
 			kongv1beta1.KongUpstreamPolicyAnnotationKey)
@@ -456,7 +453,12 @@ func (h RequestHandler) handleService(_ context.Context, request admissionv1.Adm
 		responseBuilder = responseBuilder.WithWarning(warning)
 	}
 
-	return responseBuilder.Build(), nil
+	ok, message, err := h.Validator.ValidateService(ctx, service)
+	if err != nil {
+		return nil, err
+	}
+
+	return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil
 }
 
 // +kubebuilder:webhook:verbs=create;update,groups=networking.k8s.io,resources=ingresses,versions=v1,name=ingresses.validation.ingress-controller.konghq.com,path=/,webhookVersions=v1,matchPolicy=equivalent,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1
diff --git a/internal/admission/validation/gateway/httproute.go b/internal/admission/validation/gateway/httproute.go
index 58d661b3f..5c897b64d 100644
--- a/internal/admission/validation/gateway/httproute.go
+++ b/internal/admission/validation/gateway/httproute.go
@@ -10,6 +10,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/kongplugin"
 	gatewaycontroller "github.com/kong/kubernetes-ingress-controller/v3/internal/controllers/gateway"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/subtranslator"
@@ -46,6 +47,10 @@ func ValidateHTTPRoute(
 		return true, "", nil
 	}
 
+	if err := kongplugin.ValidatePluginUniquenessPerObject(ctx, managerClient, httproute); err != nil {
+		return false, fmt.Sprintf("HTTPRoute has invalid KongPlugin annotation: %s", err), nil
+	}
+
 	if err := validateHTTPRouteTimeoutBackendRequest(httproute); err != nil {
 		return false, fmt.Sprintf("HTTPRoute spec did not pass validation: %s", err), nil
 	}
@@ -150,16 +155,6 @@ func validateHTTPRouteFeatures(httproute *gatewayapi.HTTPRoute, translatorFeatur
 			}
 
 			if filter.Type == gatewayapi.HTTPRouteFilterURLRewrite && filter.URLRewrite != nil {
-				urlrewrite := filter.URLRewrite
-
-				// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3686
-				if filter.URLRewrite.Path != nil {
-					if urlrewrite.Path.Type != gatewayapi.FullPathHTTPPathModifier {
-						return fmt.Errorf("rules[%d].filters[%d]: filter type %s (with path replace type %s) is unsupported",
-							ruleIndex, filterIndex, filter.Type, urlrewrite.Path.Type)
-					}
-				}
-
 				// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3685
 				if filter.URLRewrite.Hostname != nil {
 					return fmt.Errorf("rules[%d].filters[%d]: filter type %s (with hostname replace) is unsupported",
diff --git a/internal/admission/validation/ingress/ingress.go b/internal/admission/validation/ingress/ingress.go
index 9b7a8a3d4..abcdef45f 100644
--- a/internal/admission/validation/ingress/ingress.go
+++ b/internal/admission/validation/ingress/ingress.go
@@ -8,8 +8,10 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/kong/go-kong/kong"
 	netv1 "k8s.io/api/networking/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/kongplugin"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/failures"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/subtranslator"
@@ -28,6 +30,7 @@ func ValidateIngress(
 	ingress *netv1.Ingress,
 	logger logr.Logger,
 	storer store.Storer,
+	managerClient client.Client,
 ) (bool, string, error) {
 	var (
 		errMsgs           []string
@@ -38,6 +41,10 @@ func ValidateIngress(
 		return false, fmt.Sprintf("Ingress has invalid Kong annotations: %s", err), nil
 	}
 
+	if err := kongplugin.ValidatePluginUniquenessPerObject(ctx, managerClient, ingress); err != nil {
+		return false, fmt.Sprintf("Ingress has invalid KongPlugin annotation: %s", err), nil
+	}
+
 	for _, kg := range ingressToKongRoutesForValidation(translatorFeatures, ingress, failuresCollector, storer) {
 		kg := kg
 		// Validate by using feature of Kong Gateway.
diff --git a/internal/admission/validation/kongplugin/kongplugin.go b/internal/admission/validation/kongplugin/kongplugin.go
new file mode 100644
index 000000000..7e4125ea6
--- /dev/null
+++ b/internal/admission/validation/kongplugin/kongplugin.go
@@ -0,0 +1,59 @@
+package kongplugin
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/samber/lo"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+)
+
+// clusterObjectsLister is an interface controller-runtime client.Client.
+type clusterObjectsLister interface {
+	List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
+}
+
+type k8sObjectWithAnnotations interface {
+	GetAnnotations() map[string]string
+	GetNamespace() string
+}
+
+// ValidatePluginUniquenessPerObject validates that only one plugin of a particular type is attached to an object.
+// It can only check when instance of particular plugin has been already created (KongPlugin has been applied before and saved by Kubernetes).
+// Otherwise it will not be able to check and will return nil.
+func ValidatePluginUniquenessPerObject(ctx context.Context, objectsLister clusterObjectsLister, obj k8sObjectWithAnnotations) error {
+	attachedPlugins := annotations.ExtractKongPluginsFromAnnotations(obj.GetAnnotations())
+	if len(attachedPlugins) == 0 {
+		return nil
+	}
+
+	// Getting plugins directly from the cluster instead of operator's cache to make it more real-time.
+	pluginList := &kongv1.KongPluginList{}
+	if err := objectsLister.List(ctx, pluginList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
+		// If we cannot list plugins, we cannot check for uniqueness, hence assume it is valid.
+		return nil //nolint:nilerr
+	}
+
+	pluginsForType := make(map[string][]string)
+	for _, plugin := range pluginList.Items {
+		if lo.Contains(attachedPlugins, plugin.Name) {
+			// How many plugins of particular type e.g. "rate-limiting" are attached and what are those names.
+			pluginsForType[plugin.PluginName] = append(pluginsForType[plugin.PluginName], plugin.Name)
+		}
+	}
+
+	// Check if the plugin is unique.
+	for pluginType, pluginNames := range pluginsForType {
+		if len(pluginNames) > 1 {
+			return fmt.Errorf(
+				"cannot attach multiple plugins: %s of the same type %s",
+				strings.Join(pluginNames, ", "), pluginType,
+			)
+		}
+	}
+	return nil
+}
diff --git a/internal/admission/validator.go b/internal/admission/validator.go
index 5bb3bbc03..d376bc87e 100644
--- a/internal/admission/validator.go
+++ b/internal/admission/validator.go
@@ -18,6 +18,7 @@ import (
 	credsvalidation "github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/consumers/credentials"
 	gatewayvalidation "github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/gateway"
 	ingressvalidation "github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/ingress"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/admission/validation/kongplugin"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	gatewaycontroller "github.com/kong/kubernetes-ingress-controller/v3/internal/controllers/gateway"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/kongstate"
@@ -41,6 +42,7 @@ type KongValidator interface {
 	ValidateGateway(ctx context.Context, gateway gatewayapi.Gateway) (bool, string, error)
 	ValidateHTTPRoute(ctx context.Context, httproute gatewayapi.HTTPRoute) (bool, string, error)
 	ValidateIngress(ctx context.Context, ingress netv1.Ingress) (bool, string, error)
+	ValidateService(ctx context.Context, ingress corev1.Service) (bool, string, error)
 }
 
 // AdminAPIServicesProvider provides KongHTTPValidator with Kong Admin API services that are needed to perform
@@ -150,6 +152,9 @@ func (validator KongHTTPValidator) ValidateConsumer(
 	if !validator.ingressClassMatcher(&consumer.ObjectMeta, annotations.IngressClassKey, annotations.ExactClassMatch) {
 		return true, "", nil
 	}
+	if err := kongplugin.ValidatePluginUniquenessPerObject(ctx, validator.ManagerClient, &consumer); err != nil {
+		return false, fmt.Sprintf("KongConsumer has invalid KongPlugin annotation: %s", err), nil
+	}
 
 	errText, err := validator.ensureConsumerDoesNotExistInGateway(ctx, consumer.Username)
 	if err != nil || errText != "" {
@@ -229,6 +234,10 @@ func (validator KongHTTPValidator) ValidateConsumerGroup(
 		return true, "", nil
 	}
 
+	if err := kongplugin.ValidatePluginUniquenessPerObject(ctx, validator.ManagerClient, &consumerGroup); err != nil {
+		return false, fmt.Sprintf("KongConsumerGroup has invalid KongPlugin annotation: %s", err), nil
+	}
+
 	infoSvc, ok := validator.AdminAPIServicesProvider.GetInfoService()
 	if !ok {
 		return true, "", nil
@@ -462,7 +471,16 @@ func (validator KongHTTPValidator) ValidateIngress(
 	if routesSvc, ok := validator.AdminAPIServicesProvider.GetRoutesService(); ok {
 		routeValidator = routesSvc
 	}
-	return ingressvalidation.ValidateIngress(ctx, routeValidator, validator.TranslatorFeatures, &ingress, validator.Logger, validator.Storer)
+	return ingressvalidation.ValidateIngress(ctx, routeValidator, validator.TranslatorFeatures, &ingress, validator.Logger, validator.Storer, validator.ManagerClient)
+}
+
+func (validator KongHTTPValidator) ValidateService(
+	ctx context.Context, service corev1.Service,
+) (bool, string, error) {
+	if err := kongplugin.ValidatePluginUniquenessPerObject(ctx, validator.ManagerClient, &service); err != nil {
+		return false, fmt.Sprintf("Service has invalid KongPlugin annotation: %s", err), nil
+	}
+	return true, "", nil
 }
 
 type routeValidator interface {
diff --git a/internal/annotations/annotations.go b/internal/annotations/annotations.go
index 37795eddf..c5db8a2da 100644
--- a/internal/annotations/annotations.go
+++ b/internal/annotations/annotations.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/samber/lo"
 	netv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
@@ -61,6 +62,7 @@ const (
 	ReadTimeoutKey       = "/read-timeout"
 	RetriesKey           = "/retries"
 	HeadersKey           = "/headers"
+	HeadersSeparatorKey  = "/headers-separator"
 	PathHandlingKey      = "/path-handling"
 	UserTagKey           = "/tags"
 	RewriteURIKey        = "/rewrite"
@@ -311,14 +313,20 @@ func ExtractRetries(anns map[string]string) (string, bool) {
 // ExtractHeaders extracts the parsed headers annotations values. It returns a map of header names to slices of values.
 func ExtractHeaders(anns map[string]string) (map[string][]string, bool) {
 	headers := make(map[string][]string)
-	prefix := AnnotationPrefix + HeadersKey + "."
+	const prefix = AnnotationPrefix + HeadersKey + "."
+	separator, ok := anns[AnnotationPrefix+HeadersSeparatorKey]
+	if !ok {
+		separator = ","
+	}
 	for key, val := range anns {
 		if strings.HasPrefix(key, prefix) {
 			header := strings.TrimPrefix(key, prefix)
 			if len(header) == 0 || len(val) == 0 {
 				continue
 			}
-			headers[header] = strings.Split(val, ",")
+			headers[header] = lo.Map(strings.Split(val, separator), func(hv string, _ int) string {
+				return strings.TrimSpace(hv)
+			})
 		}
 	}
 	if len(headers) == 0 {
diff --git a/internal/dataplane/kong_client.go b/internal/dataplane/kong_client.go
index 96352ff03..54fc5d823 100644
--- a/internal/dataplane/kong_client.go
+++ b/internal/dataplane/kong_client.go
@@ -56,6 +56,7 @@ const (
 // KongConfigBuilder builds a Kong configuration from a Kubernetes object cache.
 type KongConfigBuilder interface {
 	BuildKongConfig() translator.KongConfigBuildingResult
+	UpdateCache(store.CacheStores)
 }
 
 // KongClient is a threadsafe high level API client for the Kong data-plane(s)
@@ -156,7 +157,7 @@ func NewKongClient(
 	updateStrategyResolver sendconfig.UpdateStrategyResolver,
 	configChangeDetector sendconfig.ConfigurationChangeDetector,
 	kongConfigFetcher configfetcher.LastValidConfigFetcher,
-	parser KongConfigBuilder,
+	kongConfigBuilder KongConfigBuilder,
 	cacheStores store.CacheStores,
 ) (*KongClient, error) {
 	c := &KongClient{
@@ -172,7 +173,7 @@ func NewKongClient(
 		configStatusNotifier:   clients.NoOpConfigStatusNotifier{},
 		updateStrategyResolver: updateStrategyResolver,
 		configChangeDetector:   configChangeDetector,
-		kongConfigBuilder:      parser,
+		kongConfigBuilder:      kongConfigBuilder,
 		kongConfigFetcher:      kongConfigFetcher,
 	}
 	c.initializeControllerPodReference()
@@ -396,6 +397,17 @@ func (c *KongClient) Update(ctx context.Context) error {
 		}
 	}
 
+	// If FallbackConfiguration is enabled, we take a snapshot of the cache so that we operate on a consistent
+	// set of resources in case of failures being returned from Kong. As we're going to generate a fallback config
+	// based on the cache contents, we need to ensure it is not modified during the process.
+	if c.kongConfig.FallbackConfiguration {
+		cacheSnapshot, err := c.cache.TakeSnapshot()
+		if err != nil {
+			return fmt.Errorf("failed to take snapshot of cache: %w", err)
+		}
+		c.kongConfigBuilder.UpdateCache(cacheSnapshot)
+	}
+
 	c.logger.V(util.DebugLevel).Info("Parsing kubernetes objects into data-plane configuration")
 	parsingResult := c.kongConfigBuilder.BuildKongConfig()
 	if failuresCount := len(parsingResult.TranslationFailures); failuresCount > 0 {
@@ -424,6 +436,9 @@ func (c *KongClient) Update(ctx context.Context) error {
 
 	// In case of a failure in syncing configuration with Gateways, propagate the error.
 	if gatewaysSyncErr != nil {
+		// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/5931
+		// In case of a failure in syncing configuration with Gateways, if FallbackConfiguration is enabled,
+		// we should generate a fallback configuration and push it to the gateways first.
 		if state, found := c.kongConfigFetcher.LastValidConfig(); found {
 			_, fallbackSyncErr := c.sendOutToGatewayClients(ctx, state, c.kongConfig)
 			if fallbackSyncErr != nil {
@@ -509,7 +524,7 @@ func (c *KongClient) maybeSendOutToKonnectClient(ctx context.Context, s *kongsta
 		// of the controller.
 
 		if errors.As(err, &sendconfig.UpdateSkippedDueToBackoffStrategyError{}) {
-			c.logger.Error(err, "Skipped pushing configuration to Konnect")
+			c.logger.Info("Skipped pushing configuration to Konnect due to backoff strategy", "explanation", err.Error())
 		} else {
 			c.logger.Error(err, "Failed pushing configuration to Konnect")
 			logKonnectErrors(c.logger, err)
@@ -553,7 +568,7 @@ func (c *KongClient) sendToClient(
 	// If the client is Konnect and the feature flag is turned on,
 	// we should sanitize the configuration before sending it out.
 	if client.IsKonnect() && config.SanitizeKonnectConfigDumps {
-		s = s.SanitizedCopy()
+		s = s.SanitizedCopy(util.DefaultUUIDGenerator{})
 	}
 	deckGenParams := deckgen.GenerateDeckContentParams{
 		SelectorTags:                    config.FilterTags,
@@ -641,7 +656,7 @@ func prepareSendDiagnosticFn(
 	if diagnosticConfig.DumpsIncludeSensitive {
 		redactedConfig := deckgen.ToDeckContent(ctx,
 			logger,
-			targetState.SanitizedCopy(),
+			targetState.SanitizedCopy(util.DefaultUUIDGenerator{}),
 			deckGenParams,
 		)
 		config = redactedConfig
diff --git a/internal/dataplane/kongstate/consumer.go b/internal/dataplane/kongstate/consumer.go
index 9c36945ae..21d9621e3 100644
--- a/internal/dataplane/kongstate/consumer.go
+++ b/internal/dataplane/kongstate/consumer.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/kong/go-kong/kong"
 
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 )
 
@@ -27,13 +28,13 @@ type Consumer struct {
 }
 
 // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort.
-func (c *Consumer) SanitizedCopy() *Consumer {
+func (c *Consumer) SanitizedCopy(uuidGenerator util.UUIDGenerator) *Consumer {
 	return &Consumer{
 		Consumer: c.Consumer,
 		Plugins:  c.Plugins,
 		KeyAuths: func() (res []*KeyAuth) {
 			for _, v := range c.KeyAuths {
-				res = append(res, v.SanitizedCopy())
+				res = append(res, v.SanitizedCopy(uuidGenerator))
 			}
 			return
 		}(),
diff --git a/internal/dataplane/kongstate/credentials.go b/internal/dataplane/kongstate/credentials.go
index 48d9b4d60..e3db1b3a4 100644
--- a/internal/dataplane/kongstate/credentials.go
+++ b/internal/dataplane/kongstate/credentials.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/kong/go-kong/kong"
 	"github.com/mitchellh/mapstructure"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 )
 
 // redactedString is used to redact sensitive values in the KongState.
@@ -12,6 +14,13 @@ import (
 // to pass a valid key or a vault URI).
 var redactedString = kong.String("{vault://redacted-value}")
 
+// randRedactedString is used to redact sensitive values in the KongState when the value must be random to avoid
+// collisions.
+func randRedactedString(uuidGenerator util.UUIDGenerator) *string {
+	s := fmt.Sprintf("{vault://%s}", uuidGenerator.NewString())
+	return &s
+}
+
 // KeyAuth represents a key-auth credential.
 type KeyAuth struct {
 	kong.KeyAuth
@@ -152,13 +161,13 @@ func NewMTLSAuth(config interface{}) (*MTLSAuth, error) {
 }
 
 // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort.
-func (c *KeyAuth) SanitizedCopy() *KeyAuth {
+func (c *KeyAuth) SanitizedCopy(uuidGenerator util.UUIDGenerator) *KeyAuth {
 	return &KeyAuth{
 		kong.KeyAuth{
 			// Consumer field omitted
 			CreatedAt: c.CreatedAt,
 			ID:        c.ID,
-			Key:       redactedString,
+			Key:       randRedactedString(uuidGenerator),
 			Tags:      c.Tags,
 		},
 	}
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index c30c9c1fe..a2b7d5be6 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -36,7 +36,7 @@ type KongState struct {
 }
 
 // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort.
-func (ks *KongState) SanitizedCopy() *KongState {
+func (ks *KongState) SanitizedCopy(uuidGenerator util.UUIDGenerator) *KongState {
 	return &KongState{
 		Services:  ks.Services,
 		Upstreams: ks.Upstreams,
@@ -52,7 +52,7 @@ func (ks *KongState) SanitizedCopy() *KongState {
 		}),
 		Consumers: func() (res []Consumer) {
 			for _, v := range ks.Consumers {
-				res = append(res, *v.SanitizedCopy())
+				res = append(res, *v.SanitizedCopy(uuidGenerator))
 			}
 			return
 		}(),
diff --git a/internal/dataplane/sendconfig/kong.go b/internal/dataplane/sendconfig/kong.go
index 9ce82122d..59bfb0833 100644
--- a/internal/dataplane/sendconfig/kong.go
+++ b/internal/dataplane/sendconfig/kong.go
@@ -33,4 +33,8 @@ type Config struct {
 
 	// SanitizeKonnectConfigDumps indicates whether to sanitize Konnect config dumps.
 	SanitizeKonnectConfigDumps bool
+
+	// FallbackConfiguration indicates whether to generate fallback configuration in the case of entity
+	// errors returned by the Kong Admin API.
+	FallbackConfiguration bool
 }
diff --git a/internal/dataplane/translator/subtranslator/httproute.go b/internal/dataplane/translator/subtranslator/httproute.go
index 1d5bfaa38..7b59b3cda 100644
--- a/internal/dataplane/translator/subtranslator/httproute.go
+++ b/internal/dataplane/translator/subtranslator/httproute.go
@@ -7,10 +7,13 @@ import (
 	"sort"
 	"strings"
 
+	"dario.cat/mergo"
 	"github.com/kong/go-kong/kong"
+	"github.com/samber/lo"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/kongstate"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/atc"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
 )
 
@@ -356,40 +359,67 @@ func mustMarshalJSON[T any](val T) string {
 // The plugins can be set in two different ways:
 // - Direct conversion from the respective HTTPRouteFilter.
 // - ExtensionRef to plugins annotation from the ExtensionRef filter.
-func SetRoutePlugins(route *kongstate.Route, filters []gatewayapi.HTTPRouteFilter, path string, tags []*string) error {
-	plugins, pluginAnnotation, err := generatePluginsFromHTTPRouteFilters(filters, path, tags)
+func SetRoutePlugins(
+	route *kongstate.Route,
+	filters []gatewayapi.HTTPRouteFilter,
+	path string,
+	tags []*string,
+	// As of now, expressions router is not supported for URLRewrite with PrefixMatchHTTPPathModifier.
+	// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3686
+	expressionsRouterEnabled bool,
+) error {
+	generatedPlugins, err := generatePluginsFromHTTPRouteFilters(filters, path, tags, expressionsRouterEnabled)
 	if err != nil {
 		return err
 	}
-	route.Plugins = append(route.Plugins, plugins...)
-	if len(pluginAnnotation) > 0 {
-		if route.Ingress.Annotations == nil {
-			route.Ingress.Annotations = make(map[string]string)
-		}
-		const pluginAnnotationKey = annotations.AnnotationPrefix + annotations.PluginsKey
-		if _, ok := route.Ingress.Annotations[pluginAnnotationKey]; !ok {
-			route.Ingress.Annotations[pluginAnnotationKey] = pluginAnnotation
-		} else {
-			route.Ingress.Annotations[pluginAnnotationKey] = fmt.Sprintf("%s,%s",
-				route.Ingress.Annotations[pluginAnnotationKey],
-				pluginAnnotation)
-		}
+	route.Plugins = append(route.Plugins, generatedPlugins.Plugins...)
+	for _, modifier := range generatedPlugins.KongRouteModifiers {
+		modifier(route)
 	}
 	return nil
 }
 
+// httpRouteFiltersOriginatedPlugins is a set of Kong plugins generated from HTTPRoute filters along with other
+// metadata that is required to apply these plugins to a Kong Route.
+type httpRouteFiltersOriginatedPlugins struct {
+	// Plugins is a list of Kong plugins generated from HTTPRoute filters to be applied to a Kong Route.
+	Plugins []kong.Plugin
+
+	// KongRouteModifiers is a list of functions that will be used to modify a Kong Route.
+	// These can be used to set additional properties on the Kong Route or changing its properties if required by
+	// the filters (e.g. setting different paths).
+	KongRouteModifiers []kongRouteModifier
+
+	// PluginsAnnotation is a `konghq.com/plugins: <plugins>` annotation value generated from the ExtensionRef filter.
+	PluginsAnnotation string
+}
+
+// kongRouteModifier is a function that modifies a Kong route.
+type kongRouteModifier func(*kongstate.Route)
+
 // generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong plugins.
 // path is the parameter to be used by the redirect plugin, to perform redirection.
-// It returns two values:
-// - A set of plugins generated by the conversion of all the provided filters, excluding  ExtensionRefs.
-// - A plugins annotation value, generated by the ExtensionRef filter.
-func generatePluginsFromHTTPRouteFilters(filters []gatewayapi.HTTPRouteFilter, path string, tags []*string) ([]kong.Plugin, string, error) {
-	kongPlugins := make([]kong.Plugin, 0)
+// It returns httpRouteFiltersOriginatedPlugins which contains:
+// - generated plugins
+// - Kong Route modifiers that need to be applied to the Kong Route
+// - PluginsAnnotation that is generated from the ExtensionRef filter.
+func generatePluginsFromHTTPRouteFilters(
+	filters []gatewayapi.HTTPRouteFilter,
+	path string,
+	tags []*string,
+	// As of now, expressions router is not supported for URLRewrite with PrefixMatchHTTPPathModifier.
+	// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3686
+	expressionsRouterEnabled bool,
+) (httpRouteFiltersOriginatedPlugins, error) {
 	if len(filters) == 0 {
-		return kongPlugins, "", nil
+		return httpRouteFiltersOriginatedPlugins{}, nil
 	}
+	var (
+		kongPlugins                 []kong.Plugin
+		pluginNamesFromExtensionRef []string
+		kongRouteModifiers          []kongRouteModifier
+	)
 
-	var pluginsAnnotation strings.Builder
 	for _, filter := range filters {
 		switch filter.Type {
 		case gatewayapi.HTTPRouteFilterRequestHeaderModifier:
@@ -404,27 +434,23 @@ func generatePluginsFromHTTPRouteFilters(filters []gatewayapi.HTTPRouteFilter, p
 		case gatewayapi.HTTPRouteFilterExtensionRef:
 			plugin, err := generateExtensionRefKongPlugin(filter.ExtensionRef)
 			if err != nil {
-				return nil, "", err
-			}
-			if len(pluginsAnnotation.String()) > 0 {
-				_, err := pluginsAnnotation.WriteString("," + plugin)
-				if err != nil {
-					return nil, "", err
-				}
-			} else {
-				pluginsAnnotation.WriteString(plugin)
+				return httpRouteFiltersOriginatedPlugins{}, err
 			}
+			pluginNamesFromExtensionRef = append(pluginNamesFromExtensionRef, plugin)
 
 		case gatewayapi.HTTPRouteFilterURLRewrite:
-			plugin, err := generateRequestTransformerForURLRewrite(filter.URLRewrite)
+			plugin, routeModifier, err := generateRequestTransformerForURLRewrite(filter.URLRewrite, path, expressionsRouterEnabled)
 			if err != nil {
-				return nil, "", err
+				return httpRouteFiltersOriginatedPlugins{}, err
 			}
 			kongPlugins = append(kongPlugins, plugin)
+			if routeModifier != nil {
+				kongRouteModifiers = append(kongRouteModifiers, routeModifier)
+			}
 
 		case gatewayapi.HTTPRouteFilterRequestMirror:
 			// not supported
-			return nil, "", fmt.Errorf("httpFilter %s unsupported", filter.Type)
+			return httpRouteFiltersOriginatedPlugins{}, fmt.Errorf("httpFilter %s unsupported", filter.Type)
 		}
 	}
 	for _, p := range kongPlugins {
@@ -433,7 +459,65 @@ func generatePluginsFromHTTPRouteFilters(filters []gatewayapi.HTTPRouteFilter, p
 		p.Tags = tags
 	}
 
-	return kongPlugins, pluginsAnnotation.String(), nil
+	// It's possible the above loop generates multiple plugins of the same type, so we need to merge them.
+	// It can happen for example when both RequestHeaderModifier and HTTPRouteFilterURLRewrite filters are present.
+	kongPlugins, err := mergePluginsOfTheSameType(kongPlugins)
+	if err != nil {
+		return httpRouteFiltersOriginatedPlugins{}, fmt.Errorf("failed to merge plugins of the same type: %w", err)
+	}
+
+	if len(pluginNamesFromExtensionRef) > 0 {
+		kongRouteModifiers = append(kongRouteModifiers, generateKongRouteModifierFromExtensionRef(pluginNamesFromExtensionRef))
+	}
+
+	return httpRouteFiltersOriginatedPlugins{
+		Plugins:            kongPlugins,
+		KongRouteModifiers: kongRouteModifiers,
+	}, nil
+}
+
+// mergePluginsOfTheSameType merges plugins of the same type into a single plugin with merged configurations.
+func mergePluginsOfTheSameType(plugins []kong.Plugin) ([]kong.Plugin, error) {
+	pluginsByName := lo.GroupBy(plugins, func(p kong.Plugin) string {
+		return *p.Name // Name is effectively a plugin type.
+	})
+	for pluginName, plugins := range pluginsByName {
+		// If we produced multiple plugins of the same type, we need to merge their configurations now.
+		if len(plugins) > 1 {
+			mergedPlugin := *plugins[0].DeepCopy()
+			for _, plugin := range plugins {
+				if err := mergo.Merge(&mergedPlugin.Config, plugin.Config); err != nil {
+					// Should never happen as we're passing the same type of objects.
+					return nil, fmt.Errorf("failed to merge %q plugin configurations: %w", pluginName, err)
+				}
+			}
+			pluginsByName[pluginName] = []kong.Plugin{mergedPlugin}
+		}
+	}
+
+	// Sort the plugins by name to ensure that the order is deterministic.
+	mergedPlugins := lo.Flatten(lo.Values(pluginsByName))
+	sort.Slice(mergedPlugins, func(i, j int) bool {
+		return *mergedPlugins[i].Name < *mergedPlugins[j].Name
+	})
+	return mergedPlugins, nil
+}
+
+func generateKongRouteModifierFromExtensionRef(pluginNamesFromExtensionRef []string) kongRouteModifier {
+	return func(route *kongstate.Route) {
+		if route.Ingress.Annotations == nil {
+			route.Ingress.Annotations = make(map[string]string)
+		}
+		annotationValue := strings.Join(pluginNamesFromExtensionRef, ",")
+		const pluginAnnotationKey = annotations.AnnotationPrefix + annotations.PluginsKey
+		if _, ok := route.Ingress.Annotations[pluginAnnotationKey]; !ok {
+			route.Ingress.Annotations[pluginAnnotationKey] = annotationValue
+		} else {
+			route.Ingress.Annotations[pluginAnnotationKey] = fmt.Sprintf("%s,%s",
+				route.Ingress.Annotations[pluginAnnotationKey],
+				annotationValue)
+		}
+	}
 }
 
 // generateRequestRedirectKongPlugin generates configurations of plugins to satisfy the specification
@@ -544,43 +628,190 @@ func kongHeaderFormatter(header gatewayapi.HTTPHeader) string {
 	return fmt.Sprintf("%s:%s", header.Name, header.Value)
 }
 
-func generateRequestTransformerForURLRewrite(filter *gatewayapi.HTTPURLRewriteFilter) (kong.Plugin, error) {
+func generateRequestTransformerForURLRewrite(
+	filter *gatewayapi.HTTPURLRewriteFilter,
+	path string,
+	expressionsRouterEnabled bool,
+) (kong.Plugin, kongRouteModifier, error) {
 	if filter == nil {
-		return kong.Plugin{}, fmt.Errorf("%s is not provided", gatewayapi.HTTPRouteFilterURLRewrite)
+		return kong.Plugin{}, nil, fmt.Errorf("%s is not provided", gatewayapi.HTTPRouteFilterURLRewrite)
 	}
 
 	if filter.Path == nil && filter.Hostname == nil {
-		return kong.Plugin{}, fmt.Errorf("%s missing Path and Hostname", gatewayapi.HTTPRouteFilterURLRewrite)
+		return kong.Plugin{}, nil, fmt.Errorf("%s missing Path and Hostname", gatewayapi.HTTPRouteFilterURLRewrite)
 	}
 
 	if filter.Path != nil {
 		switch filter.Path.Type {
 		case gatewayapi.FullPathHTTPPathModifier:
-			if filter.Path.ReplaceFullPath == nil {
-				return kong.Plugin{}, fmt.Errorf("%s missing ReplaceFullPath", gatewayapi.HTTPRouteFilterURLRewrite)
-			}
-
-			plugin := kong.Plugin{
-				Name: kong.String("request-transformer"),
-				Config: kong.Configuration{
-					"replace": map[string]string{
-						"uri": *filter.Path.ReplaceFullPath,
-					},
-				},
+			plugin, err := generateRequestTransformerForURLRewriteFullPath(filter)
+			if err != nil {
+				return kong.Plugin{}, nil, fmt.Errorf("failed to generate request-transformer plugin for %s: %w", gatewayapi.HTTPRouteFilterURLRewrite, err)
 			}
+			return plugin, nil, nil
 
-			return plugin, nil
-
-		// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3686
 		case gatewayapi.PrefixMatchHTTPPathModifier:
-			return kong.Plugin{}, fmt.Errorf("%s unsupported for %s", gatewayapi.PrefixMatchHTTPPathModifier, gatewayapi.HTTPRouteFilterURLRewrite)
+			plugin, routeModifier := generateRequestTransformerForURLRewritePrefixMatch(filter, path, expressionsRouterEnabled)
+			return plugin, routeModifier, nil
 		}
 	}
 
 	// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3685
 	if filter.Hostname != nil {
-		return kong.Plugin{}, fmt.Errorf("unsupported hostname replace for %s", gatewayapi.HTTPRouteFilterURLRewrite)
+		return kong.Plugin{}, nil, fmt.Errorf("unsupported hostname replace for %s", gatewayapi.HTTPRouteFilterURLRewrite)
 	}
 
-	return kong.Plugin{}, fmt.Errorf("invalid %s config", gatewayapi.HTTPRouteFilterURLRewrite)
+	return kong.Plugin{}, nil, fmt.Errorf("invalid %s config", gatewayapi.HTTPRouteFilterURLRewrite)
+}
+
+// generateRequestTransformerForURLRewriteFullPath generates a request-transformer plugin for the URLRewrite filter
+// with a FullPathHTTPPathModifier.
+func generateRequestTransformerForURLRewriteFullPath(filter *gatewayapi.HTTPURLRewriteFilter) (kong.Plugin, error) {
+	if filter.Path.ReplaceFullPath == nil {
+		return kong.Plugin{}, fmt.Errorf("%s missing ReplaceFullPath", gatewayapi.HTTPRouteFilterURLRewrite)
+	}
+
+	return kong.Plugin{
+		Name: kong.String("request-transformer"),
+		Config: kong.Configuration{
+			"replace": map[string]string{
+				"uri": *filter.Path.ReplaceFullPath,
+			},
+		},
+	}, nil
+}
+
+// generateRequestTransformerForURLRewritePrefixMatch generates a request-transformer plugin and a route modifier
+// for the URLRewrite filter with a PrefixMatchHTTPPathModifier.
+func generateRequestTransformerForURLRewritePrefixMatch(
+	filter *gatewayapi.HTTPURLRewriteFilter,
+	path string,
+	expressionsRouterEnabled bool,
+) (kong.Plugin, kongRouteModifier) {
+	// Normalize the path before passing it down.
+	path = normalizePath(path)
+
+	return kong.Plugin{
+		Name: kong.String("request-transformer"),
+		Config: kong.Configuration{
+			"replace": map[string]string{
+				"uri": generateRequestTransformerReplaceURIForURLRewritePrefixMatch(
+					filter.Path.ReplacePrefixMatch,
+					path,
+				),
+			},
+		},
+	}, generateKongRouteModifierForURLRewritePrefixMatch(path, expressionsRouterEnabled)
+}
+
+// generateRequestTransformerReplaceURIForURLRewritePrefixMatch generates the replacement URI for the request-transformer
+// plugin for the URLRewrite filter with a PrefixMatchHTTPPathModifier.
+func generateRequestTransformerReplaceURIForURLRewritePrefixMatch(
+	replacePrefixMatch *string,
+	path string,
+) string {
+	if replacePrefixMatch == nil {
+		// If no ReplacePrefixMatch is provided, we need to use a slash as the replacement URI.
+		return "/"
+	}
+
+	// If the ReplacePrefixMatch is provided, we need to use it as the replacement URI.
+	// Trim the trailing slash from the ReplacePrefixMatch to avoid double slashes in the final URI.
+	*replacePrefixMatch = strings.TrimSuffix(*replacePrefixMatch, "/")
+	pathIsRoot := isPathRoot(path)
+
+	// In the case of an empty replacePrefixMatch, we need to make sure that the path will always start with a slash,
+	// even if we have no capture group from the incoming request's URI.
+	if *replacePrefixMatch == "" {
+		// If path is "/", we need to add a slash before URI captures because the capture group won't include
+		// the leading slash.
+		if pathIsRoot {
+			// The below is a Lua ternary operator that checks if the captured group is nil, and if so, replaces it with
+			// a slash. Otherwise, it appends the captured group to a slash.
+			return `$(uri_captures[1] == nil and "/" or "/" .. uri_captures[1])`
+		}
+
+		// Otherwise, we do not need to add a leading slash before URI captures.
+		// The below is a Lua ternary operator that checks if the captured group is nil, and if so, replaces it with
+		// a slash. Otherwise, it returns the captured group (in this case the captured group will always have a
+		// leading slash).
+		return `$(uri_captures[1] == nil and "/" or uri_captures[1])`
+	}
+
+	// Otherwise, we concatenate the replacement URI with the captured group.
+	// If path is "/", we need to add a slash before URI captures because the capture group won't include the
+	// leading slash.
+	if pathIsRoot {
+		// The below Lua ternary operator checks if the captured group is nil, and if so, replaces it with
+		// an empty string (as we already know replacePrefixMatch is not empty so the resulting path will always have
+		// a leading slash). Otherwise, it appends the captured group to a slash (as the captured group won't
+		// have the leading slash).
+		return fmt.Sprintf(`%s$(uri_captures[1] == nil and "" or "/" .. uri_captures[1])`, *replacePrefixMatch)
+	}
+	// Simply concatenate the replacement URI with the captured group as the captured group will always have a
+	// leading slash.
+	return fmt.Sprintf(`%s$(uri_captures[1])`, *replacePrefixMatch)
+}
+
+// generateKongRouteModifierForURLRewritePrefixMatch generates a Kong route modifier for the URLRewrite filter with a
+// PrefixMatchHTTPPathModifier.
+// One of the paths will match the exact path, and the other will match subpaths with a capture group.
+// The capture group can be used in the request-transformer plugin to replace the path.
+// Please note that depending on the path, the capture group will:
+// - Exclude the leading slash if the path is root.
+// - Include the leading slash otherwise.
+func generateKongRouteModifierForURLRewritePrefixMatch(path string, expressionsRouterEnabled bool) func(route *kongstate.Route) {
+	pathIsRoot := isPathRoot(path)
+
+	// If expressions router is enabled, we need to set the expression on the Kong Route.
+	if expressionsRouterEnabled {
+		return func(route *kongstate.Route) {
+			exactPrefixPredicate := atc.NewPredicateHTTPPath(atc.OpEqual, path)
+			subpathsPredicate := func() atc.Predicate {
+				if pathIsRoot {
+					// If the path is "/", we don't capture the slash as Kong Route's path has to begin with a slash.
+					// If we captured the slash, we'd generate "(/.*)", and it'd be rejected by Kong.
+					return atc.NewPredicateHTTPPath(atc.OpRegexMatch, "^/(.*)")
+				}
+				// If the path is not "/", i.e. it has a prefix, we capture the slash to make it possible to
+				// route "/prefix" to "/replacement" and "/prefix/" to "/replacement/" correctly.
+				return atc.NewPredicateHTTPPath(atc.OpRegexMatch, fmt.Sprintf("^%s(/.*)", path))
+			}()
+			route.Route.Expression = lo.ToPtr(atc.Or(exactPrefixPredicate, subpathsPredicate).Expression())
+		}
+	}
+
+	// Otherwise, we set the Kong Route's paths.
+	return func(route *kongstate.Route) {
+		paths := make([]*string, 0, 2)
+		// The first path matches the exact path.
+		paths = append(paths, lo.ToPtr(fmt.Sprintf("%s%s$", KongPathRegexPrefix, path)))
+		// The second path matches subpaths, including a single slash.
+		if pathIsRoot {
+			// If the path is "/", we don't capture the slash as Kong Route's path has to begin with a slash.
+			// If we captured the slash, we'd generate "(/.*)", and it'd be rejected by Kong.
+			paths = append(paths, lo.ToPtr(fmt.Sprintf("%s/(.*)", KongPathRegexPrefix)))
+		} else {
+			// If the path is not "/", i.e. it has a prefix, we capture the slash to make it possible to
+			// route "/prefix" to "/replacement" and "/prefix/" to "/replacement/" correctly.
+			paths = append(paths, lo.ToPtr(
+				fmt.Sprintf("%s%s(/.*)", KongPathRegexPrefix, path)),
+			)
+		}
+		route.Paths = paths
+	}
+}
+
+// normalizePath normalizes the path by making it:
+// - a slash if it's empty or "/",
+// - a path without trailing slash otherwise.
+func normalizePath(path string) string {
+	if path == "/" || path == "" {
+		return "/"
+	}
+	return strings.TrimSuffix(path, "/")
+}
+
+func isPathRoot(path string) bool {
+	return path == "/"
 }
diff --git a/internal/dataplane/translator/subtranslator/httproute_atc.go b/internal/dataplane/translator/subtranslator/httproute_atc.go
index 03248ece5..ca55ed606 100644
--- a/internal/dataplane/translator/subtranslator/httproute_atc.go
+++ b/internal/dataplane/translator/subtranslator/httproute_atc.go
@@ -47,7 +47,7 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches(
 		return []kongstate.Route{r}, nil
 	}
 
-	_, hasRedirectFilter := lo.Find(translation.Filters, func(filter gatewayapi.HTTPRouteFilter) bool {
+	hasRedirectFilter := lo.ContainsBy(translation.Filters, func(filter gatewayapi.HTTPRouteFilter) bool {
 		return filter.Type == gatewayapi.HTTPRouteFilterRequestRedirect
 	})
 	// if the rule has request redirect filter(s), we need to generate a route for each match to
@@ -66,7 +66,7 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches(
 
 	atc.ApplyExpression(&r.Route, routeMatcher, 1)
 	// generate plugins.
-	if err := SetRoutePlugins(&r, translation.Filters, "", tags); err != nil {
+	if err := SetRoutePlugins(&r, translation.Filters, "", tags, true); err != nil {
 		return nil, err
 	}
 	return []kongstate.Route{r}, nil
@@ -105,7 +105,7 @@ func generateKongExpressionRoutesWithRequestRedirectFilter(
 		if match.Path != nil && match.Path.Value != nil {
 			path = *match.Path.Value
 		}
-		if err := SetRoutePlugins(&matchRoute, translation.Filters, path, tags); err != nil {
+		if err := SetRoutePlugins(&matchRoute, translation.Filters, path, tags, true); err != nil {
 			return nil, err
 		}
 		routes = append(routes, matchRoute)
@@ -594,7 +594,7 @@ func KongExpressionRouteFromHTTPRouteMatchWithPriority(
 			path = *match.Match.Path.Value
 		}
 
-		if err := SetRoutePlugins(r, rule.Filters, path, tags); err != nil {
+		if err := SetRoutePlugins(r, rule.Filters, path, tags, true); err != nil {
 			return nil, err
 		}
 	}
diff --git a/internal/dataplane/translator/translate_httproute.go b/internal/dataplane/translator/translate_httproute.go
index 25c61d9d0..01cb441ce 100644
--- a/internal/dataplane/translator/translate_httproute.go
+++ b/internal/dataplane/translator/translate_httproute.go
@@ -309,7 +309,9 @@ func generateKongRoutesFromHTTPRouteMatches(
 	// stripPath needs to be disabled by default to be conformant with the Gateway API
 	r.StripPath = kong.Bool(false)
 
-	_, hasRedirectFilter := lo.Find(filters, func(filter gatewayapi.HTTPRouteFilter) bool {
+	// Check if the route has a RequestRedirect or URLRewrite with non-nil ReplacePrefixMatch - if it does, we need to
+	// generate a route for each match as the path is used to modify routes and generate plugins.
+	hasRedirectFilter := lo.ContainsBy(filters, func(filter gatewayapi.HTTPRouteFilter) bool {
 		return filter.Type == gatewayapi.HTTPRouteFilterRequestRedirect
 	})
 
@@ -318,9 +320,25 @@ func generateKongRoutesFromHTTPRouteMatches(
 		return nil, err
 	}
 
-	// if the redirect filter has not been set, we still need to set the route plugins
+	var path string
+	if hasURLRewriteWithReplacePrefixMatchFilter := lo.ContainsBy(filters, func(filter gatewayapi.HTTPRouteFilter) bool {
+		return filter.Type == gatewayapi.HTTPRouteFilterURLRewrite &&
+			filter.URLRewrite.Path != nil &&
+			filter.URLRewrite.Path.Type == gatewayapi.PrefixMatchHTTPPathModifier &&
+			filter.URLRewrite.Path.ReplacePrefixMatch != nil
+	}); hasURLRewriteWithReplacePrefixMatchFilter {
+		// In the case of URLRewrite with non-nil ReplacePrefixMatch, we rely on a CEL validation rule that disallows
+		// rules with multiple matches if the URLRewrite filter is present. We can be certain that if the filter is
+		// present, there is at most only one match. Based on that, we can determine the path from the first match.
+		// See: https://github.com/kubernetes-sigs/gateway-api/blob/29e68bffffb9af568e35545305d78d0001a1a0f7/apis/v1/httproute_types.go#L131
+		if len(matches) > 0 && matches[0].Path != nil && matches[0].Path.Value != nil {
+			path = *matches[0].Path.Value
+		}
+	}
+
+	// If the redirect filter has not been set, we still need to set the route plugins.
 	if !hasRedirectFilter {
-		if err := subtranslator.SetRoutePlugins(&r, filters, "", tags); err != nil {
+		if err := subtranslator.SetRoutePlugins(&r, filters, path, tags, false); err != nil {
 			return nil, err
 		}
 		routes = []kongstate.Route{r}
@@ -371,7 +389,7 @@ func getRoutesFromMatches(
 			}
 
 			// generate kong plugins from rule.filters
-			if err := subtranslator.SetRoutePlugins(matchRoute, filters, path, tags); err != nil {
+			if err := subtranslator.SetRoutePlugins(matchRoute, filters, path, tags, false); err != nil {
 				return nil, err
 			}
 
diff --git a/internal/dataplane/translator/translator.go b/internal/dataplane/translator/translator.go
index c817c0222..d1abad4a8 100644
--- a/internal/dataplane/translator/translator.go
+++ b/internal/dataplane/translator/translator.go
@@ -127,6 +127,12 @@ type KongConfigBuildingResult struct {
 	ConfiguredKubernetesObjects []client.Object
 }
 
+// UpdateCache updates the store cache used by the translator.
+// This method can be used to swap the cache with another one (e.g. the last valid snapshot).
+func (t *Translator) UpdateCache(c store.CacheStores) {
+	t.storer.UpdateCache(c)
+}
+
 // BuildKongConfig creates a Kong configuration from Ingress and Custom resources
 // defined in Kubernetes.
 func (t *Translator) BuildKongConfig() KongConfigBuildingResult {
diff --git a/internal/manager/featuregates/feature_gates.go b/internal/manager/featuregates/feature_gates.go
index a1a0f7c5f..237bfcc8c 100644
--- a/internal/manager/featuregates/feature_gates.go
+++ b/internal/manager/featuregates/feature_gates.go
@@ -28,6 +28,10 @@ const (
 	// SanitizeKonnectConfigDumps is the name of the feature-gate that enables sanitization of Konnect config dumps.
 	SanitizeKonnectConfigDumps = "SanitizeKonnectConfigDumps"
 
+	// FallbackConfiguration is the name of the featuer-gate that enables generating fallback configuration in the case
+	// of entity errors returned by the Kong Admin API.
+	FallbackConfiguration = "FallbackConfiguration"
+
 	// DocsURL provides a link to the documentation for feature gates in the KIC repository.
 	DocsURL = "https://github.com/Kong/kubernetes-ingress-controller/blob/main/FEATURE_GATES.md"
 )
@@ -68,5 +72,6 @@ func GetFeatureGatesDefaults() FeatureGates {
 		RewriteURIsFeature:         false,
 		KongServiceFacade:          false,
 		SanitizeKonnectConfigDumps: true,
+		FallbackConfiguration:      false,
 	}
 }
diff --git a/internal/manager/run.go b/internal/manager/run.go
index 32c4cfd9b..ccc8a59d8 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -122,6 +122,7 @@ func Run(
 		EnableReverseSync:          c.EnableReverseSync,
 		ExpressionRoutes:           dpconf.ShouldEnableExpressionRoutes(routerFlavor),
 		SanitizeKonnectConfigDumps: featureGates.Enabled(featuregates.SanitizeKonnectConfigDumps),
+		FallbackConfiguration:      featureGates.Enabled(featuregates.FallbackConfiguration),
 	}
 
 	setupLog.Info("Configuring and building the controller manager")
@@ -177,12 +178,7 @@ func Run(
 	referenceIndexers := ctrlref.NewCacheIndexers(setupLog.WithName("reference-indexers"))
 	cache := store.NewCacheStores()
 	storer := store.New(cache, c.IngressClassName, logger)
-	configTranslator, err := translator.NewTranslator(
-		logger,
-		storer,
-		c.KongWorkspace,
-		translatorFeatureFlags,
-	)
+	configTranslator, err := translator.NewTranslator(logger, storer, c.KongWorkspace, translatorFeatureFlags)
 	if err != nil {
 		return fmt.Errorf("failed to create translator: %w", err)
 	}
diff --git a/internal/store/cache_stores.go b/internal/store/cache_stores.go
index a4483fb2c..79aa7b3db 100644
--- a/internal/store/cache_stores.go
+++ b/internal/store/cache_stores.go
@@ -20,6 +20,8 @@ import (
 	incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
 )
 
+//go:generate go run ../../hack/generators/cache-stores-snapshot
+
 // CacheStores stores cache.Store for all Kinds of k8s objects that
 // the Ingress Controller reads.
 type CacheStores struct {
diff --git a/internal/store/fake_store.go b/internal/store/fake_store.go
index 8df30fd15..2edcaa2bd 100644
--- a/internal/store/fake_store.go
+++ b/internal/store/fake_store.go
@@ -227,7 +227,7 @@ func NewFakeStore(
 		}
 	}
 
-	s = Store{
+	s = &Store{
 		stores: CacheStores{
 			IngressV1:                      ingressV1Store,
 			IngressClassV1:                 ingressClassV1Store,
diff --git a/internal/store/store.go b/internal/store/store.go
index de2b0c987..17c237a49 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -56,6 +56,8 @@ const (
 // Storer is the interface that wraps the required methods to gather information
 // about ingresses, services, secrets and ingress annotations.
 type Storer interface {
+	UpdateCache(cs CacheStores)
+
 	GetSecret(namespace, name string) (*corev1.Secret, error)
 	GetService(namespace, name string) (*corev1.Service, error)
 	GetEndpointSlicesForService(namespace, name string) ([]*discoveryv1.EndpointSlice, error)
@@ -109,11 +111,11 @@ type Store struct {
 	logger logr.Logger
 }
 
-var _ Storer = Store{}
+var _ Storer = &Store{}
 
 // New creates a new object store to be used in the ingress controller.
-func New(cs CacheStores, ingressClass string, logger logr.Logger) Storer {
-	return Store{
+func New(cs CacheStores, ingressClass string, logger logr.Logger) *Store {
+	return &Store{
 		stores:                cs,
 		ingressClass:          ingressClass,
 		ingressClassMatching:  annotations.ExactClassMatch,
@@ -123,6 +125,11 @@ func New(cs CacheStores, ingressClass string, logger logr.Logger) Storer {
 	}
 }
 
+// UpdateCache updates the cache stores in the Store.
+func (s *Store) UpdateCache(cs CacheStores) {
+	s.stores = cs
+}
+
 // GetSecret returns a Secret using the namespace and name as key.
 func (s Store) GetSecret(namespace, name string) (*corev1.Secret, error) {
 	key := fmt.Sprintf("%v/%v", namespace, name)
diff --git a/internal/store/zz_generated_cache_stores_snapshot.go b/internal/store/zz_generated_cache_stores_snapshot.go
new file mode 100644
index 000000000..300f4f22a
--- /dev/null
+++ b/internal/store/zz_generated_cache_stores_snapshot.go
@@ -0,0 +1,61 @@
+// Code generated by hack/generators/cache/snapshot.go; DO NOT EDIT.
+package store
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/cache"
+)
+
+func (c CacheStores) TakeSnapshot() (CacheStores, error) {
+	// Create a fresh CacheStores instance to store the snapshot.
+	snapshot := NewCacheStores()
+
+	// Gather cache store fields for all kinds.
+	allStores := []cache.Store{
+		c.IngressV1,
+		c.IngressClassV1,
+		c.Service,
+		c.Secret,
+		c.EndpointSlice,
+		c.HTTPRoute,
+		c.UDPRoute,
+		c.TCPRoute,
+		c.TLSRoute,
+		c.GRPCRoute,
+		c.ReferenceGrant,
+		c.Gateway,
+		c.Plugin,
+		c.ClusterPlugin,
+		c.Consumer,
+		c.ConsumerGroup,
+		c.KongIngress,
+		c.TCPIngress,
+		c.UDPIngress,
+		c.KongUpstreamPolicy,
+		c.IngressClassParametersV1alpha1,
+		c.KongServiceFacade,
+		c.KongVault,
+	}
+
+	c.l.RLock()
+	defer c.l.RUnlock()
+
+	// Iterate over all stores and add a deep copy of each object to the snapshot.
+	for _, store := range allStores {
+		for _, item := range store.List() {
+			obj, ok := item.(runtime.Object)
+			if !ok {
+				return CacheStores{}, fmt.Errorf("expected runtime.Object, got %T", item)
+			}
+
+			copiedObj := obj.DeepCopyObject()
+			if err := snapshot.Add(copiedObj); err != nil {
+				return CacheStores{}, err
+			}
+		}
+	}
+
+	return snapshot, nil
+}
diff --git a/internal/util/builder/ingress.go b/internal/util/builder/ingress.go
index e9e945c88..b18d7633e 100644
--- a/internal/util/builder/ingress.go
+++ b/internal/util/builder/ingress.go
@@ -1,6 +1,8 @@
 package builder
 
 import (
+	"strings"
+
 	netv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
@@ -65,6 +67,12 @@ func (b *IngressBuilder) WithAnnotations(annotations map[string]string) *Ingress
 	return b
 }
 
+func (b *IngressBuilder) WithKongPlugins(names ...string) *IngressBuilder {
+	return b.WithAnnotations(map[string]string{
+		annotations.AnnotationPrefix + annotations.PluginsKey: strings.Join(names, ","),
+	})
+}
+
 func (b *IngressBuilder) WithDefaultBackend(backend *netv1.IngressBackend) *IngressBuilder {
 	b.ingress.Spec.DefaultBackend = backend
 	return b
diff --git a/internal/util/uuid.go b/internal/util/uuid.go
new file mode 100644
index 000000000..03374b4ba
--- /dev/null
+++ b/internal/util/uuid.go
@@ -0,0 +1,15 @@
+package util
+
+import "github.com/google/uuid"
+
+// UUIDGenerator is an interface to generate UUIDs.
+type UUIDGenerator interface {
+	NewString() string
+}
+
+// DefaultUUIDGenerator is the default implementation of UUIDGenerator.
+type DefaultUUIDGenerator struct{}
+
+func (DefaultUUIDGenerator) NewString() string {
+	return uuid.NewString()
+}
diff --git a/pkg/apis/configuration/v1alpha1/kong_custom_entity_types.go b/pkg/apis/configuration/v1alpha1/kong_custom_entity_types.go
new file mode 100644
index 000000000..1f672091b
--- /dev/null
+++ b/pkg/apis/configuration/v1alpha1/kong_custom_entity_types.go
@@ -0,0 +1,85 @@
+package v1alpha1
+
+import (
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	KongCustomEntityKind = "KongCustomEntity"
+)
+
+type KongEntityScope string
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope=Namespaced,shortName=kce,categories=kong-ingress-controller,path=kongcustomentities,singular=kongcustomentity
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Entity Type",type=string,JSONPath=`.spec.type`,description="type of the Kong entity"
+// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
+// +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=`.status.conditions[?(@.type=="Programmed")].status`
+// +kubebuilder:validation:XValidation:rule="self.spec.type == oldSelf.spec.type",message="The spec.type field is immutable"
+// +kubebuilder:validation:XValidation:rule="!(self.spec.type in ['services','routes','upstreams','targets','plugins','consumers','consumer_groups'])",message="The spec.type field cannot be known Kong entity types"
+
+// KongCustomEntity defines a "custom" Kong entity that KIC cannot support the entity type directly.
+type KongCustomEntity struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec KongCustomEntitySpec `json:"spec"`
+
+	// Status stores the reconciling status of the resource.
+	Status KongCustomEntityStatus `json:"status,omitempty"`
+}
+
+type KongCustomEntitySpec struct {
+	// EntityType is the type of the Kong entity. The type is used in generating declarative configuration.
+	EntityType string `json:"type"`
+	// Fields defines the fields of the Kong entity itself.
+	Fields apiextensionsv1.JSON `json:"fields"`
+	// ControllerName specifies the controller that should reconcile it, like ingress class.
+	ControllerName string `json:"controllerName"`
+
+	// ParentRef references the kubernetes resource it attached to when its scope is "attached".
+	// Currently only KongPlugin/KongClusterPlugin allowed. This will make the custom entity to be attached
+	// to the entity(service/route/consumer) where the plugin is attached.
+	ParentRef *ObjectReference `json:"parentRef,omitempty"`
+}
+
+// ObjectReference defines reference of a kubernetes object.
+type ObjectReference struct {
+	Group *string `json:"group,omitempty"`
+	Kind  *string `json:"kind,omitempty"`
+	// Empty namespace means the same namespace of the owning object.
+	Namespace *string `json:"namespace,omitempty"`
+	Name      string  `json:"name"`
+}
+
+type KongCustomEntityStatus struct {
+	// Conditions describe the current conditions of the KongCustomEntityStatus.
+	//
+	// Known condition types are:
+	//
+	// * "Programmed"
+	//
+	// +listType=map
+	// +listMapKey=type
+	// +kubebuilder:validation:MaxItems=8
+	// +kubebuilder:default={{type: "Programmed", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}
+	Conditions []metav1.Condition `json:"conditions"`
+}
+
+// +kubebuilder:object:root=true
+
+// KongCustomEntityList contains a list of KongCustomEntity.
+type KongCustomEntityList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []KongCustomEntity `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&KongCustomEntity{}, &KongCustomEntityList{})
+}
diff --git a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
index 05997c29c..a3ec7c9de 100644
--- a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
@@ -128,6 +128,108 @@ func (in *IngressClassParametersSpec) DeepCopy() *IngressClassParametersSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KongCustomEntity) DeepCopyInto(out *KongCustomEntity) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KongCustomEntity.
+func (in *KongCustomEntity) DeepCopy() *KongCustomEntity {
+	if in == nil {
+		return nil
+	}
+	out := new(KongCustomEntity)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KongCustomEntity) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KongCustomEntityList) DeepCopyInto(out *KongCustomEntityList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]KongCustomEntity, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KongCustomEntityList.
+func (in *KongCustomEntityList) DeepCopy() *KongCustomEntityList {
+	if in == nil {
+		return nil
+	}
+	out := new(KongCustomEntityList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KongCustomEntityList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KongCustomEntitySpec) DeepCopyInto(out *KongCustomEntitySpec) {
+	*out = *in
+	in.Fields.DeepCopyInto(&out.Fields)
+	if in.ParentRef != nil {
+		in, out := &in.ParentRef, &out.ParentRef
+		*out = new(ObjectReference)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KongCustomEntitySpec.
+func (in *KongCustomEntitySpec) DeepCopy() *KongCustomEntitySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(KongCustomEntitySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KongCustomEntityStatus) DeepCopyInto(out *KongCustomEntityStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KongCustomEntityStatus.
+func (in *KongCustomEntityStatus) DeepCopy() *KongCustomEntityStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(KongCustomEntityStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *KongLicense) DeepCopyInto(out *KongLicense) {
 	*out = *in
@@ -331,3 +433,33 @@ func (in *KongVaultStatus) DeepCopy() *KongVaultStatus {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ObjectReference) DeepCopyInto(out *ObjectReference) {
+	*out = *in
+	if in.Group != nil {
+		in, out := &in.Group, &out.Group
+		*out = new(string)
+		**out = **in
+	}
+	if in.Kind != nil {
+		in, out := &in.Kind, &out.Kind
+		*out = new(string)
+		**out = **in
+	}
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference.
+func (in *ObjectReference) DeepCopy() *ObjectReference {
+	if in == nil {
+		return nil
+	}
+	out := new(ObjectReference)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/clientset/typed/configuration/v1alpha1/configuration_client.go b/pkg/clientset/typed/configuration/v1alpha1/configuration_client.go
index 63b28f3aa..440b7a55e 100644
--- a/pkg/clientset/typed/configuration/v1alpha1/configuration_client.go
+++ b/pkg/clientset/typed/configuration/v1alpha1/configuration_client.go
@@ -29,6 +29,7 @@ import (
 type ConfigurationV1alpha1Interface interface {
 	RESTClient() rest.Interface
 	IngressClassParametersesGetter
+	KongCustomEntitiesGetter
 	KongLicensesGetter
 	KongVaultsGetter
 }
@@ -42,6 +43,10 @@ func (c *ConfigurationV1alpha1Client) IngressClassParameterses(namespace string)
 	return newIngressClassParameterses(c, namespace)
 }
 
+func (c *ConfigurationV1alpha1Client) KongCustomEntities(namespace string) KongCustomEntityInterface {
+	return newKongCustomEntities(c, namespace)
+}
+
 func (c *ConfigurationV1alpha1Client) KongLicenses() KongLicenseInterface {
 	return newKongLicenses(c)
 }
diff --git a/pkg/clientset/typed/configuration/v1alpha1/fake/fake_configuration_client.go b/pkg/clientset/typed/configuration/v1alpha1/fake/fake_configuration_client.go
index 6a7de0703..be08b5cd2 100644
--- a/pkg/clientset/typed/configuration/v1alpha1/fake/fake_configuration_client.go
+++ b/pkg/clientset/typed/configuration/v1alpha1/fake/fake_configuration_client.go
@@ -32,6 +32,10 @@ func (c *FakeConfigurationV1alpha1) IngressClassParameterses(namespace string) v
 	return &FakeIngressClassParameterses{c, namespace}
 }
 
+func (c *FakeConfigurationV1alpha1) KongCustomEntities(namespace string) v1alpha1.KongCustomEntityInterface {
+	return &FakeKongCustomEntities{c, namespace}
+}
+
 func (c *FakeConfigurationV1alpha1) KongLicenses() v1alpha1.KongLicenseInterface {
 	return &FakeKongLicenses{c}
 }
diff --git a/pkg/clientset/typed/configuration/v1alpha1/fake/fake_kongcustomentity.go b/pkg/clientset/typed/configuration/v1alpha1/fake/fake_kongcustomentity.go
new file mode 100644
index 000000000..61b69d7fb
--- /dev/null
+++ b/pkg/clientset/typed/configuration/v1alpha1/fake/fake_kongcustomentity.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2021 Kong, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeKongCustomEntities implements KongCustomEntityInterface
+type FakeKongCustomEntities struct {
+	Fake *FakeConfigurationV1alpha1
+	ns   string
+}
+
+var kongcustomentitiesResource = v1alpha1.SchemeGroupVersion.WithResource("kongcustomentities")
+
+var kongcustomentitiesKind = v1alpha1.SchemeGroupVersion.WithKind("KongCustomEntity")
+
+// Get takes name of the kongCustomEntity, and returns the corresponding kongCustomEntity object, and an error if there is any.
+func (c *FakeKongCustomEntities) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(kongcustomentitiesResource, c.ns, name), &v1alpha1.KongCustomEntity{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.KongCustomEntity), err
+}
+
+// List takes label and field selectors, and returns the list of KongCustomEntities that match those selectors.
+func (c *FakeKongCustomEntities) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.KongCustomEntityList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(kongcustomentitiesResource, kongcustomentitiesKind, c.ns, opts), &v1alpha1.KongCustomEntityList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.KongCustomEntityList{ListMeta: obj.(*v1alpha1.KongCustomEntityList).ListMeta}
+	for _, item := range obj.(*v1alpha1.KongCustomEntityList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested kongCustomEntities.
+func (c *FakeKongCustomEntities) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(kongcustomentitiesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a kongCustomEntity and creates it.  Returns the server's representation of the kongCustomEntity, and an error, if there is any.
+func (c *FakeKongCustomEntities) Create(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.CreateOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(kongcustomentitiesResource, c.ns, kongCustomEntity), &v1alpha1.KongCustomEntity{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.KongCustomEntity), err
+}
+
+// Update takes the representation of a kongCustomEntity and updates it. Returns the server's representation of the kongCustomEntity, and an error, if there is any.
+func (c *FakeKongCustomEntities) Update(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(kongcustomentitiesResource, c.ns, kongCustomEntity), &v1alpha1.KongCustomEntity{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.KongCustomEntity), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeKongCustomEntities) UpdateStatus(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (*v1alpha1.KongCustomEntity, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(kongcustomentitiesResource, "status", c.ns, kongCustomEntity), &v1alpha1.KongCustomEntity{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.KongCustomEntity), err
+}
+
+// Delete takes name of the kongCustomEntity and deletes it. Returns an error if one occurs.
+func (c *FakeKongCustomEntities) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteActionWithOptions(kongcustomentitiesResource, c.ns, name, opts), &v1alpha1.KongCustomEntity{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeKongCustomEntities) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(kongcustomentitiesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.KongCustomEntityList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched kongCustomEntity.
+func (c *FakeKongCustomEntities) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.KongCustomEntity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(kongcustomentitiesResource, c.ns, name, pt, data, subresources...), &v1alpha1.KongCustomEntity{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.KongCustomEntity), err
+}
diff --git a/pkg/clientset/typed/configuration/v1alpha1/generated_expansion.go b/pkg/clientset/typed/configuration/v1alpha1/generated_expansion.go
index bab2185b7..a05eae8a7 100644
--- a/pkg/clientset/typed/configuration/v1alpha1/generated_expansion.go
+++ b/pkg/clientset/typed/configuration/v1alpha1/generated_expansion.go
@@ -20,6 +20,8 @@ package v1alpha1
 
 type IngressClassParametersExpansion interface{}
 
+type KongCustomEntityExpansion interface{}
+
 type KongLicenseExpansion interface{}
 
 type KongVaultExpansion interface{}
diff --git a/pkg/clientset/typed/configuration/v1alpha1/kongcustomentity.go b/pkg/clientset/typed/configuration/v1alpha1/kongcustomentity.go
new file mode 100644
index 000000000..6401393d5
--- /dev/null
+++ b/pkg/clientset/typed/configuration/v1alpha1/kongcustomentity.go
@@ -0,0 +1,195 @@
+/*
+Copyright 2021 Kong, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+	scheme "github.com/kong/kubernetes-ingress-controller/v3/pkg/clientset/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// KongCustomEntitiesGetter has a method to return a KongCustomEntityInterface.
+// A group's client should implement this interface.
+type KongCustomEntitiesGetter interface {
+	KongCustomEntities(namespace string) KongCustomEntityInterface
+}
+
+// KongCustomEntityInterface has methods to work with KongCustomEntity resources.
+type KongCustomEntityInterface interface {
+	Create(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.CreateOptions) (*v1alpha1.KongCustomEntity, error)
+	Update(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (*v1alpha1.KongCustomEntity, error)
+	UpdateStatus(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (*v1alpha1.KongCustomEntity, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.KongCustomEntity, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.KongCustomEntityList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.KongCustomEntity, err error)
+	KongCustomEntityExpansion
+}
+
+// kongCustomEntities implements KongCustomEntityInterface
+type kongCustomEntities struct {
+	client rest.Interface
+	ns     string
+}
+
+// newKongCustomEntities returns a KongCustomEntities
+func newKongCustomEntities(c *ConfigurationV1alpha1Client, namespace string) *kongCustomEntities {
+	return &kongCustomEntities{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the kongCustomEntity, and returns the corresponding kongCustomEntity object, and an error if there is any.
+func (c *kongCustomEntities) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	result = &v1alpha1.KongCustomEntity{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of KongCustomEntities that match those selectors.
+func (c *kongCustomEntities) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.KongCustomEntityList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.KongCustomEntityList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested kongCustomEntities.
+func (c *kongCustomEntities) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a kongCustomEntity and creates it.  Returns the server's representation of the kongCustomEntity, and an error, if there is any.
+func (c *kongCustomEntities) Create(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.CreateOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	result = &v1alpha1.KongCustomEntity{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(kongCustomEntity).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a kongCustomEntity and updates it. Returns the server's representation of the kongCustomEntity, and an error, if there is any.
+func (c *kongCustomEntities) Update(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	result = &v1alpha1.KongCustomEntity{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		Name(kongCustomEntity.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(kongCustomEntity).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *kongCustomEntities) UpdateStatus(ctx context.Context, kongCustomEntity *v1alpha1.KongCustomEntity, opts v1.UpdateOptions) (result *v1alpha1.KongCustomEntity, err error) {
+	result = &v1alpha1.KongCustomEntity{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		Name(kongCustomEntity.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(kongCustomEntity).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the kongCustomEntity and deletes it. Returns an error if one occurs.
+func (c *kongCustomEntities) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *kongCustomEntities) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched kongCustomEntity.
+func (c *kongCustomEntities) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.KongCustomEntity, err error) {
+	result = &v1alpha1.KongCustomEntity{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("kongcustomentities").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/third_party/go.mod b/third_party/go.mod
index 22d23b108..2f5e1b90c 100644
--- a/third_party/go.mod
+++ b/third_party/go.mod
@@ -1,6 +1,6 @@
 module github.com/kong/kubernetes-ingress-controller/tools
 
-go 1.22.2
+go 1.22
 
 require (
 	github.com/go-delve/delve v1.22.1


Test patch:
diff --git a/.github/test_dependencies.yaml b/.github/test_dependencies.yaml
index 6d4e412b8..6df6298c7 100644
--- a/.github/test_dependencies.yaml
+++ b/.github/test_dependencies.yaml
@@ -12,7 +12,7 @@ e2e:
     - 'v1.25.16'
   gke:
     # renovate: datasource=custom.gke-rapid depName=gke versioning=semver
-    - '1.29.4'
+    - '1.30.0'
 
   # For Istio, we define combinations of Kind and Istio versions that will be
   # used directly in the test matrix `include` section.
diff --git a/.github/workflows/_integration_tests.yaml b/.github/workflows/_integration_tests.yaml
index b170a16c3..4b49fedb9 100644
--- a/.github/workflows/_integration_tests.yaml
+++ b/.github/workflows/_integration_tests.yaml
@@ -122,10 +122,12 @@ jobs:
           # Set enterprise to 'true' to enable isolated integration test cases requiring enterprise features.
           - name: isolated-dbless
             test: isolated.dbless
+            router-flavor: traditional
             feature_gates: "GatewayAlpha=true"
             enterprise: true
           - name: isolated-postgres
             test: isolated.postgres
+            router-flavor: traditional
             feature_gates: "GatewayAlpha=true"
             enterprise: true
           - name: isolated-dbless-expression-router
diff --git a/internal/admission/handler_test.go b/internal/admission/handler_test.go
index 6c28955c0..d0e1dd3f6 100644
--- a/internal/admission/handler_test.go
+++ b/internal/admission/handler_test.go
@@ -16,6 +16,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	k8stypes "k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	ctrlref "github.com/kong/kubernetes-ingress-controller/v3/internal/controllers/reference"
@@ -109,7 +110,10 @@ func TestHandleService(t *testing.T) {
 	tests := []struct {
 		name         string
 		resource     corev1.Service
+		validator    KongHTTPValidator
 		wantWarnings []string
+		wantMessage  string
+		isAllowed    bool
 	}{
 		{
 			name: "has kongingress annotation",
@@ -121,10 +125,12 @@ func TestHandleService(t *testing.T) {
 					},
 				},
 			},
+
 			wantWarnings: []string{
 				fmt.Sprintf(serviceWarning, annotations.AnnotationPrefix+annotations.ConfigurationKey,
 					kongv1beta1.KongUpstreamPolicyAnnotationKey),
 			},
+			isAllowed: true,
 		},
 		{
 			name: "has upstream policy annotation",
@@ -136,13 +142,97 @@ func TestHandleService(t *testing.T) {
 					},
 				},
 			},
-			wantWarnings: nil,
+			isAllowed: true,
+		},
+		{
+			name: "fails when many plugins of the same type are attached",
+			resource: corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: "default",
+					Annotations: map[string]string{
+						annotations.AnnotationPrefix + annotations.PluginsKey: "plugin1, plugin2, plugin3",
+					},
+				},
+			},
+			validator: KongHTTPValidator{
+				ManagerClient: func() client.Client {
+					scheme := runtime.NewScheme()
+					require.NoError(t, kongv1.AddToScheme(scheme))
+					fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+						&kongv1.KongPlugin{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "plugin1",
+								Namespace: "default",
+							},
+							PluginName: "foo",
+						},
+						&kongv1.KongPlugin{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "plugin2",
+								Namespace: "default",
+							},
+							PluginName: "bar",
+						},
+						&kongv1.KongPlugin{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "plugin3",
+								Namespace: "default",
+							},
+							PluginName: "foo",
+						},
+					).Build()
+					return fakeClient
+				}(),
+			},
+			isAllowed:   false,
+			wantMessage: "Service has invalid KongPlugin annotation: cannot attach multiple plugins: plugin1, plugin3 of the same type foo",
+		},
+		{
+			name: "pass when many valid plugins are attached",
+			resource: corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: "default",
+					Annotations: map[string]string{
+						annotations.AnnotationPrefix + annotations.PluginsKey:       "plugin1, plugin2, plugin3",
+						annotations.AnnotationPrefix + annotations.ConfigurationKey: "test",
+					},
+				},
+			},
+			validator: KongHTTPValidator{
+				ManagerClient: func() client.Client {
+					scheme := runtime.NewScheme()
+					require.NoError(t, kongv1.AddToScheme(scheme))
+					fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+						&kongv1.KongPlugin{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "plugin1",
+								Namespace: "default",
+							},
+							PluginName: "foo",
+						},
+						&kongv1.KongPlugin{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "plugin2",
+								Namespace: "default",
+							},
+							PluginName: "bar",
+						},
+					).Build()
+					return fakeClient
+				}(),
+			},
+			isAllowed: true,
+			wantWarnings: []string{
+				fmt.Sprintf(serviceWarning, annotations.AnnotationPrefix+annotations.ConfigurationKey,
+					kongv1beta1.KongUpstreamPolicyAnnotationKey),
+			},
 		},
 	}
 	for _, tt := range tests {
 		resource := tt.resource
 		t.Run(tt.name, func(t *testing.T) {
-			validator := KongHTTPValidator{}
 			raw, err := json.Marshal(tt.resource)
 			require.NoError(t, err)
 			request := admissionv1.AdmissionRequest{
@@ -152,7 +242,7 @@ func TestHandleService(t *testing.T) {
 				},
 			}
 			handler := RequestHandler{
-				Validator: validator,
+				Validator: tt.validator,
 				Logger:    logr.Discard(),
 			}
 
@@ -160,8 +250,9 @@ func TestHandleService(t *testing.T) {
 
 			got, err := handler.handleService(context.Background(), request, responseBuilder)
 			require.NoError(t, err)
-			require.True(t, got.Allowed)
+			require.Equal(t, tt.isAllowed, got.Allowed)
 			require.Equal(t, tt.wantWarnings, got.Warnings)
+			require.Equal(t, tt.wantMessage, got.Result.Message)
 		})
 	}
 }
diff --git a/internal/admission/server_test.go b/internal/admission/server_test.go
index e83593120..01afbf419 100644
--- a/internal/admission/server_test.go
+++ b/internal/admission/server_test.go
@@ -82,6 +82,10 @@ func (v KongFakeValidator) ValidateVault(_ context.Context, _ kongv1alpha1.KongV
 	return v.Result, v.Message, v.Error
 }
 
+func (v KongFakeValidator) ValidateService(_ context.Context, _ corev1.Service) (bool, string, error) {
+	return v.Result, v.Message, v.Error
+}
+
 func TestServeHTTPBasic(t *testing.T) {
 	assert := assert.New(t)
 	res := httptest.NewRecorder()
diff --git a/internal/admission/validation/gateway/httproute_test.go b/internal/admission/validation/gateway/httproute_test.go
index 9d8cc60cc..a52342c4b 100644
--- a/internal/admission/validation/gateway/httproute_test.go
+++ b/internal/admission/validation/gateway/httproute_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/manager/scheme"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 )
 
 func TestValidateHTTPRoute(t *testing.T) {
@@ -78,7 +79,7 @@ func TestValidateHTTPRoute(t *testing.T) {
 			valid: true,
 		},
 		{
-			msg: "route with parentRef to non-gateway object is accepted with no vlaidation",
+			msg: "route with parentRef to non-gateway object is accepted with no validation",
 			route: &gatewayapi.HTTPRoute{
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: corev1.NamespaceDefault,
@@ -161,6 +162,128 @@ func TestValidateHTTPRoute(t *testing.T) {
 			},
 			valid: true,
 		},
+		{
+			msg: "valid HTTPRoute with multiple plugins of the same type attached - does not pass validation",
+			route: &gatewayapi.HTTPRoute{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: corev1.NamespaceDefault,
+					Name:      "testing-httproute",
+					Annotations: map[string]string{
+						"konghq.com/plugins": "plugin1,plugin2,plugin3",
+					},
+				},
+				Spec: gatewayapi.HTTPRouteSpec{
+					CommonRouteSpec: gatewayapi.CommonRouteSpec{
+						ParentRefs: []gatewayapi.ParentReference{{
+							Name: "testing-gateway",
+						}},
+					},
+				},
+			},
+			cachedObjects: []client.Object{
+				gatewayClass,
+				&gatewayapi.Gateway{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: corev1.NamespaceDefault,
+						Name:      "testing-gateway",
+					},
+					Spec: gatewayapi.GatewaySpec{
+						GatewayClassName: gatewayClassName,
+						Listeners: []gatewayapi.Listener{{
+							Name:     "http",
+							Port:     80,
+							Protocol: (gatewayapi.HTTPProtocolType),
+							AllowedRoutes: &gatewayapi.AllowedRoutes{
+								Kinds: []gatewayapi.RouteGroupKind{{
+									Group: &group,
+									Kind:  "HTTPRoute",
+								}},
+							},
+						}},
+					},
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "bar",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin3",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+			},
+			valid:         false,
+			validationMsg: "HTTPRoute has invalid KongPlugin annotation: cannot attach multiple plugins: plugin1, plugin3 of the same type foo",
+		},
+		{
+			msg: "valid HTTPRoute with multiple plugins of different types and one no-existing attached - passes validation",
+			route: &gatewayapi.HTTPRoute{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: corev1.NamespaceDefault,
+					Name:      "testing-httproute",
+					Annotations: map[string]string{
+						"konghq.com/plugins": "plugin1,plugin2,plugin3",
+					},
+				},
+				Spec: gatewayapi.HTTPRouteSpec{
+					CommonRouteSpec: gatewayapi.CommonRouteSpec{
+						ParentRefs: []gatewayapi.ParentReference{{
+							Name: "testing-gateway",
+						}},
+					},
+				},
+			},
+			cachedObjects: []client.Object{
+				gatewayClass,
+				&gatewayapi.Gateway{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: corev1.NamespaceDefault,
+						Name:      "testing-gateway",
+					},
+					Spec: gatewayapi.GatewaySpec{
+						GatewayClassName: gatewayClassName,
+						Listeners: []gatewayapi.Listener{{
+							Name:     "http",
+							Port:     80,
+							Protocol: (gatewayapi.HTTPProtocolType),
+							AllowedRoutes: &gatewayapi.AllowedRoutes{
+								Kinds: []gatewayapi.RouteGroupKind{{
+									Group: &group,
+									Kind:  "HTTPRoute",
+								}},
+							},
+						}},
+					},
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "bar",
+				},
+			},
+			valid: true,
+		},
 		{
 			msg: "if an HTTPRoute is using queryparams matching it fails validation due to only supporting expression router",
 			route: &gatewayapi.HTTPRoute{
@@ -828,8 +951,7 @@ func TestValidateHTTPRoute(t *testing.T) {
 					},
 				},
 			},
-			valid:         false,
-			validationMsg: "HTTPRoute spec did not pass validation: rules[0].filters[0]: filter type URLRewrite (with path replace type ReplacePrefixMatch) is unsupported",
+			valid: true,
 		},
 	} {
 		t.Run(tt.msg, func(t *testing.T) {
diff --git a/internal/admission/validation/ingress/ingress_test.go b/internal/admission/validation/ingress/ingress_test.go
index a6986eba1..aee88a2b1 100644
--- a/internal/admission/validation/ingress/ingress_test.go
+++ b/internal/admission/validation/ingress/ingress_test.go
@@ -12,6 +12,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	netv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator"
@@ -56,6 +57,7 @@ func TestValidateIngress(t *testing.T) {
 				tt.ingress,
 				logger,
 				fakestore,
+				fake.NewFakeClient(),
 			)
 			assert.Equal(t, tt.valid, valid, tt.msg)
 			assert.Equal(t, tt.validationMsg, validMsg, tt.msg)
diff --git a/internal/admission/validator_test.go b/internal/admission/validator_test.go
index 56499efca..95aac2362 100644
--- a/internal/admission/validator_test.go
+++ b/internal/admission/validator_test.go
@@ -19,6 +19,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	testk8sclient "k8s.io/client-go/kubernetes/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
@@ -566,6 +567,107 @@ func TestKongHTTPValidator_ValidateConsumer(t *testing.T) {
 		require.False(t, valid)
 		require.Equal(t, ErrTextConsumerExists, errText)
 	})
+
+	t.Run("fails when many plugins of the same type are attached", func(t *testing.T) {
+		s, err := store.NewFakeStore(store.FakeObjects{})
+		require.NoError(t, err)
+		const cfgNamespace = "default"
+		scheme := runtime.NewScheme()
+		require.NoError(t, kongv1.AddToScheme(scheme))
+		fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+			&kongv1.KongPlugin{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "plugin1",
+					Namespace: cfgNamespace,
+				},
+				PluginName: "foo",
+			},
+			&kongv1.KongPlugin{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "plugin2",
+					Namespace: cfgNamespace,
+				},
+				PluginName: "bar",
+			},
+			&kongv1.KongPlugin{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "plugin3",
+					Namespace: cfgNamespace,
+				},
+				PluginName: "foo",
+			},
+		).Build()
+		validator := KongHTTPValidator{
+			SecretGetter:  s,
+			ManagerClient: fakeClient,
+			AdminAPIServicesProvider: fakeServicesProvider{
+				consumerSvc: fakeConsumersSvc{},
+			},
+			ingressClassMatcher: fakeClassMatcher,
+		}
+
+		valid, errText, err := validator.ValidateConsumer(context.Background(), kongv1.KongConsumer{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					annotations.AnnotationPrefix + annotations.PluginsKey: "plugin1,plugin2,plugin3",
+				},
+				Namespace: cfgNamespace,
+			},
+			Username: "username",
+		})
+		require.NoError(t, err)
+		require.False(t, valid)
+		require.Equal(
+			t,
+			"KongConsumer has invalid KongPlugin annotation: cannot attach multiple plugins: plugin1, plugin3 of the same type foo",
+			errText,
+		)
+	})
+
+	t.Run("pass when different valid plugins and one no existing are attached", func(t *testing.T) {
+		s, err := store.NewFakeStore(store.FakeObjects{})
+		require.NoError(t, err)
+		const cfgNamespace = "default"
+		scheme := runtime.NewScheme()
+		require.NoError(t, kongv1.AddToScheme(scheme))
+		fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+			&kongv1.KongPlugin{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "plugin1",
+					Namespace: cfgNamespace,
+				},
+				PluginName: "foo",
+			},
+			&kongv1.KongPlugin{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "plugin2",
+					Namespace: cfgNamespace,
+				},
+				PluginName: "bar",
+			},
+		).Build()
+		validator := KongHTTPValidator{
+			SecretGetter:  s,
+			ManagerClient: fakeClient,
+			AdminAPIServicesProvider: fakeServicesProvider{
+				consumerSvc: fakeConsumersSvc{},
+			},
+			ingressClassMatcher: fakeClassMatcher,
+		}
+
+		valid, errText, err := validator.ValidateConsumer(context.Background(), kongv1.KongConsumer{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{
+					annotations.AnnotationPrefix + annotations.PluginsKey: "plugin1,plugin2,plugin3",
+				},
+				Namespace: cfgNamespace,
+			},
+			Username: "username",
+		})
+		require.NoError(t, err)
+		require.True(t, valid)
+		require.Empty(t, errText)
+	})
 }
 
 type fakeConsumerGroupSvc struct {
@@ -599,13 +701,14 @@ func TestKongHTTPValidator_ValidateConsumerGroup(t *testing.T) {
 		cg kongv1beta1.KongConsumerGroup
 	}
 	tests := []struct {
-		name             string
-		ConsumerGroupSvc kong.AbstractConsumerGroupService
-		InfoSvc          kong.AbstractInfoService
-		args             args
-		wantOK           bool
-		wantMessage      string
-		wantErr          bool
+		name                 string
+		ConsumerGroupSvc     kong.AbstractConsumerGroupService
+		InfoSvc              kong.AbstractInfoService
+		ManagerClientObjects []client.Object
+		args                 args
+		wantOK               bool
+		wantMessage          string
+		wantErr              bool
 	}{
 		{
 			name:             "Enterprise version",
@@ -618,6 +721,79 @@ func TestKongHTTPValidator_ValidateConsumerGroup(t *testing.T) {
 			wantMessage: "",
 			wantErr:     false,
 		},
+		{
+			name:             "Enterprise Kong Gateway and KongConsumerGroup with multiple plugins of the same type attached",
+			ConsumerGroupSvc: &fakeConsumerGroupSvc{err: nil},
+			InfoSvc:          &fakeInfoSvc{version: "3.4.1.0"},
+			ManagerClientObjects: []client.Object{
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "bar",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin3",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+			},
+			args: args{
+				cg: kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "plugin1,plugin2,plugin3",
+						},
+					},
+				},
+			},
+			wantOK:      false,
+			wantMessage: "KongConsumerGroup has invalid KongPlugin annotation: cannot attach multiple plugins: plugin1, plugin3 of the same type foo",
+			wantErr:     false,
+		},
+		{
+			name:             "Enterprise Kong Gateway and KongConsumerGroup with plugins of the different types and non-existing one attached",
+			ConsumerGroupSvc: &fakeConsumerGroupSvc{err: nil},
+			InfoSvc:          &fakeInfoSvc{version: "3.4.1.0"},
+			ManagerClientObjects: []client.Object{
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "bar",
+				},
+			},
+			args: args{
+				cg: kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "plugin1,plugin2,plugin3",
+						},
+					},
+				},
+			},
+			wantOK:      true,
+			wantMessage: "",
+			wantErr:     false,
+		},
 		{
 			name:             "OSS version",
 			ConsumerGroupSvc: &fakeConsumerGroupSvc{err: nil},
@@ -685,10 +861,14 @@ func TestKongHTTPValidator_ValidateConsumerGroup(t *testing.T) {
 			wantErr:     false,
 		},
 	}
+
+	scheme := runtime.NewScheme()
+	require.NoError(t, kongv1.AddToScheme(scheme))
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			validator := KongHTTPValidator{
-				SecretGetter: store,
+				ManagerClient: fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.ManagerClientObjects...).Build(),
+				SecretGetter:  store,
 				AdminAPIServicesProvider: fakeServicesProvider{
 					infoSvc:          tt.InfoSvc,
 					consumerGroupSvc: tt.ConsumerGroupSvc,
@@ -844,12 +1024,11 @@ func (f fakeConsumerGetter) ListAllConsumers(context.Context) ([]kongv1.KongCons
 
 func TestValidator_ValidateIngress(t *testing.T) {
 	const testSvcFacadeName = "svc-facade"
-	s := lo.Must(managerscheme.Get())
-	b := fake.NewClientBuilder().WithScheme(s)
 
 	testCases := []struct {
 		name                          string
 		storerObjects                 store.FakeObjects
+		clientObjects                 []client.Object
 		kongRouteValidationShouldFail bool
 		translatorFeatures            translator.FeatureFlags
 		ingress                       *netv1.Ingress
@@ -872,7 +1051,6 @@ func TestValidator_ValidateIngress(t *testing.T) {
 				).
 				Build(),
 			kongRouteValidationShouldFail: true, // Despite the route validation failing, the ingress class is not kong, so it's ok.
-			storerObjects:                 store.FakeObjects{},
 			wantOK:                        true,
 		},
 		{
@@ -890,8 +1068,83 @@ func TestValidator_ValidateIngress(t *testing.T) {
 					}),
 				).
 				Build(),
-			storerObjects: store.FakeObjects{},
-			wantOK:        true,
+			wantOK: true,
+		},
+		{
+			name: "valid Ingress fails when many plugins of the same type are attached",
+			ingress: builder.NewIngress("ingress", "kong").
+				WithNamespace("default").
+				WithKongPlugins("plugin1", "plugin2", "plugin3").
+				WithRules(
+					newHTTPIngressRule(netv1.IngressBackend{
+						Service: &netv1.IngressServiceBackend{
+							Name: "svc",
+							Port: netv1.ServiceBackendPort{
+								Number: 8080,
+							},
+						},
+					}),
+				).
+				Build(),
+			clientObjects: []client.Object{
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "default",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin3",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+			},
+			wantOK:      false,
+			wantMessage: "Ingress has invalid KongPlugin annotation: cannot attach multiple plugins: plugin1, plugin3 of the same type foo",
+		},
+		{
+			name: "valid Ingress with different valid plugins and one no existing are attached passes",
+			ingress: builder.NewIngress("ingress", "kong").
+				WithNamespace("default").
+				WithKongPlugins("plugin1", "plugin2", "plugin3").
+				WithRules(
+					newHTTPIngressRule(netv1.IngressBackend{
+						Service: &netv1.IngressServiceBackend{
+							Name: "svc",
+							Port: netv1.ServiceBackendPort{
+								Number: 8080,
+							},
+						},
+					}),
+				).
+				Build(),
+			clientObjects: []client.Object{
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin1",
+						Namespace: "default",
+					},
+					PluginName: "foo",
+				},
+				&kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "plugin2",
+						Namespace: "default",
+					},
+					PluginName: "bar",
+				},
+			},
+			wantOK: true,
 		},
 		{
 			name: "invalid with Service backend",
@@ -909,7 +1162,6 @@ func TestValidator_ValidateIngress(t *testing.T) {
 				).
 				Build(),
 			kongRouteValidationShouldFail: true,
-			storerObjects:                 store.FakeObjects{},
 			wantOK:                        false,
 			wantMessage:                   "Ingress failed schema validation: something is wrong with the route",
 		},
@@ -995,7 +1247,7 @@ func TestValidator_ValidateIngress(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			storer := lo.Must(store.NewFakeStore(tc.storerObjects))
 			validator := KongHTTPValidator{
-				ManagerClient: b.Build(),
+				ManagerClient: fake.NewClientBuilder().WithScheme(lo.Must(managerscheme.Get())).WithObjects(tc.clientObjects...).Build(),
 				Storer:        storer,
 				AdminAPIServicesProvider: fakeServicesProvider{
 					routeSvc: &fakeRouteSvc{
diff --git a/internal/annotations/annotations_test.go b/internal/annotations/annotations_test.go
index ea68a9e56..f283c7a48 100644
--- a/internal/annotations/annotations_test.go
+++ b/internal/annotations/annotations_test.go
@@ -845,6 +845,15 @@ func TestExtractHeaders(t *testing.T) {
 			name: "empty",
 			want: map[string][]string{},
 		},
+		{
+			name: "empty with custom separator",
+			args: args{
+				anns: map[string]string{
+					"konghq.com/headers-separator": ";",
+				},
+			},
+			want: map[string][]string{},
+		},
 		{
 			name: "non-empty",
 			args: args{
@@ -863,6 +872,15 @@ func TestExtractHeaders(t *testing.T) {
 			},
 			want: map[string][]string{},
 		},
+		{
+			name: "separator with no header results in empty header value",
+			args: args{
+				anns: map[string]string{
+					"konghq.com/headers.foo": "foo,",
+				},
+			},
+			want: map[string][]string{"foo": {"foo", ""}},
+		},
 		{
 			name: "no header name",
 			args: args{
@@ -872,6 +890,47 @@ func TestExtractHeaders(t *testing.T) {
 			},
 			want: map[string][]string{},
 		},
+		{
+			name: "multiple header, multiple values, trailing spaces",
+			args: args{
+				anns: map[string]string{
+					"konghq.com/headers.x-example":    "foo, bar, baz  ",
+					"konghq.com/headers.x-additional": "foo",
+				},
+			},
+			want: map[string][]string{
+				"x-example":    {"foo", "bar", "baz"},
+				"x-additional": {"foo"},
+			},
+		},
+		{
+			name: "multiple header, multiple values, custom separator",
+			args: args{
+				anns: map[string]string{
+					"konghq.com/headers-separator":    ";",
+					"konghq.com/headers.x-example":    "foo, bar;baz",
+					"konghq.com/headers.x-additional": "foo",
+				},
+			},
+			want: map[string][]string{
+				"x-example":    {"foo, bar", "baz"},
+				"x-additional": {"foo"},
+			},
+		},
+		{
+			name: "multiple header, multiple values, custom separator, leading & trailing spaces",
+			args: args{
+				anns: map[string]string{
+					"konghq.com/headers-separator":    ";",
+					"konghq.com/headers.x-example":    " foo, bar;cat,dog ;   baz ",
+					"konghq.com/headers.x-additional": "foo;",
+				},
+			},
+			want: map[string][]string{
+				"x-example":    {"foo, bar", "cat,dog", "baz"},
+				"x-additional": {"foo", ""},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/internal/dataplane/kong_client_test.go b/internal/dataplane/kong_client_test.go
index 391616d3b..de1fda47f 100644
--- a/internal/dataplane/kong_client_test.go
+++ b/internal/dataplane/kong_client_test.go
@@ -427,6 +427,7 @@ func (m *mockConfigStatusQueue) Notifications() []clients.ConfigStatus {
 type mockKongConfigBuilder struct {
 	translationFailuresToReturn []failures.ResourceFailure
 	kongState                   *kongstate.KongState
+	updateCacheCalled           bool
 }
 
 func newMockKongConfigBuilder() *mockKongConfigBuilder {
@@ -442,6 +443,14 @@ func (p *mockKongConfigBuilder) BuildKongConfig() translator.KongConfigBuildingR
 	}
 }
 
+func (p *mockKongConfigBuilder) UpdateCache(store.CacheStores) {
+	p.updateCacheCalled = true
+}
+
+func (p *mockKongConfigBuilder) IngressClassName() string {
+	return "kong"
+}
+
 func (p *mockKongConfigBuilder) returnTranslationFailures(enabled bool) {
 	if enabled {
 		// Return some mocked translation failures.
@@ -928,3 +937,34 @@ func TestKongClientUpdate_KonnectUpdatesAreSanitized(t *testing.T) {
 	require.NotNil(t, cert.Key, "expected Konnect to have certificate key")
 	require.Equal(t, "{vault://redacted-value}", *cert.Key, "expected Konnect to have redacted certificate key")
 }
+
+func TestKongClient_FallbackConfiguration(t *testing.T) {
+	ctx := context.Background()
+	clientsProvider := mockGatewayClientsProvider{
+		gatewayClients: []*adminapi.Client{mustSampleGatewayClient(t)},
+	}
+	updateStrategyResolver := newMockUpdateStrategyResolver(t)
+	configChangeDetector := mockConfigurationChangeDetector{hasConfigurationChanged: true}
+	configBuilder := newMockKongConfigBuilder()
+	kongRawStateGetter := &mockKongLastValidConfigFetcher{}
+	kongClient, err := NewKongClient(
+		zapr.NewLogger(zap.NewNop()),
+		time.Second,
+		util.ConfigDumpDiagnostic{},
+		sendconfig.Config{
+			FallbackConfiguration: true,
+		},
+		mocks.NewEventRecorder(),
+		dpconf.DBModeOff,
+		clientsProvider,
+		updateStrategyResolver,
+		configChangeDetector,
+		kongRawStateGetter,
+		configBuilder,
+		store.NewCacheStores(),
+	)
+	require.NoError(t, err)
+
+	require.NoError(t, kongClient.Update(ctx))
+	require.True(t, configBuilder.updateCacheCalled, "expected store to be updated with a snapshot")
+}
diff --git a/internal/dataplane/kongstate/consumer_test.go b/internal/dataplane/kongstate/consumer_test.go
index 644e79bcc..8933b9de3 100644
--- a/internal/dataplane/kongstate/consumer_test.go
+++ b/internal/dataplane/kongstate/consumer_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	"github.com/kong/kubernetes-ingress-controller/v3/test/mocks"
 )
 
 func int64Ptr(i int64) *int64 {
@@ -50,7 +51,7 @@ func TestConsumer_SanitizedCopy(t *testing.T) {
 					Tags:      []*string{kong.String("5.1"), kong.String("5.2")},
 				},
 				Plugins:    []kong.Plugin{{ID: kong.String("1")}},
-				KeyAuths:   []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: redactedString}}},
+				KeyAuths:   []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: kong.String("{vault://52fdfc07-2182-454f-963f-5f0f9a621d72}")}}},
 				HMACAuths:  []*HMACAuth{{kong.HMACAuth{ID: kong.String("1"), Secret: redactedString}}},
 				JWTAuths:   []*JWTAuth{{kong.JWTAuth{ID: kong.String("1"), Secret: redactedString}}},
 				BasicAuths: []*BasicAuth{{kong.BasicAuth{ID: kong.String("1"), Password: redactedString}}},
@@ -64,7 +65,7 @@ func TestConsumer_SanitizedCopy(t *testing.T) {
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
-			got := *tt.in.SanitizedCopy()
+			got := *tt.in.SanitizedCopy(mocks.StaticUUIDGenerator{UUID: "52fdfc07-2182-454f-963f-5f0f9a621d72"})
 			assert.Equal(t, tt.want, got)
 		})
 	}
diff --git a/internal/dataplane/kongstate/credentials_test.go b/internal/dataplane/kongstate/credentials_test.go
index 23d7bf27d..2c090a74e 100644
--- a/internal/dataplane/kongstate/credentials_test.go
+++ b/internal/dataplane/kongstate/credentials_test.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/kong/go-kong/kong"
 	"github.com/stretchr/testify/assert"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/test/mocks"
 )
 
 func TestKeyAuth_SanitizedCopy(t *testing.T) {
@@ -25,13 +27,13 @@ func TestKeyAuth_SanitizedCopy(t *testing.T) {
 			want: KeyAuth{kong.KeyAuth{
 				CreatedAt: kong.Int(1),
 				ID:        kong.String("2"),
-				Key:       redactedString,
+				Key:       kong.String("{vault://52fdfc07-2182-454f-963f-5f0f9a621d72}"),
 				Tags:      []*string{kong.String("4.1"), kong.String("4.2")},
 			}},
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
-			got := *tt.in.SanitizedCopy()
+			got := *tt.in.SanitizedCopy(mocks.StaticUUIDGenerator{UUID: "52fdfc07-2182-454f-963f-5f0f9a621d72"})
 			assert.Equal(t, tt.want, got)
 		})
 	}
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 522707a54..e87399b3b 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -31,6 +31,7 @@ import (
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
+	"github.com/kong/kubernetes-ingress-controller/v3/test/mocks"
 )
 
 var kongConsumerTypeMeta = metav1.TypeMeta{
@@ -86,7 +87,7 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 					Plugin:              kong.Plugin{ID: kong.String("1"), Config: kong.Configuration{"secret": *redactedString}},
 				}},
 				Consumers: []Consumer{{
-					KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: redactedString}}},
+					KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: kong.String("{vault://52fdfc07-2182-454f-963f-5f0f9a621d72}")}}},
 				}},
 				Licenses: []License{{kong.License{ID: kong.String("1"), Payload: redactedString}}},
 				ConsumerGroups: []ConsumerGroup{{
@@ -104,7 +105,7 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 	} {
 		t.Run(tt.name, func(t *testing.T) {
 			testedFields.Insert(extractNotEmptyFieldNames(tt.in)...)
-			got := *tt.in.SanitizedCopy()
+			got := *tt.in.SanitizedCopy(mocks.StaticUUIDGenerator{UUID: "52fdfc07-2182-454f-963f-5f0f9a621d72"})
 			assert.Equal(t, tt.want, got)
 		})
 	}
diff --git a/internal/dataplane/translator/subtranslator/httproute_test.go b/internal/dataplane/translator/subtranslator/httproute_test.go
index 072cc7688..d47288cbc 100644
--- a/internal/dataplane/translator/subtranslator/httproute_test.go
+++ b/internal/dataplane/translator/subtranslator/httproute_test.go
@@ -9,22 +9,24 @@ import (
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/require"
 
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/kongstate"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 )
 
 func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) {
 	testCases := []struct {
-		name                      string
-		filters                   []gatewayapi.HTTPRouteFilter
-		path                      string
-		expectedPlugins           []kong.Plugin
-		expectedPluginsAnnotation string
-		expectedErr               error
+		name                       string
+		filters                    []gatewayapi.HTTPRouteFilter
+		path                       string
+		expectedPlugins            []kong.Plugin
+		expectedRouteModifications kongstate.Route
+		expectedErr                error
 	}{
 		{
 			name:            "no filters",
 			filters:         []gatewayapi.HTTPRouteFilter{},
-			expectedPlugins: []kong.Plugin{},
+			expectedPlugins: nil,
 		},
 		{
 			name: "request header modifier filter",
@@ -177,8 +179,14 @@ func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) {
 					},
 				},
 			},
-			expectedPluginsAnnotation: "plugin1,plugin2",
-			expectedPlugins:           []kong.Plugin{},
+			expectedRouteModifications: kongstate.Route{
+				Ingress: util.K8sObjectInfo{
+					Annotations: map[string]string{
+						"konghq.com/plugins": "plugin1,plugin2",
+					},
+				},
+			},
+			expectedPlugins: []kong.Plugin{},
 		},
 		{
 			name: "invalid extensionrefs filter group",
@@ -192,8 +200,7 @@ func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) {
 					},
 				},
 			},
-			expectedPluginsAnnotation: "",
-			expectedErr:               errors.New("plugin wrong.group/KongPlugin unsupported"),
+			expectedErr: errors.New("plugin wrong.group/KongPlugin unsupported"),
 		},
 		{
 			name: "invalid extensionrefs filter kind",
@@ -207,28 +214,86 @@ func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) {
 					},
 				},
 			},
-			expectedPluginsAnnotation: "",
-			expectedErr:               errors.New("plugin configuration.konghq.com/WrongKind unsupported"),
+			expectedErr: errors.New("plugin configuration.konghq.com/WrongKind unsupported"),
+		},
+		{
+			name: "RequestHeaderModifier and PrefixMatchHTTPPathModifier",
+			filters: []gatewayapi.HTTPRouteFilter{
+				{
+					Type: gatewayapi.HTTPRouteFilterRequestHeaderModifier,
+					RequestHeaderModifier: &gatewayapi.HTTPHeaderFilter{
+						Set: []gatewayapi.HTTPHeader{
+							{
+								Name:  "header-to-set",
+								Value: "bar",
+							},
+						},
+					},
+				},
+				{
+					Type: gatewayapi.HTTPRouteFilterURLRewrite,
+					URLRewrite: &gatewayapi.HTTPURLRewriteFilter{
+						Path: &gatewayapi.HTTPPathModifier{
+							Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+							ReplacePrefixMatch: lo.ToPtr("/new"),
+						},
+					},
+				},
+			},
+			path: "/prefix",
+			expectedPlugins: []kong.Plugin{
+				{
+					Name: kong.String("request-transformer"),
+					Config: kong.Configuration{
+						"add": map[string]interface{}{
+							"headers": []interface{}{
+								"header-to-set:bar",
+							},
+						},
+						"replace": map[string]interface{}{
+							"uri": "/new$(uri_captures[1])",
+							"headers": []interface{}{
+								"header-to-set:bar",
+							},
+						},
+					},
+				},
+			},
+			expectedRouteModifications: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/prefix$"),
+						lo.ToPtr("~/prefix(/.*)"),
+					},
+				},
+			},
 		},
 	}
 
 	for _, tc := range testCases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			plugins, pluginsAnnotation, err := generatePluginsFromHTTPRouteFilters(tc.filters, tc.path, nil)
+			result, err := generatePluginsFromHTTPRouteFilters(tc.filters, tc.path, nil, false)
 			require.Equal(t, tc.expectedErr, err)
-			require.Equal(t, tc.expectedPlugins, plugins)
-			require.Equal(t, tc.expectedPluginsAnnotation, pluginsAnnotation)
+			require.Equal(t, tc.expectedPlugins, result.Plugins)
+
+			route := kongstate.Route{}
+			for _, modifier := range result.KongRouteModifiers {
+				modifier(&route)
+			}
+			require.Equal(t, tc.expectedRouteModifications, route)
 		})
 	}
 }
 
 func TestGenerateRequestTransformerForURLRewrite(t *testing.T) {
 	testCases := []struct {
-		name        string
-		modifier    *gatewayapi.HTTPURLRewriteFilter
-		expected    kong.Plugin
-		expectedErr error
+		name                          string
+		modifier                      *gatewayapi.HTTPURLRewriteFilter
+		firstMatchPath                string
+		expectedKongRouteModification kongstate.Route
+		expected                      kong.Plugin
+		expectedErr                   error
 	}{
 		{
 			name: "valid URLRewriteFilter with ReplaceFullPath",
@@ -248,17 +313,187 @@ func TestGenerateRequestTransformerForURLRewrite(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3686
 		{
-			name: "valid URLRewriteFilter with unsupported",
+			name: "URLRewriteFilter with non-empty ReplacePrefixMatch",
 			modifier: &gatewayapi.HTTPURLRewriteFilter{
 				Path: &gatewayapi.HTTPPathModifier{
 					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
 					ReplacePrefixMatch: lo.ToPtr("/new"),
 				},
 			},
-			expected:    kong.Plugin{},
-			expectedErr: fmt.Errorf("%s unsupported for %s", gatewayapi.PrefixMatchHTTPPathModifier, gatewayapi.HTTPRouteFilterURLRewrite),
+			firstMatchPath: "/prefix",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": "/new$(uri_captures[1])",
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/prefix$"),
+						lo.ToPtr("~/prefix(/.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with empty ReplacePrefixMatch",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr(""),
+				},
+			},
+			firstMatchPath: "/prefix",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `$(uri_captures[1] == nil and "/" or uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/prefix$"),
+						lo.ToPtr("~/prefix(/.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with empty firstMatchPath",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr("/prefix"),
+				},
+			},
+			firstMatchPath: "",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `/prefix$(uri_captures[1] == nil and "" or "/" .. uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/$"),
+						lo.ToPtr("~/(.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with '/' ReplacePrefixPatch",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr("/"),
+				},
+			},
+			firstMatchPath: "/prefix",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `$(uri_captures[1] == nil and "/" or uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/prefix$"),
+						lo.ToPtr("~/prefix(/.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with '/' firstMatchPath and '/' ReplacePrefixPatch",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr("/"),
+				},
+			},
+			firstMatchPath: "/",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `$(uri_captures[1] == nil and "/" or "/" .. uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/$"),
+						lo.ToPtr("~/(.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with '/' firstMatchPath and non-empty ReplacePrefixPatch",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr("/new-prefix"),
+				},
+			},
+			firstMatchPath: "/",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `/new-prefix$(uri_captures[1] == nil and "" or "/" .. uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/$"),
+						lo.ToPtr("~/(.*)"),
+					},
+				},
+			},
+		},
+		{
+			name: "URLRewriteFilter with firstMatchPath with a trailing slash",
+			modifier: &gatewayapi.HTTPURLRewriteFilter{
+				Path: &gatewayapi.HTTPPathModifier{
+					Type:               gatewayapi.PrefixMatchHTTPPathModifier,
+					ReplacePrefixMatch: lo.ToPtr("/new-prefix"),
+				},
+			},
+			firstMatchPath: "/prefix/",
+			expected: kong.Plugin{
+				Name: lo.ToPtr("request-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]string{
+						"uri": `/new-prefix$(uri_captures[1])`,
+					},
+				},
+			},
+			expectedKongRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Paths: []*string{
+						lo.ToPtr("~/prefix$"),
+						lo.ToPtr("~/prefix(/.*)"),
+					},
+				},
+			},
 		},
 		// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/3685
 		{
@@ -279,9 +514,186 @@ func TestGenerateRequestTransformerForURLRewrite(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			plugin, err := generateRequestTransformerForURLRewrite(tc.modifier)
+			plugin, routeModifier, err := generateRequestTransformerForURLRewrite(tc.modifier, tc.firstMatchPath, false)
 			require.Equal(t, tc.expectedErr, err)
 			require.Equal(t, tc.expected, plugin)
+
+			route := kongstate.Route{}
+			if routeModifier != nil {
+				routeModifier(&route)
+			}
+			require.Equal(t, tc.expectedKongRouteModification, route)
+		})
+	}
+}
+
+func TestGenerateKongRouteModifierForURLRewritePrefixMatch_ExpressionsRouter(t *testing.T) {
+	testCases := []struct {
+		name                      string
+		path                      string
+		expectedRouteModification kongstate.Route
+	}{
+		{
+			name: "root path",
+			path: "/",
+			expectedRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Expression: lo.ToPtr(`(http.path == "/") || (http.path ~ "^/(.*)")`),
+				},
+			},
+		},
+		{
+			name: "prefix path",
+			path: "/prefix",
+			expectedRouteModification: kongstate.Route{
+				Route: kong.Route{
+					Expression: lo.ToPtr(`(http.path == "/prefix") || (http.path ~ "^/prefix(/.*)")`),
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			modifier := generateKongRouteModifierForURLRewritePrefixMatch(tc.path, true)
+			route := kongstate.Route{}
+			modifier(&route)
+			require.Equal(t, tc.expectedRouteModification, route)
+		})
+	}
+}
+
+func TestMergePluginsOfTheSameType(t *testing.T) {
+	testCases := []struct {
+		name     string
+		plugins  []kong.Plugin
+		expected []kong.Plugin
+	}{
+		{
+			name:     "no plugins",
+			plugins:  []kong.Plugin{},
+			expected: []kong.Plugin{},
+		},
+		{
+			name: "single plugin",
+			plugins: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+			},
+			expected: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+			},
+		},
+		{
+			name: "multiple plugins of different types",
+			plugins: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+				{
+					Name: lo.ToPtr("plugin2"),
+				},
+				{
+					Name: lo.ToPtr("plugin3"),
+				},
+			},
+			expected: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+				{
+					Name: lo.ToPtr("plugin2"),
+				},
+				{
+					Name: lo.ToPtr("plugin3"),
+				},
+			},
+		},
+		{
+			name: "multiple plugins of the same types",
+			plugins: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+				{
+					Name: lo.ToPtr("plugin2"),
+				},
+				{
+					Name: lo.ToPtr("plugin2"),
+				},
+			},
+			expected: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+				},
+				{
+					Name: lo.ToPtr("plugin2"),
+				},
+			},
+		},
+		{
+			name: "multiple plugins of the same types with different configurations - configuration is merged",
+			plugins: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key1": "value1",
+					},
+				},
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key2": "value2",
+					},
+				},
+			},
+			expected: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key1": "value1",
+						"key2": "value2",
+					},
+				},
+			},
+		},
+		{
+			name: "multiple plugins of the same types with same configuration keys - configuration is merged and the first wins",
+			plugins: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key1": "value1",
+					},
+				},
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key1": "value2",
+					},
+				},
+			},
+			expected: []kong.Plugin{
+				{
+					Name: lo.ToPtr("plugin1"),
+					Config: kong.Configuration{
+						"key1": "value1",
+					},
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			plugins, err := mergePluginsOfTheSameType(tc.plugins)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, plugins)
 		})
 	}
 }
diff --git a/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/default_golden.yaml b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/default_golden.yaml
new file mode 100644
index 000000000..f83a4fc09
--- /dev/null
+++ b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/default_golden.yaml
@@ -0,0 +1,50 @@
+_format_version: "3.0"
+services:
+- connect_timeout: 60000
+  host: httproute.default.httproute-testing.0
+  id: 4e3cb785-a8d0-5866-aa05-117f7c64f24d
+  name: httproute.default.httproute-testing.0
+  port: 8080
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - https_redirect_status_code: 426
+    id: 073fc413-1c03-50b4-8f44-43367c13daba
+    name: httproute.default.httproute-testing.0.0
+    path_handling: v0
+    paths:
+    - ~/prefix$
+    - ~/prefix(/.*)
+    plugins:
+    - config:
+        replace:
+          uri: /new-prefix$(uri_captures[1])
+      name: request-transformer
+    preserve_host: true
+    protocols:
+    - http
+    - https
+    strip_path: false
+    tags:
+    - k8s-name:httproute-testing
+    - k8s-namespace:default
+    - k8s-kind:HTTPRoute
+    - k8s-group:gateway.networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:httproute-testing
+  - k8s-namespace:default
+  - k8s-kind:HTTPRoute
+  - k8s-group:gateway.networking.k8s.io
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: httproute.default.httproute-testing.0
+  tags:
+  - k8s-name:httproute-testing
+  - k8s-namespace:default
+  - k8s-kind:HTTPRoute
+  - k8s-group:gateway.networking.k8s.io
+  - k8s-version:v1
diff --git a/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml
new file mode 100644
index 000000000..ecd687cae
--- /dev/null
+++ b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml
@@ -0,0 +1,45 @@
+_format_version: "3.0"
+services:
+- connect_timeout: 60000
+  host: httproute.default.httproute-testing._.0
+  id: 2fad71d1-7599-5c6e-9d4f-4afd44f99587
+  name: httproute.default.httproute-testing._.0
+  port: 8080
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - expression: (http.path == "/prefix") || (http.path ~ "^/prefix(/.*)")
+    https_redirect_status_code: 426
+    id: 91833860-2041-5eea-abf8-a1e85b7c64cf
+    name: httproute.default.httproute-testing._.0.0
+    plugins:
+    - config:
+        replace:
+          uri: /new-prefix$(uri_captures[1])
+      name: request-transformer
+    preserve_host: true
+    priority: 35184422424575
+    strip_path: false
+    tags:
+    - k8s-name:httproute-testing
+    - k8s-namespace:default
+    - k8s-kind:HTTPRoute
+    - k8s-group:gateway.networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:httproute-testing
+  - k8s-namespace:default
+  - k8s-kind:HTTPRoute
+  - k8s-group:gateway.networking.k8s.io
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: httproute.default.httproute-testing._.0
+  tags:
+  - k8s-name:httproute-testing
+  - k8s-namespace:default
+  - k8s-kind:HTTPRoute
+  - k8s-group:gateway.networking.k8s.io
+  - k8s-version:v1
diff --git a/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_settings.yaml b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_settings.yaml
new file mode 100644
index 000000000..5c4f81d45
--- /dev/null
+++ b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_settings.yaml
@@ -0,0 +1,2 @@
+feature_flags:
+  ExpressionRoutes: true
diff --git a/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/in.yaml b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/in.yaml
new file mode 100644
index 000000000..b3cede04b
--- /dev/null
+++ b/internal/dataplane/translator/testdata/golden/httproute-url-rewrite-path-prefix/in.yaml
@@ -0,0 +1,61 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: httpbin
+  name: httpbin
+  namespace: default
+spec:
+  ports:
+    - port: 80
+      protocol: TCP
+      targetPort: 80
+  selector:
+    app: httpbin
+  type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: nginx
+  name: nginx
+  namespace: default
+spec:
+  ports:
+    - port: 8080
+      protocol: TCP
+      targetPort: 80
+  selector:
+    app: nginx
+  type: ClusterIP
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httproute-testing
+  namespace: default
+spec:
+  parentRefs:
+    - name: kong
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /prefix
+      filters:
+        - type: URLRewrite
+          urlRewrite:
+            path:
+              type: ReplacePrefixMatch
+              replacePrefixMatch: /new-prefix
+      backendRefs:
+        - name: httpbin
+          kind: Service
+          port: 80
+          weight: 75
+        - name: nginx
+          kind: Service
+          port: 8080
+          weight: 25
diff --git a/internal/dataplane/translator/translate_httproute_test.go b/internal/dataplane/translator/translate_httproute_test.go
index 9ff2533dd..b4d359fa5 100644
--- a/internal/dataplane/translator/translate_httproute_test.go
+++ b/internal/dataplane/translator/translate_httproute_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/go-logr/zapr"
+	"github.com/google/go-cmp/cmp"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/assert"
@@ -944,7 +945,6 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
 				}
 			},
 		},
-
 		{
 			msg: "a single HTTPRoute with multiple rules with equal backendRefs and different filters results in a single service",
 			routes: []*gatewayapi.HTTPRoute{
@@ -1102,7 +1102,6 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
 				}
 			},
 		},
-
 		{
 			msg: "a single HTTPRoute with single rule and multiple matches generates consolidated kong route paths",
 			routes: []*gatewayapi.HTTPRoute{{
@@ -1272,7 +1271,6 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
 				}
 			},
 		},
-
 		{
 			msg: "a single HTTPRoute with multiple rules and matches generates consolidated kong route paths",
 			routes: []*gatewayapi.HTTPRoute{{
@@ -1481,7 +1479,6 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
 				}
 			},
 		},
-
 		{
 			msg: "a single HTTPRoute with timeouts will set the timeout in the service",
 			routes: []*gatewayapi.HTTPRoute{{
@@ -1592,7 +1589,7 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
 
 			// verify that we receive the expected values
 			expectedIngressRules := tt.expected(tt.routes)
-			assert.Equal(t, expectedIngressRules, ingressRules)
+			assert.Empty(t, cmp.Diff(expectedIngressRules, ingressRules, cmp.AllowUnexported(SecretNameToSNIs{}, kongstate.ServiceBackend{})))
 
 			// verify that we receive any and all expected errors
 			for i := range tt.errs {
diff --git a/internal/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go
index 7676913eb..03f6171d3 100644
--- a/internal/dataplane/translator/translator_test.go
+++ b/internal/dataplane/translator/translator_test.go
@@ -5020,14 +5020,12 @@ func TestTranslator_ConfiguredKubernetesObjects(t *testing.T) {
 }
 
 func mustNewTranslator(t *testing.T, storer store.Storer) *Translator {
-	p, err := NewTranslator(zapr.NewLogger(zap.NewNop()), storer, "",
-		FeatureFlags{
-			// We'll assume these are true for all tests.
-			FillIDs:                           true,
-			ReportConfiguredKubernetesObjects: true,
-			KongServiceFacade:                 true,
-		},
-	)
+	p, err := NewTranslator(zapr.NewLogger(zap.NewNop()), storer, "", FeatureFlags{
+		// We'll assume these are true for all tests.
+		FillIDs:                           true,
+		ReportConfiguredKubernetesObjects: true,
+		KongServiceFacade:                 true,
+	})
 	require.NoError(t, err)
 	return p
 }
@@ -5164,3 +5162,34 @@ func TestUpdateTargetMap(t *testing.T) {
 		})
 	}
 }
+
+func TestTranslator_UpdateStore(t *testing.T) {
+	originalStore, err := store.NewFakeStore(store.FakeObjects{})
+	require.NoError(t, err)
+	translator := mustNewTranslator(t, originalStore)
+
+	originalBuildConfigResult := translator.BuildKongConfig()
+
+	newStore, err := store.NewCacheStoresFromObjs(
+		&kongv1.KongConsumer{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "KongConsumer",
+				APIVersion: kongv1.GroupVersion.String(),
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "consumer1",
+				Namespace: "bar",
+				Annotations: map[string]string{
+					annotations.IngressClassKey: annotations.DefaultIngressClass,
+				},
+			},
+			Username: "consumer1",
+		},
+	)
+	require.NoError(t, err)
+	translator.UpdateCache(newStore)
+
+	newBuildConfigResult := translator.BuildKongConfig()
+	require.NotEqual(t, originalBuildConfigResult.KongState, newBuildConfigResult.KongState, "KongState should be different after updating the store")
+	require.Len(t, newBuildConfigResult.KongState.Consumers, 1, "expected 1 consumer in the KongState")
+}
diff --git a/internal/manager/setup_test.go b/internal/manager/setup_test.go
index 93a42c973..f5a320b6b 100644
--- a/internal/manager/setup_test.go
+++ b/internal/manager/setup_test.go
@@ -22,7 +22,7 @@ import (
 func TestAdminAPIClientFromServiceDiscovery(t *testing.T) {
 	log := logr.Discard()
 	adminAPISvcNN := k8stypes.NamespacedName{Name: "admin-api", Namespace: "kong"}
-	kubeClient := fake.NewClientBuilder().Build()
+	kubeClient := fake.NewFakeClient()
 	genericErr := errors.New("some generic error")
 	someDiscoveredAPI := func(address string) adminapi.DiscoveredAdminAPI {
 		return adminapi.DiscoveredAdminAPI{
diff --git a/internal/store/cache_stores_snapshot_test.go b/internal/store/cache_stores_snapshot_test.go
new file mode 100644
index 000000000..df8a533cd
--- /dev/null
+++ b/internal/store/cache_stores_snapshot_test.go
@@ -0,0 +1,88 @@
+package store_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	netv1 "k8s.io/api/networking/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+)
+
+func TestCacheStores_TakeSnapshot(t *testing.T) {
+	originalStores, err := store.NewCacheStoresFromObjs([]runtime.Object{
+		&netv1.Ingress{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "networking.k8s.io/v1",
+				Kind:       "Ingress",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "foo",
+				Namespace: "default",
+				Annotations: map[string]string{
+					"foo": "bar",
+				},
+			},
+		},
+	}...)
+	require.NoError(t, err)
+
+	t.Log("Taking a snapshot of the originalStores")
+	snapshot, err := originalStores.TakeSnapshot()
+	require.NoError(t, err)
+
+	t.Log("Adding a new object to the original store")
+	err = originalStores.Add(&netv1.Ingress{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "networking.k8s.io/v1",
+			Kind:       "Ingress",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "bar",
+			Namespace: "default",
+			Annotations: map[string]string{
+				"foo": "baz",
+			},
+		},
+	})
+	require.NoError(t, err)
+
+	// We'll use ingressMeta in .Get() calls.
+	ingressMeta := &netv1.Ingress{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "networking.k8s.io/v1",
+			Kind:       "Ingress",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "foo",
+			Namespace: "default",
+		},
+	}
+
+	t.Log("Modifying the original object")
+	obj, ok, err := originalStores.IngressV1.Get(ingressMeta)
+	require.NoError(t, err)
+	require.True(t, ok)
+	ingressFromOriginalStore, ok := obj.(*netv1.Ingress)
+	require.True(t, ok)
+	require.Equal(t, "bar", ingressFromOriginalStore.Annotations["foo"])
+	ingressFromOriginalStore.Annotations["foo"] = "qux"
+
+	t.Log("Checking that the original store returns the modified object")
+	obj, ok, err = originalStores.IngressV1.Get(ingressMeta)
+	require.NoError(t, err)
+	require.True(t, ok)
+	ingressFromOriginalStore2, ok := obj.(*netv1.Ingress)
+	require.True(t, ok)
+	require.Same(t, ingressFromOriginalStore, ingressFromOriginalStore2, "Store should return pointers the same object")
+
+	t.Log("Checking that the snapshot returns the unmodified object")
+	obj, ok, err = snapshot.IngressV1.Get(obj)
+	require.NoError(t, err)
+	require.True(t, ok)
+	ingressFromSnapshot, ok := obj.(*netv1.Ingress)
+	require.True(t, ok)
+	require.Equal(t, "bar", ingressFromSnapshot.Annotations["foo"], "Snapshot should not be affected by the change in the original store")
+}
diff --git a/internal/store/store_test.go b/internal/store/store_test.go
index 791bcdc9c..57abfce3f 100644
--- a/internal/store/store_test.go
+++ b/internal/store/store_test.go
@@ -147,7 +147,7 @@ func TestGetIngressClassHandling(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			s, err := NewFakeStore(tt.objs)
 			require.NoError(t, err)
-			if got := s.(Store).getIngressClassHandling(); got != tt.want {
+			if got := s.(*Store).getIngressClassHandling(); got != tt.want {
 				t.Errorf("s.getIngressClassHandling() = %v, want %v", got, tt.want)
 			}
 		})
diff --git a/test/conformance/gateway_conformance_test.go b/test/conformance/gateway_conformance_test.go
index 7bfbb56c5..c5f47acc3 100644
--- a/test/conformance/gateway_conformance_test.go
+++ b/test/conformance/gateway_conformance_test.go
@@ -31,6 +31,7 @@ var traditionalRoutesSupportedFeatures = []suite.SupportedFeature{
 	suite.SupportHTTPRoute,
 	// extended features
 	suite.SupportHTTPRouteResponseHeaderModification,
+	suite.SupportHTTPRoutePathRewrite,
 	// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/5868
 	// Temporarily disabled and tracking through the following issue.
 	// suite.SupportHTTPRouteBackendTimeout,
@@ -44,6 +45,7 @@ var expressionRoutesSupportedFeatures = []suite.SupportedFeature{
 	suite.SupportHTTPRouteQueryParamMatching,
 	suite.SupportHTTPRouteMethodMatching,
 	suite.SupportHTTPRouteResponseHeaderModification,
+	suite.SupportHTTPRoutePathRewrite,
 	// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/5868
 	// Temporarily disabled and tracking through the following issue.
 	// suite.SupportHTTPRouteBackendTimeout,
@@ -106,7 +108,7 @@ func TestGatewayConformance(t *testing.T) {
 	// To work with individual tests only, you can disable the normal Run call and construct a slice containing a
 	// single test only, e.g.:
 	//
-	// cSuite.Run(t, []suite.ConformanceTest{tests.HTTPRouteRedirectPortAndScheme})
+	// require.NoError(t, cSuite.Run(t, []suite.ConformanceTest{tests.HTTPRouteRewritePath}))
 	require.NoError(t, cSuite.Run(t, tests.ConformanceTests))
 
 	const reportFileName = "kong-kubernetes-ingress-controller.yaml"
diff --git a/test/envtest/telemetry_test.go b/test/envtest/telemetry_test.go
index 7a4a5e334..175249c4d 100644
--- a/test/envtest/telemetry_test.go
+++ b/test/envtest/telemetry_test.go
@@ -356,6 +356,7 @@ func verifyTelemetryReport(t *testing.T, k8sVersion *version.Info, report string
 		"<14>"+
 			"signal=kic-ping;"+
 			"db=off;"+
+			"feature-fallbackconfiguration=false;"+
 			"feature-fillids=true;"+
 			"feature-gateway-service-discovery=false;"+
 			"feature-gatewayalpha=false;"+
diff --git a/test/integration/isolated/exmples_httproute_rewrite_test.go b/test/integration/isolated/examples_httproute_rewrite_test.go
similarity index 71%
rename from test/integration/isolated/exmples_httproute_rewrite_test.go
rename to test/integration/isolated/examples_httproute_rewrite_test.go
index dae295ea0..6566b9192 100644
--- a/test/integration/isolated/exmples_httproute_rewrite_test.go
+++ b/test/integration/isolated/examples_httproute_rewrite_test.go
@@ -19,7 +19,7 @@ import (
 )
 
 func TestHTTPRouteRewriteExample(t *testing.T) {
-	httprouteURLRewritePathFullExampleManifests := examplesManifestPath("gateway-httproute-rewrite-path-full.yaml")
+	httprouteURLRewriteExampleManifests := examplesManifestPath("gateway-httproute-rewrite-path.yaml")
 
 	f := features.
 		New("example").
@@ -35,20 +35,33 @@ func TestHTTPRouteRewriteExample(t *testing.T) {
 				cluster := GetClusterFromCtx(ctx)
 				proxyURL := GetHTTPURLFromCtx(ctx)
 
-				t.Logf("applying yaml manifest %s", httprouteURLRewritePathFullExampleManifests)
-				manifest, err := os.ReadFile(httprouteURLRewritePathFullExampleManifests)
+				t.Logf("applying yaml manifest %s", httprouteURLRewriteExampleManifests)
+				manifest, err := os.ReadFile(httprouteURLRewriteExampleManifests)
 				assert.NoError(t, err)
 				assert.NoError(t, clusters.ApplyManifestByYAML(ctx, cluster, string(manifest)))
 				cleaner.AddManifest(string(manifest))
 
 				t.Logf("verifying that the UDPIngress routes traffic properly")
 
-				t.Logf("asserting /dummy-random-string path is redirected (as any other path for this HTTPRoute) to /echo?msg=hello from the manifest")
+				t.Logf("asserting /full-path-prefix path is redirected to /echo?msg=hello from the manifest")
 				helpers.EventuallyGETPath(
 					t,
 					proxyURL,
 					proxyURL.Host,
-					"/dummy-random-string",
+					"/full-path-prefix",
+					http.StatusOK,
+					"hello",
+					nil,
+					consts.IngressWait,
+					consts.WaitTick,
+				)
+
+				t.Logf("asserting /old-prefix?msg=hello path is redirected to /echo?msg=hello replacing the prefix")
+				helpers.EventuallyGETPath(
+					t,
+					proxyURL,
+					proxyURL.Host,
+					"/old-prefix?msg=hello",
 					http.StatusOK,
 					"hello",
 					nil,
diff --git a/test/mocks/uuid.go b/test/mocks/uuid.go
new file mode 100644
index 000000000..f0cd6d375
--- /dev/null
+++ b/test/mocks/uuid.go
@@ -0,0 +1,13 @@
+package mocks
+
+import "github.com/kong/kubernetes-ingress-controller/v3/internal/util"
+
+var _ = util.UUIDGenerator(&StaticUUIDGenerator{})
+
+type StaticUUIDGenerator struct {
+	UUID string
+}
+
+func (s StaticUUIDGenerator) NewString() string {
+	return s.UUID
+}