Repository: Kong/kubernetes-ingress-controller
Pull request #6138 (2024-06-05 11:40:20)

fix: do not sanitize plugins' config
**What this PR does / why we need it**:

This reverts commit 2d615e3df5956d6ebd4e0b0137ff6dffc20294c1.


<!-- Please describe why this particular PR is necessary or why you see it as a nice addition -->

**Which issue this PR fixes**:

Fixes https://github.com/Kong/kubernetes-ingress-controller/issues/5692.

**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/6138?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 75.5%. Comparing base [(`8a88685`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/8a88685739440391cc6c47830f2c7b73a8e58d5e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) to head [(`f38af29`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/f38af29895d801994f7e1888ad1416f359529f1c?dropdown=coverage&el=desc&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   #6138     +/-   ##
=======================================
+ Coverage   73.8%   75.5%   +1.7%     
=======================================
  Files        197     197             
  Lines      19382   19330     -52     
=======================================
+ Hits       14314   14611    +297     
+ Misses      4085    3726    -359     
- Partials     983     993     +10     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6138?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/CHANGELOG.md b/CHANGELOG.md
index 462f5ead6..8b022708e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -140,9 +140,19 @@ Adding a new version? You'll need three changes:
   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.
+  defined in KIC yet. The current version only supports translating custom
+  entities into declarative configuration in DBless mode, and cannot apply
+  custom entities to DB backed Kong gateways.
+  Feature gate `KongCustomEntity` is required to set to `true` to enabled the
+  `KongCustomEntity` controller.
+  **Note**: The IDs of Kong services, routes and consumers referred by custom
+  entities via `foreign` type fields of custom entities are filled by the `FillID`
+  method of the corresponding type because the IDs of these entities are required
+  to fill the `foreign` fields of custom entities. So the `FillIDs` feature gate
+  is also required when `KongCustomEntity` is enabled.
   [#5982](https://github.com/Kong/kubernetes-ingress-controller/pull/5982)
   [#6006](https://github.com/Kong/kubernetes-ingress-controller/pull/6006)
+  [#6055](https://github.com/Kong/kubernetes-ingress-controller/pull/6055)
 - Added `FallbackConfiguration` feature gate to enable the controller to generate a fallback configuration
   for Kong when it fails to apply the original one. The feature gate is disabled by default.
   [#5993](https://github.com/Kong/kubernetes-ingress-controller/pull/5993)
@@ -201,15 +211,17 @@ Adding a new version? You'll need three changes:
   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)
+  [#5964](https://github.com/Kong/kubernetes-ingress-controller/pull/5964)
 - The `--dump-sensitive-config` flag is no longer backwards.
-  [6073](https://github.com/Kong/kubernetes-ingress-controller/pull/6073)
+  [#6073](https://github.com/Kong/kubernetes-ingress-controller/pull/6073)
 - Fixed KIC clearing Gateway API *Route status of routes that it shouldn't reconcilce, e.g.
   those attached to Gateways that do not belong to GatewayClass that KIC reconciles.
-  [6079](https://github.com/Kong/kubernetes-ingress-controller/pull/6079)
+  [#6079](https://github.com/Kong/kubernetes-ingress-controller/pull/6079)
 - Fixed KIC non leaders correctly getting up to date Admin API addresses by not
   requiring leader election for the related controller.
-  [6126](https://github.com/Kong/kubernetes-ingress-controller/pull/6126)
+  [#6126](https://github.com/Kong/kubernetes-ingress-controller/pull/6126)
+- KongPlugin's `config` field is no longer incorrectly sanitized.
+  [#6138](https://github.com/Kong/kubernetes-ingress-controller/pull/6138)
 
 ### Changed
 
diff --git a/Dockerfile b/Dockerfile
index 3d66b7465..2cbd4a0e6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 ### Standard binary
 # Build the manager binary
-FROM --platform=$BUILDPLATFORM golang:1.22.3 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.4 as builder
 
 ARG GOPATH
 ARG GOCACHE
diff --git a/Dockerfile.debug b/Dockerfile.debug
index 13dbb328a..e0fe65aed 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.3 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.4 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.3 AS debug
+FROM golang:1.22.4 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/FEATURE_GATES.md b/FEATURE_GATES.md
index ae7c071ea..ce8c070ea 100644
--- a/FEATURE_GATES.md
+++ b/FEATURE_GATES.md
@@ -71,6 +71,7 @@ Features that reach GA and over time become stable will be removed from this tab
 | KongServiceFacade          | `false` | Alpha | 3.1.0  | TBD   |
 | SanitizeKonnectConfigDumps | `true`  | Beta  | 3.1.0  | TBD   |
 | FallbackConfiguration      | `false` | Alpha | 3.2.0  | TBD   |
+| KongCustomEntity           | `false` | Alpha | 3.2.0  | TBD   |
 
 **NOTE**: The `Gateway` feature gate refers to [Gateway
  API](https://github.com/kubernetes-sigs/gateway-api) APIs which are in
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 2566adab5..2f0eddb39 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -15,6 +15,7 @@ resources:
 - bases/configuration.konghq.com_kongupstreampolicies.yaml
 - bases/configuration.konghq.com_kongvaults.yaml
 - bases/configuration.konghq.com_konglicenses.yaml
+- bases/configuration.konghq.com_kongcustomentities.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index b20091894..817f3a53a 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -105,6 +105,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md
index 73a29e0fc..a0ad16120 100644
--- a/docs/cli-arguments.md
+++ b/docs/cli-arguments.md
@@ -26,6 +26,7 @@
 | `--enable-controller-ingress-class-networkingv1` | `bool` | Enable the networking.k8s.io/v1 IngressClass controller. | `true` |
 | `--enable-controller-ingress-class-parameters` | `bool` | Enable the IngressClassParameters controller. | `true` |
 | `--enable-controller-ingress-networkingv1` | `bool` | Enable the networking.k8s.io/v1 Ingress controller. | `true` |
+| `--enable-controller-kong-custom-entity` | `bool` | Enable the KongCustomEntity controller. | `true` |
 | `--enable-controller-kong-license` | `bool` | Enable the KongLicense controller. | `true` |
 | `--enable-controller-kong-service-facade` | `bool` | Enable the KongServiceFacade controller. | `true` |
 | `--enable-controller-kong-upstream-policy` | `bool` | Enable the KongUpstreamPolicy controller. | `true` |
diff --git a/go.mod b/go.mod
index 2f0b760bf..482bd3226 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/testcontainers/testcontainers-go v0.31.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/api v0.182.0
+	google.golang.org/api v0.183.0
 	k8s.io/api v0.30.1
 	k8s.io/apiextensions-apiserver v0.30.1
 	k8s.io/apimachinery v0.30.1
@@ -64,7 +64,7 @@ require (
 )
 
 require (
-	cloud.google.com/go/auth v0.4.2 // indirect
+	cloud.google.com/go/auth v0.5.1 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.26.0 // indirect
@@ -205,7 +205,7 @@ require (
 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
 	golang.org/x/mod v0.17.0 // indirect
 	golang.org/x/net v0.25.0 // indirect
-	golang.org/x/oauth2 v0.20.0 // indirect
+	golang.org/x/oauth2 v0.21.0 // indirect
 	golang.org/x/sync v0.7.0
 	golang.org/x/sys v0.20.0 // indirect
 	golang.org/x/term v0.20.0 // indirect
@@ -213,8 +213,8 @@ require (
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.21.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
 	google.golang.org/grpc v1.64.0
 	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
diff --git a/go.sum b/go.sum
index a84512c53..ea4ca1877 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
 cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
-cloud.google.com/go/auth v0.4.2 h1:sb0eyLkhRtpq5jA+a8KWw0W70YcdVca7KJ8TM0AFYDg=
-cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc=
+cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
+cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
 cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
 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=
@@ -514,8 +514,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
-golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.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=
@@ -583,17 +583,17 @@ 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.182.0 h1:if5fPvudRQ78GeRx3RayIoiuV7modtErPIZC/T2bIvE=
-google.golang.org/api v0.182.0/go.mod h1:cGhjy4caqA5yXRzEhkHI8Y9mfyC2VLTlER2l08xaqtM=
+google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
+google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
 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-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
-google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/hack/generators/cache-stores/spec.go b/hack/generators/cache-stores/spec.go
index befcbbf53..aadc25f96 100644
--- a/hack/generators/cache-stores/spec.go
+++ b/hack/generators/cache-stores/spec.go
@@ -108,4 +108,8 @@ var supportedTypes = []cacheStoreSupportedType{
 		Package: "kongv1alpha1",
 		KeyFunc: clusterWideKeyFunc,
 	},
+	{
+		Type:    "KongCustomEntity",
+		Package: "kongv1alpha1",
+	},
 }
diff --git a/hack/generators/controllers/networking/main.go b/hack/generators/controllers/networking/main.go
index ee7bc6ab8..967616663 100644
--- a/hack/generators/controllers/networking/main.go
+++ b/hack/generators/controllers/networking/main.go
@@ -264,6 +264,23 @@ var inputControllersNeeded = &typesNeeded{
 		AcceptsIngressClassNameAnnotation: true,
 		RBACVerbs:                         []string{"get", "list", "watch"},
 	},
+	typeNeeded{
+		Group:                            "configuration.konghq.com",
+		Version:                          "v1alpha1",
+		Kind:                             "KongCustomEntity",
+		PackageImportAlias:               "kongv1alpha1",
+		PackageAlias:                     "KongV1Alpha1",
+		Package:                          kongv1alpha1,
+		Plural:                           "kongcustomentities",
+		CacheType:                        "KongCustomEntity",
+		NeedsStatusPermissions:           true,
+		ConfigStatusNotificationsEnabled: true,
+		ProgrammedCondition: ProgrammedConditionConfiguration{
+			UpdatesEnabled: true,
+		},
+		AcceptsIngressClassNameAnnotation: true,
+		RBACVerbs:                         []string{"get", "list", "watch"},
+	},
 }
 
 var inputRBACPermissionsNeeded = &rbacsNeeded{
diff --git a/internal/controllers/configuration/zz_generated_controllers.go b/internal/controllers/configuration/zz_generated_controllers.go
index 0ddb99ca9..16398f013 100644
--- a/internal/controllers/configuration/zz_generated_controllers.go
+++ b/internal/controllers/configuration/zz_generated_controllers.go
@@ -2016,6 +2016,171 @@ func (r *KongV1Alpha1KongVaultReconciler) Reconcile(ctx context.Context, req ctr
 	return ctrl.Result{}, nil
 }
 
+// -----------------------------------------------------------------------------
+// KongV1Alpha1 KongCustomEntity - Reconciler
+// -----------------------------------------------------------------------------
+
+// KongV1Alpha1KongCustomEntityReconciler reconciles KongCustomEntity resources
+type KongV1Alpha1KongCustomEntityReconciler struct {
+	client.Client
+
+	Log              logr.Logger
+	Scheme           *runtime.Scheme
+	DataplaneClient  controllers.DataPlane
+	CacheSyncTimeout time.Duration
+	StatusQueue      *status.Queue
+
+	IngressClassName           string
+	DisableIngressClassLookups bool
+}
+
+var _ controllers.Reconciler = &KongV1Alpha1KongCustomEntityReconciler{}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *KongV1Alpha1KongCustomEntityReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	blder := ctrl.NewControllerManagedBy(mgr).
+		// set the controller name
+		Named("KongV1Alpha1KongCustomEntity").
+		WithOptions(controller.Options{
+			LogConstructor: func(_ *reconcile.Request) logr.Logger {
+				return r.Log
+			},
+			CacheSyncTimeout: r.CacheSyncTimeout,
+		})
+	// if configured, start the status updater controller
+	if r.StatusQueue != nil {
+		blder.WatchesRawSource(
+			source.Channel(
+				r.StatusQueue.Subscribe(schema.GroupVersionKind{
+					Group:   "configuration.konghq.com",
+					Version: "v1alpha1",
+					Kind:    "KongCustomEntity",
+				}),
+				&handler.EnqueueRequestForObject{},
+			),
+		)
+	}
+	if !r.DisableIngressClassLookups {
+		blder.Watches(&netv1.IngressClass{},
+			handler.EnqueueRequestsFromMapFunc(r.listClassless),
+			builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass)),
+		)
+	}
+	preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName)
+	return blder.Watches(&kongv1alpha1.KongCustomEntity{},
+		&handler.EnqueueRequestForObject{},
+		builder.WithPredicates(preds),
+	).
+		Complete(r)
+}
+
+// listClassless finds and reconciles all objects without ingress class information
+func (r *KongV1Alpha1KongCustomEntityReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request {
+	resourceList := &kongv1alpha1.KongCustomEntityList{}
+	if err := r.Client.List(ctx, resourceList); err != nil {
+		r.Log.Error(err, "Failed to list classless kongcustomentities")
+		return nil
+	}
+	var recs []reconcile.Request
+	for i, resource := range resourceList.Items {
+		if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) {
+			recs = append(recs, reconcile.Request{
+				NamespacedName: k8stypes.NamespacedName{
+					Namespace: resource.Namespace,
+					Name:      resource.Name,
+				},
+			})
+		}
+	}
+	return recs
+}
+
+// SetLogger sets the logger.
+func (r *KongV1Alpha1KongCustomEntityReconciler) SetLogger(l logr.Logger) {
+	r.Log = l
+}
+
+//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcustomentities,verbs=get;list;watch
+//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcustomentities/status,verbs=get;update;patch
+
+// Reconcile processes the watched objects
+func (r *KongV1Alpha1KongCustomEntityReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("KongV1Alpha1KongCustomEntity", req.NamespacedName)
+
+	// get the relevant object
+	obj := new(kongv1alpha1.KongCustomEntity)
+
+	if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
+		if apierrors.IsNotFound(err) {
+			obj.Namespace = req.Namespace
+			obj.Name = req.Name
+
+			return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj)
+		}
+		return ctrl.Result{}, err
+	}
+	log.V(util.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name)
+
+	// clean the object up if it's being deleted
+	if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) {
+		log.V(util.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongCustomEntity", "namespace", req.Namespace, "name", req.Name)
+
+		objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+		if objectExistsInCache {
+			if err := r.DataplaneClient.DeleteObject(obj); err != nil {
+				return ctrl.Result{}, err
+			}
+			return ctrl.Result{Requeue: true}, nil // wait until the object is no longer present in the cache
+		}
+		return ctrl.Result{}, nil
+	}
+
+	class := new(netv1.IngressClass)
+	if !r.DisableIngressClassLookups {
+		if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil {
+			// we log this without taking action to support legacy configurations that only set ingressClassName or
+			// used the class annotation and did not create a corresponding IngressClass. We only need this to determine
+			// if the IngressClass is default or to configure default settings, and can assume no/no additional defaults
+			// if none exists.
+			log.V(util.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName)
+		}
+	}
+	// if the object is not configured with our ingress.class, then we need to ensure it's removed from the cache
+	if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) {
+		log.V(util.DebugLevel).Info("Object missing ingress class, ensuring it's removed from configuration",
+			"namespace", req.Namespace, "name", req.Name, "class", r.IngressClassName)
+		return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj)
+	} else {
+		log.V(util.DebugLevel).Info("Object has matching ingress class", "namespace", req.Namespace, "name", req.Name,
+			"class", r.IngressClassName)
+	}
+
+	// update the kong Admin API with the changes
+	if err := r.DataplaneClient.UpdateObject(obj); err != nil {
+		return ctrl.Result{}, err
+	}
+	// if status updates are enabled report the status for the object
+	if r.DataplaneClient.AreKubernetesObjectReportsEnabled() {
+		log.V(util.DebugLevel).Info("Updating programmed condition status", "namespace", req.Namespace, "name", req.Name)
+		configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj)
+		conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(
+			configurationStatus,
+			obj.Generation,
+			obj.Status.Conditions,
+		)
+		obj.Status.Conditions = conditions
+		if updateNeeded {
+			return ctrl.Result{}, r.Status().Update(ctx, obj)
+		}
+		log.V(util.DebugLevel).Info("Status update not needed", "namespace", req.Namespace, "name", req.Name)
+	}
+
+	return ctrl.Result{}, nil
+}
+
 // -----------------------------------------------------------------------------
 // API Group "" resource nodes
 // -----------------------------------------------------------------------------
diff --git a/internal/controllers/utils/utils.go b/internal/controllers/utils/utils.go
index 1eeea14b9..89b6db4f0 100644
--- a/internal/controllers/utils/utils.go
+++ b/internal/controllers/utils/utils.go
@@ -9,6 +9,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 )
 
 const defaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
@@ -32,6 +33,12 @@ func MatchesIngressClass(obj client.Object, controllerIngressClass string, isDef
 			return true
 		}
 	}
+	// For KongCustomEntities, we check whether the `spec.ControllerName` matches.
+	if customEntity, isKongCustomEntity := obj.(*kongv1alpha1.KongCustomEntity); isKongCustomEntity {
+		if customEntity.Spec.ControllerName == controllerIngressClass {
+			return true
+		}
+	}
 	return objectIngressClass == controllerIngressClass
 }
 
diff --git a/internal/dataplane/deckgen/deckgen.go b/internal/dataplane/deckgen/deckgen.go
index 9f41eb733..87bd213f1 100644
--- a/internal/dataplane/deckgen/deckgen.go
+++ b/internal/dataplane/deckgen/deckgen.go
@@ -8,15 +8,24 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/kong/go-database-reconciler/pkg/file"
 	"github.com/kong/go-kong/kong"
+	"github.com/kong/go-kong/kong/custom"
 )
 
 // GenerateSHA generates a SHA256 checksum of targetContent, with the purpose
 // of change detection.
-func GenerateSHA(targetContent *file.Content) ([]byte, error) {
+func GenerateSHA(targetContent *file.Content, customEntities map[string][]custom.Object) ([]byte, error) {
 	jsonConfig, err := gojson.Marshal(targetContent)
 	if err != nil {
 		return nil, fmt.Errorf("marshaling Kong declarative configuration to JSON: %w", err)
 	}
+	// Calculate SHA including the custom entities.
+	if len(customEntities) > 0 {
+		jsonCustomEntities, err := gojson.Marshal(customEntities)
+		if err != nil {
+			return nil, fmt.Errorf("marshaling Kong custom entities to JSON: %w", err)
+		}
+		jsonConfig = append(jsonConfig, jsonCustomEntities...)
+	}
 
 	shaSum := sha256.Sum256(jsonConfig)
 	return shaSum[:], nil
diff --git a/internal/dataplane/fallback/graph_dependencies.go b/internal/dataplane/fallback/graph_dependencies.go
index 77e285fcf..dfb3cfc9c 100644
--- a/internal/dataplane/fallback/graph_dependencies.go
+++ b/internal/dataplane/fallback/graph_dependencies.go
@@ -63,6 +63,10 @@ func ResolveDependencies(cache store.CacheStores, obj client.Object) ([]client.O
 		*kongv1alpha1.IngressClassParameters,
 		*kongv1alpha1.KongVault:
 		return nil, nil
+	case *kongv1alpha1.KongCustomEntity:
+		// TODO: KongCustomEnity is not supported in failure domain yet.
+		// https://github.com/Kong/kubernetes-ingress-controller/issues/6122
+		return nil, nil
 	default:
 		return nil, fmt.Errorf("unsupported object type: %T", obj)
 	}
diff --git a/internal/dataplane/kong_client.go b/internal/dataplane/kong_client.go
index 3ee98b448..1fcdb1ca5 100644
--- a/internal/dataplane/kong_client.go
+++ b/internal/dataplane/kong_client.go
@@ -777,6 +777,13 @@ func (c *KongClient) sendToClient(
 		AppendStubEntityWhenConfigEmpty: !client.IsKonnect() && config.InMemory,
 	}
 	targetContent := deckgen.ToDeckContent(ctx, logger, s, deckGenParams)
+	customEntities := make(sendconfig.CustomEntitiesByType)
+	for entityType, collection := range s.CustomEntities {
+		for _, entity := range collection.Entities {
+			customEntities[entityType] = append(customEntities[entityType], entity.Object)
+		}
+	}
+
 	sendDiagnostic := prepareSendDiagnosticFn(ctx, logger, c.diagnostic, s, targetContent, deckGenParams, c.brokenObjects)
 
 	// apply the configuration update in Kong
@@ -788,6 +795,7 @@ func (c *KongClient) sendToClient(
 		client,
 		config,
 		targetContent,
+		customEntities,
 		c.prometheusMetrics,
 		c.updateStrategyResolver,
 		c.configChangeDetector,
diff --git a/internal/dataplane/kongstate/customentity.go b/internal/dataplane/kongstate/customentity.go
index de9fadf1d..9ded14097 100644
--- a/internal/dataplane/kongstate/customentity.go
+++ b/internal/dataplane/kongstate/customentity.go
@@ -1,6 +1,14 @@
 package kongstate
 
-import "github.com/kong/go-kong/kong"
+import (
+	"context"
+	"sort"
+
+	"github.com/kong/go-kong/kong"
+	"github.com/kong/go-kong/kong/custom"
+
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+)
 
 // EntityFieldType represents type of a Kong entity field.
 // possible field types include boolean, integer, number, string, array, set, map, record, json, foreign.
@@ -89,3 +97,63 @@ func ExtractEntityFieldDefinitions(schema kong.Schema) EntitySchema {
 	}
 	return retSchema
 }
+
+// IsKnownEntityType returns true if the entities of the type are "standard" and processed elsewhere in KIC.
+func IsKnownEntityType(entityType string) bool {
+	switch entityType {
+	case
+		// Types of standard Kong entities that are processed elsewhere in KIC.
+		// So the entities cannot be specified via KongCustomEntity types.
+		string(kong.EntityTypeServices),
+		string(kong.EntityTypeRoutes),
+		string(kong.EntityTypeUpstreams),
+		string(kong.EntityTypeTargets),
+		string(kong.EntityTypeConsumers),
+		string(kong.EntityTypeConsumerGroups),
+		string(kong.EntityTypePlugins):
+		return true
+	default:
+		return false
+	}
+}
+
+// KongCustomEntityCollection is a collection of custom Kong entities with the same type.
+type KongCustomEntityCollection struct {
+	// Schema is the Schema of the entity.
+	Schema EntitySchema `json:"-"`
+	// Entities is the list of entities in the collection.
+	Entities []CustomEntity
+}
+
+// CustomEntity saves content of a Kong custom entity with the pointer to the k8s resource translating to it.
+type CustomEntity struct {
+	custom.Object
+	// K8sKongCustomEntity refers to the KongCustomEntity resource that translate to it.
+	K8sKongCustomEntity *kongv1alpha1.KongCustomEntity
+}
+
+// SchemaGetter is the interface to fetch the schema of a Kong entity by its type.
+// Used for fetching schema of custom entity for filling "foreign" field referring to other entities.
+type SchemaGetter interface {
+	Get(ctx context.Context, entityType string) (kong.Schema, error)
+}
+
+// sortCustomEntities sorts the custom entities of each type.
+// Since there may not be a consistent field to identify an entity, here we sort them by the k8s namespace/name.
+func (ks *KongState) sortCustomEntities() {
+	for _, collection := range ks.CustomEntities {
+		sort.Slice(collection.Entities, func(i, j int) bool {
+			e1 := collection.Entities[i]
+			e2 := collection.Entities[j]
+			// Compare namespace first.
+			if e1.K8sKongCustomEntity.Namespace < e2.K8sKongCustomEntity.Namespace {
+				return true
+			}
+			if e1.K8sKongCustomEntity.Namespace > e2.K8sKongCustomEntity.Namespace {
+				return false
+			}
+			// If namespace are the same, compare name.
+			return e1.K8sKongCustomEntity.Name < e2.K8sKongCustomEntity.Name
+		})
+	}
+}
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index 75169adce..404e53674 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -1,7 +1,9 @@
 package kongstate
 
 import (
+	"context"
 	"crypto/sha256"
+	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
@@ -37,6 +39,8 @@ type KongState struct {
 	Consumers      []Consumer
 	ConsumerGroups []ConsumerGroup
 	Vaults         []Vault
+
+	CustomEntities map[string]*KongCustomEntityCollection
 }
 
 // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort.
@@ -51,9 +55,7 @@ func (ks *KongState) SanitizedCopy(uuidGenerator util.UUIDGenerator) *KongState
 			return
 		}(),
 		CACertificates: ks.CACertificates,
-		Plugins: lo.Map(ks.Plugins, func(p Plugin, _ int) Plugin {
-			return p.SanitizedCopy()
-		}),
+		Plugins:        ks.Plugins,
 		Consumers: func() (res []Consumer) {
 			for _, v := range ks.Consumers {
 				res = append(res, *v.SanitizedCopy(uuidGenerator))
@@ -68,6 +70,7 @@ func (ks *KongState) SanitizedCopy(uuidGenerator util.UUIDGenerator) *KongState
 		}(),
 		ConsumerGroups: ks.ConsumerGroups,
 		Vaults:         ks.Vaults,
+		CustomEntities: ks.CustomEntities,
 	}
 }
 
@@ -361,22 +364,10 @@ func (ks *KongState) getPluginRelations(cacheStore store.Storer, log logr.Logger
 		//
 		// Code in buildPlugins() will combine plugin associations into
 		// multi-entity plugins within the local namespace
-		namespace := referer.GetNamespace()
-		if plugin.Namespace != "" {
-			// remote KongPlugin, permitted if ReferenceGrant allows
-			if err := isRemotePluginReferenceAllowed(
-				cacheStore,
-				pluginReference{
-					Referer:   referer,
-					Namespace: plugin.Namespace,
-					Name:      plugin.Name,
-				},
-			); err != nil {
-				log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
-				return
-			}
-
-			namespace = plugin.Namespace
+		namespace, err := extractReferredPluginNamespace(cacheStore, referer, plugin)
+		if err != nil {
+			log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			return
 		}
 
 		pluginKey := namespace + ":" + plugin.Name
@@ -690,3 +681,305 @@ func maybeLogKongIngressDeprecationError(logger logr.Logger, services []*corev1.
 		}
 	}
 }
+
+// FillCustomEntities fills custom entities in KongState.
+func (ks *KongState) FillCustomEntities(
+	logger logr.Logger,
+	s store.Storer,
+	failuresCollector *failures.ResourceFailuresCollector,
+	schemaGetter SchemaGetter,
+	workspace string,
+) {
+	entities := s.ListKongCustomEntities()
+	if len(entities) == 0 {
+		return
+	}
+	logger = logger.WithName("fillCustomEntities")
+
+	if ks.CustomEntities == nil {
+		ks.CustomEntities = map[string]*KongCustomEntityCollection{}
+	}
+	// Fetch relations between plugins and services/routes/consumers and store the pointer to translated Kong entities.
+	// Used for fetching entity referred by a custom entity and fill the ID of referred entity.
+	pluginRels := ks.getPluginRelatedEntitiesRef(s, logger)
+
+	for _, entity := range entities {
+		// reject the custom entity if its type is in "known" entity types that are already processed.
+		if IsKnownEntityType(entity.Spec.EntityType) {
+			failuresCollector.PushResourceFailure(
+				fmt.Sprintf("cannot use known entity type %s in custom entity", entity.Spec.EntityType),
+				entity,
+			)
+			continue
+		}
+		// Fetch the entity schema.
+		schema, err := ks.fetchEntitySchema(schemaGetter, entity.Spec.EntityType)
+		if err != nil {
+			failuresCollector.PushResourceFailure(
+				fmt.Sprintf("failed to fetch entity schema for entity type %s: %v", entity.Spec.EntityType, err),
+				entity,
+			)
+			continue
+		}
+		// Unmarshal fields of the entity.
+		var parsedEntity map[string]interface{}
+		if err = json.Unmarshal(entity.Spec.Fields.Raw, &parsedEntity); err != nil {
+			failuresCollector.PushResourceFailure(fmt.Sprintf("failed to unmarshal fields of entity: %v", err), entity)
+			continue
+		}
+		// Fill the "foreign" fields if the entity has such fields referencing services/routes/consumers.
+		ks.fillCustomEntityForeignFields(logger, entity, schema, parsedEntity, pluginRels, workspace)
+		// Put the entity into the custom collection to store the entities of its type.
+		if _, ok := ks.CustomEntities[entity.Spec.EntityType]; !ok {
+			ks.CustomEntities[entity.Spec.EntityType] = &KongCustomEntityCollection{
+				Schema: schema,
+			}
+		}
+		collection := ks.CustomEntities[entity.Spec.EntityType]
+		collection.Entities = append(collection.Entities, CustomEntity{
+			Object:              parsedEntity,
+			K8sKongCustomEntity: entity,
+		})
+	}
+
+	ks.sortCustomEntities()
+}
+
+// fetchEntitySchema fetches schema of an entity by its type and stores the schema in its custom entity collection
+// as a cache to avoid excessive calling of Kong admin APIs.
+func (ks *KongState) fetchEntitySchema(schemaGetter SchemaGetter, entityType string) (EntitySchema, error) {
+	collection, ok := ks.CustomEntities[entityType]
+	if ok {
+		return collection.Schema, nil
+	}
+	// Use `context.Background()` here because `BuildKongConfig` does not provide a context.
+	schema, err := schemaGetter.Get(context.Background(), entityType)
+	if err != nil {
+		return EntitySchema{}, err
+	}
+	return ExtractEntityFieldDefinitions(schema), nil
+}
+
+// fillCustomEntityForeignFields fills the "foreign" fields of a custom Kong entity
+// if it refers to a service/route/consumer.
+// Because Kong gateway requires the "foreign" fields to use IDs of the referred entity,
+// it fills ID of the referred entity and fill the ID of the entity to the field.
+// So the function has the side effect that the referred entity will have a generated fixed ID.
+// It does not support fields referring to other types of entities.
+func (ks *KongState) fillCustomEntityForeignFields(
+	logger logr.Logger,
+	k8sEntity *kongv1alpha1.KongCustomEntity,
+	schema EntitySchema,
+	parsedEntity map[string]any,
+	pluginRelEntities PluginRelatedEntitiesRefs,
+	workspace string,
+) {
+	logger = logger.WithValues("entity_namespace", k8sEntity.Namespace, "entity_name", k8sEntity.Name)
+	// Find referred entity via the plugin in its spec.parentRef.
+	// Then we can fetch the referred service/route/consumer from the reference relations of the plugin.
+	parentRef := k8sEntity.Spec.ParentRef
+	var namespace string
+	// Abort if the parentRef is empty or does not refer to a plugin.
+	if parentRef == nil ||
+		(parentRef.Group == nil || *parentRef.Group != kongv1alpha1.GroupVersion.Group) {
+		return
+	}
+	if parentRef.Kind == nil || (*parentRef.Kind != "KongPlugin" && *parentRef.Kind != "KongClusterPlugin") {
+		return
+	}
+	// Extract the plugin key to get the plugin relations.
+	if parentRef.Namespace == nil || *parentRef.Namespace == "" {
+		namespace = k8sEntity.Namespace
+	} else {
+		namespace = *parentRef.Namespace
+	}
+	pluginKey := namespace + ":" + parentRef.Name
+	// Get the relations with other entities of the plugin.
+	rels, ok := pluginRelEntities.RelatedEntities[pluginKey]
+	if !ok {
+		return
+	}
+	logger.V(util.DebugLevel).Info("fetch references via plugin", "plugin_key", pluginKey)
+
+	// Traverse through the fields of the entity and fill the "foreign" fields with IDs of referring entities.
+	// Note: this procedure will make referred services'/routes'/consumers' ID to be filled.
+	// So it requires the `FillIDs` feature gate to be enabled.
+	for fieldName, field := range schema.Fields {
+		if field.Type != EntityFieldTypeForeign {
+			continue
+		}
+		switch field.Reference {
+		case string(kong.EntityTypeServices):
+			serviceIDs := getServiceIDFromPluginRels(logger, rels, pluginRelEntities.RouteAttachedService, workspace)
+			// TODO: we should generate multiple entities if the plugin is attached to multiple services/routes/consumers.
+			// https://github.com/Kong/kubernetes-ingress-controller/issues/6123
+			if len(serviceIDs) > 0 {
+				parsedEntity[fieldName] = map[string]interface{}{
+					"id": serviceIDs[0],
+				}
+				logger.V(util.DebugLevel).Info("added ref to service", "service_id", serviceIDs[0])
+			}
+		case string(kong.EntityTypeRoutes):
+			routeIDs := lo.FilterMap(rels.Routes, func(r *Route, _ int) (string, bool) {
+				if err := r.FillID(workspace); err != nil {
+					return "", false
+				}
+				return *r.ID, true
+			})
+			if len(routeIDs) > 0 {
+				parsedEntity[fieldName] = map[string]interface{}{
+					"id": routeIDs[0],
+				}
+				logger.V(util.DebugLevel).Info("added ref to route", "route_id", routeIDs[0])
+			}
+		case string(kong.EntityTypeConsumers):
+			consumerIDs := lo.FilterMap(rels.Consumers, func(c *Consumer, _ int) (string, bool) {
+				if err := c.FillID(workspace); err != nil {
+					return "", false
+				}
+				return *c.ID, true
+			})
+			if len(consumerIDs) > 0 {
+				parsedEntity[fieldName] = map[string]interface{}{
+					"id": consumerIDs[0],
+				}
+				logger.V(util.DebugLevel).Info("added ref to consumer", "consumer_id", consumerIDs[0])
+			}
+		}
+	}
+}
+
+// getServiceIDFromPluginRels returns the ID of the services which a plugin refers to in RelatedEntitiesRef.
+// It fills the IDs of services directly referred, and IDs of services where referred routes attaches to.
+func getServiceIDFromPluginRels(log logr.Logger, rels RelatedEntitiesRef, routeAttachedService map[string]*Service, workspace string) []string {
+	// Return IDs of directly referred services.
+	if len(rels.Services) > 0 {
+		return lo.FilterMap(rels.Services, func(s *Service, _ int) (string, bool) {
+			if err := s.FillID(workspace); err != nil {
+				log.Error(err, "failed to fill ID for service")
+				return "", false
+			}
+			return *s.ID, true
+		},
+		)
+	}
+	// Returns IDs of services where the referred routes attaches.
+	if len(rels.Routes) > 0 {
+		serviceIDs := lo.FilterMap(
+			rels.Routes, func(r *Route, _ int) (string, bool) {
+				svc, ok := routeAttachedService[*r.Name]
+				if !ok {
+					return "", false
+				}
+				if err := svc.FillID(workspace); err != nil {
+					log.Error(err, "failed to fill ID for service")
+					return "", false
+				}
+				return *svc.ID, true
+			},
+		)
+		return lo.Uniq(serviceIDs)
+	}
+	return nil
+}
+
+// getPluginRelatedEntitiesRef gets services/routes/consumers referred by each plugin and returns the pointer to them.
+// It is for the custom entities to fill the IDs of the referred entities into their "foreign" fields.
+// It basically does the same thing as getPluginRelations but stores the pointers
+// because we need to call the FillID method of the entities to fetch the ID,
+// as Kong gateway requires IDs in the "foreign" fields (not other identifiers such as name.)
+//
+// TODO: refactor the building of plugin related entities and share the result between here and building plugins:
+// https://github.com/Kong/kubernetes-ingress-controller/issues/6115
+func (ks *KongState) getPluginRelatedEntitiesRef(cacheStore store.Storer, log logr.Logger) PluginRelatedEntitiesRefs {
+	pluginRels := PluginRelatedEntitiesRefs{
+		RelatedEntities:      map[string]RelatedEntitiesRef{},
+		RouteAttachedService: map[string]*Service{},
+	}
+	addRelation := func(referer client.Object, plugin annotations.NamespacedKongPlugin, entity any) {
+		namespace, err := extractReferredPluginNamespace(cacheStore, referer, plugin)
+		if err != nil {
+			log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			return
+		}
+		pluginKey := namespace + ":" + plugin.Name
+		relations, ok := pluginRels.RelatedEntities[pluginKey]
+		if !ok {
+			relations = RelatedEntitiesRef{}
+		}
+		switch e := entity.(type) {
+		case *Consumer:
+			relations.Consumers = append(relations.Consumers, e)
+		case *Route:
+			relations.Routes = append(relations.Routes, e)
+		case *Service:
+			relations.Services = append(relations.Services, e)
+		}
+		pluginRels.RelatedEntities[pluginKey] = relations
+	}
+
+	for i := range ks.Services {
+		for _, svc := range ks.Services[i].K8sServices {
+			pluginList := annotations.ExtractNamespacedKongPluginsFromAnnotations(svc.GetAnnotations())
+			for _, plugin := range pluginList {
+				addRelation(svc, plugin, &ks.Services[i])
+			}
+		}
+
+		for j, r := range ks.Services[i].Routes {
+			ingress := ks.Services[i].Routes[j].Ingress
+			pluginList := annotations.ExtractNamespacedKongPluginsFromAnnotations(ingress.Annotations)
+			for _, plugin := range pluginList {
+				// Pretend we have a full Ingress struct for reference checks.
+				virtualIngress := netv1.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: ingress.Namespace,
+						Name:      ingress.Name,
+					},
+				}
+				addRelation(&virtualIngress, plugin, &ks.Services[i].Routes[j])
+				// For some entities, we need to find the referred service via route
+				// but the `Service` field of translated routes are empty because we do not want the field to appear in final declarative config.
+				// So we need to maintain a map from route name to service to find the service object.
+				pluginRels.RouteAttachedService[*r.Name] = &ks.Services[i]
+			}
+		}
+	}
+
+	for i, c := range ks.Consumers {
+		pluginList := annotations.ExtractNamespacedKongPluginsFromAnnotations(c.K8sKongConsumer.GetAnnotations())
+		for _, plugin := range pluginList {
+			addRelation(&c.K8sKongConsumer, plugin, &ks.Consumers[i])
+		}
+	}
+	return pluginRels
+}
+
+func extractReferredPluginNamespace(
+	cacheStore store.Storer, referer client.Object, plugin annotations.NamespacedKongPlugin,
+) (string, error) {
+	// There are 2 types of KongPlugin references: local and remote.
+	// A local reference is one where the KongPlugin is in the same namespace as the referer.
+	// A remote reference is one where the KongPlugin is in a different namespace.
+	// By default a KongPlugin is considered local.
+	// If the plugin has a namespace specified, it is considered remote.
+	//
+	// The referer is the entity that the KongPlugin is associated with.
+	if plugin.Namespace == "" {
+		return referer.GetNamespace(), nil
+	}
+
+	// remote KongPlugin, permitted if ReferenceGrant allows.
+	err := isRemotePluginReferenceAllowed(
+		cacheStore,
+		pluginReference{
+			Referer:   referer,
+			Namespace: plugin.Namespace,
+			Name:      plugin.Name,
+		},
+	)
+	if err != nil {
+		return "", err
+	}
+	return plugin.Namespace, nil
+}
diff --git a/internal/dataplane/kongstate/plugin.go b/internal/dataplane/kongstate/plugin.go
index 598b023f5..31ce8c582 100644
--- a/internal/dataplane/kongstate/plugin.go
+++ b/internal/dataplane/kongstate/plugin.go
@@ -4,14 +4,11 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"strings"
 
 	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/kong/go-kong/kong"
-	"github.com/samber/lo"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/yaml"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
@@ -19,107 +16,6 @@ import (
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 )
 
-// Plugin represents a plugin Object in Kong.
-type Plugin struct {
-	kong.Plugin
-	K8sParent           client.Object
-	SensitiveFieldsMeta PluginSensitiveFieldsMetadata
-}
-
-func (p Plugin) DeepCopy() Plugin {
-	return Plugin{
-		Plugin:              *p.Plugin.DeepCopy(),
-		K8sParent:           p.K8sParent,
-		SensitiveFieldsMeta: p.SensitiveFieldsMeta,
-	}
-}
-
-func (p Plugin) SanitizedCopy() Plugin {
-	// We do not want to return an error if any of below fails - the best we can do
-	// is to return a plugin with wholly redacted config.
-	// Let's have a closure returning a plugin with wholly redacted config prepared.
-	whollySanitized := func() Plugin {
-		p := p.DeepCopy()
-		p.Config = sanitizeWholePluginConfig(p.Config)
-		return p
-	}
-
-	// If the whole config is sensitive, we need to redact the entire config.
-	if p.SensitiveFieldsMeta.WholeConfigIsSensitive {
-		return whollySanitized()
-	}
-
-	// If there are JSON paths, we need to redact them.
-	if len(p.SensitiveFieldsMeta.JSONPaths) > 0 {
-		var patchOperations []string
-		for _, path := range p.SensitiveFieldsMeta.JSONPaths {
-			// If the path is empty, we need to sanitize the whole config.
-			// An empty path means that the patch is on the root of the config.
-			if path == "" {
-				return whollySanitized()
-			}
-
-			patchOperations = append(patchOperations, fmt.Sprintf(
-				`{"op":"replace","path":"%s","value":"%s"}`,
-				path,
-				*redactedString,
-			))
-		}
-
-		// Decode the patch and apply it to the config.
-		// We need to marshal the config to JSON and then unmarshal it back to Configuration
-		// because the patch library works with bytes.
-		patch, err := jsonpatch.DecodePatch([]byte(fmt.Sprintf("[%s]", strings.Join(patchOperations, ","))))
-		if err != nil {
-			return whollySanitized()
-		}
-		configB, err := json.Marshal(p.Config)
-		if err != nil {
-			return whollySanitized()
-		}
-		sanitizedConfigB, err := patch.Apply(configB)
-		if err != nil {
-			return whollySanitized()
-		}
-		sanitizedConfig := kong.Configuration{}
-		if err := json.Unmarshal(sanitizedConfigB, &sanitizedConfig); err != nil {
-			return whollySanitized()
-		}
-
-		sanitized := p.DeepCopy()
-		sanitized.Config = sanitizedConfig
-		return sanitized
-	}
-
-	// Nothing to sanitize.
-	return p
-}
-
-// sanitizeWholePluginConfig redacts the entire config of a plugin by replacing all of its
-// values with a redacted string.
-func sanitizeWholePluginConfig(config kong.Configuration) kong.Configuration {
-	sanitized := config.DeepCopy()
-	for k := range config {
-		sanitized[k] = *redactedString
-	}
-	return sanitized
-}
-
-// PluginSensitiveFieldsMetadata holds metadata about sensitive fields in a plugin's configuration.
-// It can be used to sanitize them before exposing the configuration to the user (e.g. in debug dumps
-// or in Konnect Admin API).
-type PluginSensitiveFieldsMetadata struct {
-	// WholeConfigIsSensitive indicates that the entire configuration of the plugin is sensitive.
-	// If this is true, the configuration should be redacted entirely (each of its fields' values
-	// should be replaced with a redacted string).
-	WholeConfigIsSensitive bool
-
-	// JSONPaths holds a list of JSON paths to sensitive fields in the plugin's configuration.
-	// If this is not empty, the configuration should be redacted by replacing the values of the
-	// fields at these paths with a redacted string.
-	JSONPaths []string
-}
-
 // getKongPluginOrKongClusterPlugin fetches a KongPlugin or KongClusterPlugin (as fallback) from the store.
 // If both are not found, an error is returned.
 func getKongPluginOrKongClusterPlugin(s store.Storer, namespace, name string) (
@@ -181,14 +77,6 @@ func kongPluginFromK8SClusterPlugin(
 		}
 	}
 
-	// Prepare sensitive fields metadata for the plugin.
-	sensitiveFieldsMeta := PluginSensitiveFieldsMetadata{
-		JSONPaths: lo.Map(k8sPlugin.ConfigPatches, func(patch kongv1.NamespacedConfigPatch, _ int) string {
-			return patch.Path
-		}),
-		WholeConfigIsSensitive: k8sPlugin.ConfigFrom != nil,
-	}
-
 	return Plugin{
 		Plugin: plugin{
 			Name:   k8sPlugin.PluginName,
@@ -201,8 +89,7 @@ func kongPluginFromK8SClusterPlugin(
 			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
 			Tags:         util.GenerateTagsForObject(&k8sPlugin),
 		}.toKongPlugin(),
-		K8sParent:           &k8sPlugin,
-		SensitiveFieldsMeta: sensitiveFieldsMeta,
+		K8sParent: &k8sPlugin,
 	}, nil
 }
 
@@ -244,14 +131,6 @@ func kongPluginFromK8SPlugin(
 		}
 	}
 
-	// Prepare sensitive fields metadata for the plugin.
-	sensitiveFieldsMeta := PluginSensitiveFieldsMetadata{
-		JSONPaths: lo.Map(k8sPlugin.ConfigPatches, func(patch kongv1.ConfigPatch, _ int) string {
-			return patch.Path
-		}),
-		WholeConfigIsSensitive: k8sPlugin.ConfigFrom != nil,
-	}
-
 	return Plugin{
 		Plugin: plugin{
 			Name:   k8sPlugin.PluginName,
@@ -264,8 +143,7 @@ func kongPluginFromK8SPlugin(
 			Protocols:    protocolsToStrings(k8sPlugin.Protocols),
 			Tags:         util.GenerateTagsForObject(&k8sPlugin),
 		}.toKongPlugin(),
-		K8sParent:           &k8sPlugin,
-		SensitiveFieldsMeta: sensitiveFieldsMeta,
+		K8sParent: &k8sPlugin,
 	}, nil
 }
 
@@ -463,3 +341,14 @@ func (p plugin) toKongPlugin() kong.Plugin {
 	}
 	return result
 }
+
+type RelatedEntitiesRef struct {
+	Services  []*Service
+	Routes    []*Route
+	Consumers []*Consumer
+}
+
+type PluginRelatedEntitiesRefs struct {
+	RelatedEntities      map[string]RelatedEntitiesRef
+	RouteAttachedService map[string]*Service
+}
diff --git a/internal/dataplane/kongstate/types.go b/internal/dataplane/kongstate/types.go
index 282d81081..fcfa7afe5 100644
--- a/internal/dataplane/kongstate/types.go
+++ b/internal/dataplane/kongstate/types.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/kong/go-kong/kong"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 type PortMode int
@@ -65,3 +66,16 @@ func (c *Certificate) SanitizedCopy() *Certificate {
 		},
 	}
 }
+
+// Plugin represents a plugin Object in Kong.
+type Plugin struct {
+	kong.Plugin
+	K8sParent client.Object
+}
+
+func (p Plugin) DeepCopy() Plugin {
+	return Plugin{
+		Plugin:    *p.Plugin.DeepCopy(),
+		K8sParent: p.K8sParent,
+	}
+}
diff --git a/internal/dataplane/sendconfig/inmemory.go b/internal/dataplane/sendconfig/inmemory.go
index 2da7f8a7e..21699002f 100644
--- a/internal/dataplane/sendconfig/inmemory.go
+++ b/internal/dataplane/sendconfig/inmemory.go
@@ -11,6 +11,7 @@ import (
 	"github.com/kong/go-database-reconciler/pkg/file"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/metrics"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 )
 
 type ConfigService interface {
@@ -54,6 +55,22 @@ func (s UpdateStrategyInMemory) Update(ctx context.Context, targetState ContentW
 	if err != nil {
 		return fmt.Errorf("constructing kong configuration: %w", err)
 	}
+
+	if len(targetState.CustomEntities) > 0 {
+		unmarshaledConfig := map[string]any{}
+		if err := json.Unmarshal(config, &unmarshaledConfig); err != nil {
+			return fmt.Errorf("unmarshaling config for adding custom entities: %w", err)
+		}
+		for entityType, entities := range targetState.CustomEntities {
+			unmarshaledConfig[entityType] = entities
+			s.logger.V(util.DebugLevel).Info("Filled custom entities", "entity_type", entityType)
+		}
+		config, err = json.Marshal(unmarshaledConfig)
+		if err != nil {
+			return fmt.Errorf("constructing kong configuration again with custom entities: %w", err)
+		}
+	}
+
 	if errBody, reloadConfigErr := s.configService.ReloadDeclarativeRawConfig(ctx, bytes.NewReader(config), true, true); reloadConfigErr != nil {
 		resourceErrors, parseErr := parseFlatEntityErrors(errBody, s.logger)
 		if parseErr != nil {
diff --git a/internal/dataplane/sendconfig/sendconfig.go b/internal/dataplane/sendconfig/sendconfig.go
index d6fd1174c..7946ce21b 100644
--- a/internal/dataplane/sendconfig/sendconfig.go
+++ b/internal/dataplane/sendconfig/sendconfig.go
@@ -41,13 +41,14 @@ func PerformUpdate(
 	client AdminAPIClient,
 	config Config,
 	targetContent *file.Content,
+	customEntities CustomEntitiesByType,
 	promMetrics *metrics.CtrlFuncMetrics,
 	updateStrategyResolver UpdateStrategyResolver,
 	configChangeDetector ConfigurationChangeDetector,
 	isFallback bool,
 ) ([]byte, error) {
 	oldSHA := client.LastConfigSHA()
-	newSHA, err := deckgen.GenerateSHA(targetContent)
+	newSHA, err := deckgen.GenerateSHA(targetContent, customEntities)
 	if err != nil {
 		return oldSHA, fmt.Errorf("failed to generate SHA for target content: %w", err)
 	}
@@ -72,8 +73,9 @@ func PerformUpdate(
 	logger = logger.WithValues("update_strategy", updateStrategy.Type())
 	timeStart := time.Now()
 	err = updateStrategy.Update(ctx, ContentWithHash{
-		Content: targetContent,
-		Hash:    newSHA,
+		Content:        targetContent,
+		CustomEntities: customEntities,
+		Hash:           newSHA,
 	})
 	duration := time.Since(timeStart)
 
diff --git a/internal/dataplane/sendconfig/strategy.go b/internal/dataplane/sendconfig/strategy.go
index c81dd5f60..2dc02ea36 100644
--- a/internal/dataplane/sendconfig/strategy.go
+++ b/internal/dataplane/sendconfig/strategy.go
@@ -7,15 +7,22 @@ import (
 	"github.com/kong/go-database-reconciler/pkg/dump"
 	"github.com/kong/go-database-reconciler/pkg/file"
 	"github.com/kong/go-kong/kong"
+	"github.com/kong/go-kong/kong/custom"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/adminapi"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/metrics"
 )
 
+// CustomEntitiesByType stores all custom entities by types.
+// The key is the type of the entity,
+// and the corresponding slice stores the sorted list of custom entities with that type.
+type CustomEntitiesByType map[string][]custom.Object
+
 // ContentWithHash encapsulates file.Content along with its precalculated hash.
 type ContentWithHash struct {
-	Content *file.Content
-	Hash    []byte
+	Content        *file.Content
+	CustomEntities CustomEntitiesByType
+	Hash           []byte
 }
 
 // UpdateStrategy is the way we approach updating data-plane's configuration, depending on its type.
diff --git a/internal/dataplane/translator/translator.go b/internal/dataplane/translator/translator.go
index 4806ea0d4..bac5e82f5 100644
--- a/internal/dataplane/translator/translator.go
+++ b/internal/dataplane/translator/translator.go
@@ -1,7 +1,11 @@
 package translator
 
 import (
+	"context"
+	"errors"
+
 	"github.com/go-logr/logr"
+	"github.com/kong/go-kong/kong"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	dpconf "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/config"
@@ -45,6 +49,9 @@ type FeatureFlags struct {
 
 	// KongServiceFacade indicates whether we should support KongServiceFacades as Ingress backends.
 	KongServiceFacade bool
+
+	// KongCustomEntity indicates whether we should support translating custom entities from KongCustomEntity CRs.
+	KongCustomEntity bool
 }
 
 func NewFeatureFlags(
@@ -60,9 +67,15 @@ func NewFeatureFlags(
 		FillIDs:                           featureGates.Enabled(featuregates.FillIDsFeature),
 		RewriteURIs:                       featureGates.Enabled(featuregates.RewriteURIsFeature),
 		KongServiceFacade:                 featureGates.Enabled(featuregates.KongServiceFacade),
+		KongCustomEntity:                  featureGates.Enabled(featuregates.KongCustomEntity),
 	}
 }
 
+// SchemaServiceProvider returns a kong schema service required for translating custom entities.
+type SchemaServiceProvider interface {
+	GetSchemaService() kong.AbstractSchemaService
+}
+
 // Translator translates Kubernetes objects and configurations into their
 // equivalent Kong objects and configurations, producing a complete
 // state configuration for the Kong Admin API.
@@ -73,6 +86,9 @@ type Translator struct {
 	licenseGetter license.Getter
 	featureFlags  FeatureFlags
 
+	// schemaServiceProvider provides the schema service required for fetching schemas of custom entities.
+	schemaServiceProvider SchemaServiceProvider
+
 	failuresCollector          *failures.ResourceFailuresCollector
 	translatedObjectsCollector *ObjectsCollector
 }
@@ -84,6 +100,7 @@ func NewTranslator(
 	storer store.Storer,
 	workspace string,
 	featureFlags FeatureFlags,
+	schemaServiceProvider SchemaServiceProvider,
 ) (*Translator, error) {
 	failuresCollector := failures.NewResourceFailuresCollector(logger)
 
@@ -98,6 +115,7 @@ func NewTranslator(
 		storer:                     storer,
 		workspace:                  workspace,
 		featureFlags:               featureFlags,
+		schemaServiceProvider:      schemaServiceProvider,
 		failuresCollector:          failuresCollector,
 		translatedObjectsCollector: translatedObjectsCollector,
 	}, nil
@@ -188,6 +206,17 @@ func (t *Translator) BuildKongConfig() KongConfigBuildingResult {
 		t.registerSuccessfullyTranslatedObject(result.Plugins[i].K8sParent)
 	}
 
+	// process custom entities
+	if t.featureFlags.KongCustomEntity {
+		result.FillCustomEntities(t.logger, t.storer, t.failuresCollector, t.schemaServiceProvider.GetSchemaService(), t.workspace)
+		// Register successcully translated KCEs to set the status of these KCEs.
+		for _, collection := range result.CustomEntities {
+			for i := range collection.Entities {
+				t.registerSuccessfullyTranslatedObject(collection.Entities[i].K8sKongCustomEntity)
+			}
+		}
+	}
+
 	// generate Certificates and SNIs
 	ingressCerts := t.getCerts(ingressRules.SecretNameToSNIs)
 	gatewayCerts := t.getGatewayCerts()
@@ -249,3 +278,17 @@ func (t *Translator) registerSuccessfullyTranslatedObject(obj client.Object) {
 func (t *Translator) popConfiguredKubernetesObjects() []client.Object {
 	return t.translatedObjectsCollector.Pop()
 }
+
+// UnavailableSchemaService is a fake schema service used when no gateway admin API clients available.
+// It always returns error in its Get and Validate methods.
+type UnavailableSchemaService struct{}
+
+var _ kong.AbstractSchemaService = UnavailableSchemaService{}
+
+func (s UnavailableSchemaService) Get(_ context.Context, _ string) (kong.Schema, error) {
+	return nil, errors.New("schema service unavailable")
+}
+
+func (s UnavailableSchemaService) Validate(_ context.Context, _ kong.EntityType, _ any) (bool, string, error) {
+	return false, "", errors.New("schema service unavailable")
+}
diff --git a/internal/manager/config.go b/internal/manager/config.go
index aa1df49a7..ab2b85310 100644
--- a/internal/manager/config.go
+++ b/internal/manager/config.go
@@ -116,6 +116,7 @@ type Config struct {
 	KongServiceFacadeEnabled      bool
 	KongVaultEnabled              bool
 	KongLicenseEnabled            bool
+	KongCustomEntityEnabled       bool
 
 	// Gateway API toggling.
 	GatewayAPIGatewayController        bool
@@ -268,6 +269,7 @@ func (c *Config) FlagSet() *pflag.FlagSet {
 	flagSet.BoolVar(&c.KongServiceFacadeEnabled, "enable-controller-kong-service-facade", true, "Enable the KongServiceFacade controller.")
 	flagSet.BoolVar(&c.KongVaultEnabled, "enable-controller-kong-vault", true, "Enable the KongVault controller.")
 	flagSet.BoolVar(&c.KongLicenseEnabled, "enable-controller-kong-license", true, "Enable the KongLicense controller.")
+	flagSet.BoolVar(&c.KongCustomEntityEnabled, "enable-controller-kong-custom-entity", true, "Enable the KongCustomEntity controller.")
 
 	// Admission Webhook server config
 	flagSet.StringVar(&c.AdmissionServer.ListenAddr, "admission-webhook-listen", "off",
diff --git a/internal/manager/controllerdef.go b/internal/manager/controllerdef.go
index 5f5bdd68d..a5adfd348 100644
--- a/internal/manager/controllerdef.go
+++ b/internal/manager/controllerdef.go
@@ -296,6 +296,20 @@ func setupControllers(
 				StatusQueue:                kubernetesStatusQueue,
 			},
 		},
+		{
+			Enabled: featureGates.Enabled(featuregates.KongCustomEntity) && c.KongCustomEntityEnabled,
+			Controller: &configuration.KongV1Alpha1KongCustomEntityReconciler{
+				Client:           mgr.GetClient(),
+				Log:              ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongCustomEntity"),
+				DataplaneClient:  dataplaneClient,
+				CacheSyncTimeout: c.CacheSyncTimeout,
+				IngressClassName: c.IngressClassName,
+				// KongCustomEntities do not accept entities without `kubernetes.io/ingress.class` annotation
+				// even the controlled ingress class is the default to avoid putting resources not managed in,.
+				DisableIngressClassLookups: true,
+				StatusQueue:                kubernetesStatusQueue,
+			},
+		},
 		// ---------------------------------------------------------------------------
 		// Gateway API Controllers
 		// ---------------------------------------------------------------------------
diff --git a/internal/manager/featuregates/feature_gates.go b/internal/manager/featuregates/feature_gates.go
index 237bfcc8c..914ae273e 100644
--- a/internal/manager/featuregates/feature_gates.go
+++ b/internal/manager/featuregates/feature_gates.go
@@ -32,6 +32,13 @@ const (
 	// of entity errors returned by the Kong Admin API.
 	FallbackConfiguration = "FallbackConfiguration"
 
+	// KongCustomEntity is the name of the feature-gate for enabling KongCustomEntity CR reconciliation
+	// for configuring custom Kong entities that KIC does not support yet.
+	// Requires feature gate `FillIDs` to be enabled.
+	// TODO: enable the feature gate by default when ready:
+	// https://github.com/Kong/kubernetes-ingress-controller/issues/6124
+	KongCustomEntity = "KongCustomEntity"
+
 	// 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"
 )
@@ -53,6 +60,11 @@ func New(setupLog logr.Logger, featureGates map[string]bool) (FeatureGates, erro
 		ctrlMap[feature] = enabled
 	}
 
+	// KongCustomEntity requires FillIDs to be enabled, because custom entities requires stable IDs to fill in its "foreign" fields.
+	if ctrlMap.Enabled(KongCustomEntity) && !ctrlMap.Enabled(FillIDsFeature) {
+		return nil, fmt.Errorf("%s is required if %s is enabled", FillIDsFeature, KongCustomEntity)
+	}
+
 	return ctrlMap, nil
 }
 
@@ -73,5 +85,6 @@ func GetFeatureGatesDefaults() FeatureGates {
 		KongServiceFacade:          false,
 		SanitizeKonnectConfigDumps: true,
 		FallbackConfiguration:      false,
+		KongCustomEntity:           false,
 	}
 }
diff --git a/internal/manager/run.go b/internal/manager/run.go
index 335841972..8d4f1d9a1 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -64,7 +64,6 @@ func Run(
 	if err != nil {
 		return fmt.Errorf("failed to configure feature gates: %w", err)
 	}
-
 	setupLog.Info("Getting the kubernetes client configuration")
 	kubeconfig, err := c.GetKubeconfig()
 	if err != nil {
@@ -180,7 +179,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, NewSchemaServiceGetter(clientsManager))
 	if err != nil {
 		return fmt.Errorf("failed to create translator: %w", err)
 	}
diff --git a/internal/manager/schema_service.go b/internal/manager/schema_service.go
new file mode 100644
index 000000000..183af19a7
--- /dev/null
+++ b/internal/manager/schema_service.go
@@ -0,0 +1,36 @@
+package manager
+
+import (
+	"github.com/kong/go-kong/kong"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/adminapi"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator"
+)
+
+// GatewayClientsProvider is an interface that provides clients for the currently discovered Gateway instances.
+type GatewayClientsProvider interface {
+	GatewayClients() []*adminapi.Client
+}
+
+// SchemaServiceGetter returns schema service of an admin API client if there is any client available.
+type SchemaServiceGetter struct {
+	clientsManager GatewayClientsProvider
+}
+
+// NewSchemaServiceGetter creates a schema service getter that uses given client manager to maintain admin API clients.
+func NewSchemaServiceGetter(cm GatewayClientsProvider) SchemaServiceGetter {
+	return SchemaServiceGetter{
+		clientsManager: cm,
+	}
+}
+
+// GetSchemaService returns the schema service for admin API client.
+// It uses the configured clients manager to get the clients and then it uses one of those to obtain the service.
+func (ssg SchemaServiceGetter) GetSchemaService() kong.AbstractSchemaService {
+	clients := ssg.clientsManager.GatewayClients()
+	if len(clients) > 0 {
+		return clients[0].AdminAPIClient().Schemas
+	}
+	// returns a fake schema service when no gateway clients available.
+	return translator.UnavailableSchemaService{}
+}
diff --git a/internal/store/fake_store.go b/internal/store/fake_store.go
index ba65dacfc..142488106 100644
--- a/internal/store/fake_store.go
+++ b/internal/store/fake_store.go
@@ -53,6 +53,7 @@ type FakeObjects struct {
 	KongUpstreamPolicies           []*kongv1beta1.KongUpstreamPolicy
 	KongServiceFacades             []*incubatorv1alpha1.KongServiceFacade
 	KongVaults                     []*kongv1alpha1.KongVault
+	KongCustomEntities             []*kongv1alpha1.KongCustomEntity
 }
 
 // NewFakeStore creates a store backed by the objects passed in as arguments.
@@ -214,6 +215,12 @@ func NewFakeStore(
 			return nil, err
 		}
 	}
+	kongCustomEntityStore := cache.NewStore(namespacedKeyFunc)
+	for _, e := range objects.KongCustomEntities {
+		if err := kongCustomEntityStore.Add(e); err != nil {
+			return nil, err
+		}
+	}
 
 	s = &Store{
 		stores: CacheStores{
@@ -240,6 +247,7 @@ func NewFakeStore(
 			KongUpstreamPolicy:             kongUpstreamPolicyStore,
 			KongServiceFacade:              kongServiceFacade,
 			KongVault:                      kongVaultStore,
+			KongCustomEntity:               kongCustomEntityStore,
 		},
 		ingressClass:          annotations.DefaultIngressClass,
 		isValidIngressClass:   annotations.IngressClassValidatorFuncFromObjectMeta(annotations.DefaultIngressClass),
@@ -277,6 +285,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) {
 		reflect.TypeOf(&kongv1.KongConsumer{}):                 kongv1.SchemeGroupVersion.WithKind("KongConsumer"),
 		reflect.TypeOf(&kongv1beta1.KongConsumerGroup{}):       kongv1beta1.SchemeGroupVersion.WithKind("KongConsumerGroup"),
 		reflect.TypeOf(&kongv1alpha1.KongVault{}):              kongv1alpha1.SchemeGroupVersion.WithKind(kongv1alpha1.KongVaultKind),
+		reflect.TypeOf(&kongv1alpha1.KongCustomEntity{}):       kongv1alpha1.SchemeGroupVersion.WithKind(kongv1alpha1.KongCustomEntityKind),
 	}
 
 	out := &bytes.Buffer{}
@@ -320,6 +329,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) {
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumers)...)
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumerGroups)...)
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongVaults)...)
+	allObjects = append(allObjects, lo.ToAnySlice(objects.KongCustomEntities)...)
 
 	for _, obj := range allObjects {
 		if err := fillGVKAndAppendToBuffer(obj.(runtime.Object)); err != nil {
diff --git a/internal/store/store.go b/internal/store/store.go
index 86972f481..f7e30bc2d 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -73,6 +73,7 @@ type Storer interface {
 	GetKongUpstreamPolicy(namespace, name string) (*kongv1beta1.KongUpstreamPolicy, error)
 	GetKongServiceFacade(namespace, name string) (*incubatorv1alpha1.KongServiceFacade, error)
 	GetKongVault(name string) (*kongv1alpha1.KongVault, error)
+	GetKongCustomEntity(namespace, name string) (*kongv1alpha1.KongCustomEntity, error)
 
 	ListIngressesV1() []*netv1.Ingress
 	ListIngressClassesV1() []*netv1.IngressClass
@@ -93,6 +94,7 @@ type Storer interface {
 	ListKongConsumerGroups() []*kongv1beta1.KongConsumerGroup
 	ListCACerts() ([]*corev1.Secret, error)
 	ListKongVaults() []*kongv1alpha1.KongVault
+	ListKongCustomEntities() []*kongv1alpha1.KongCustomEntity
 }
 
 // Store implements Storer and can be used to list Ingress, Services
@@ -562,6 +564,18 @@ func (s Store) GetKongVault(name string) (*kongv1alpha1.KongVault, error) {
 	return p.(*kongv1alpha1.KongVault), nil
 }
 
+func (s Store) GetKongCustomEntity(namespace, name string) (*kongv1alpha1.KongCustomEntity, error) {
+	key := fmt.Sprintf("%v/%v", namespace, name)
+	e, exists, err := s.stores.KongCustomEntity.GetByKey(key)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, NotFoundError{fmt.Sprintf("KongCustomEntity %s/%s not found", namespace, name)}
+	}
+	return e.(*kongv1alpha1.KongCustomEntity), nil
+}
+
 // ListKongConsumers returns all KongConsumers filtered by the ingress.class
 // annotation.
 func (s Store) ListKongConsumers() []*kongv1.KongConsumer {
@@ -670,6 +684,21 @@ func (s Store) ListKongVaults() []*kongv1alpha1.KongVault {
 	return kongVaults
 }
 
+func (s Store) ListKongCustomEntities() []*kongv1alpha1.KongCustomEntity {
+	var kongCustomEntities []*kongv1alpha1.KongCustomEntity
+	for _, obj := range s.stores.KongCustomEntity.List() {
+		kongCustomEntity, ok := obj.(*kongv1alpha1.KongCustomEntity)
+		if ok && s.isValidIngressClass(
+			&metav1.ObjectMeta{
+				Annotations: map[string]string{annotations.IngressClassKey: kongCustomEntity.Spec.ControllerName},
+			}, annotations.IngressClassKey, s.getIngressClassHandling(),
+		) {
+			kongCustomEntities = append(kongCustomEntities, kongCustomEntity)
+		}
+	}
+	return kongCustomEntities
+}
+
 // getIngressClassHandling returns annotations.ExactOrEmptyClassMatch if an IngressClass is the default class, or
 // annotations.ExactClassMatch if the IngressClass is not default or does not exist.
 func (s Store) getIngressClassHandling() annotations.ClassMatching {
@@ -760,6 +789,8 @@ func mkObjFromGVK(gvk schema.GroupVersionKind) (runtime.Object, error) {
 		return &kongv1beta1.KongUpstreamPolicy{}, nil
 	case incubatorv1alpha1.SchemeGroupVersion.WithKind("KongServiceFacade"):
 		return &incubatorv1alpha1.KongServiceFacade{}, nil
+	case kongv1alpha1.GroupVersion.WithKind(kongv1alpha1.KongCustomEntityKind):
+		return &kongv1alpha1.KongCustomEntity{}, nil
 	default:
 		return nil, fmt.Errorf("%s is not a supported runtime.Object", gvk)
 	}
diff --git a/internal/store/zz_generated_cache_stores.go b/internal/store/zz_generated_cache_stores.go
index 2df2e6615..eaf237e7a 100644
--- a/internal/store/zz_generated_cache_stores.go
+++ b/internal/store/zz_generated_cache_stores.go
@@ -46,6 +46,7 @@ type CacheStores struct {
 	IngressClassParametersV1alpha1 cache.Store
 	KongServiceFacade              cache.Store
 	KongVault                      cache.Store
+	KongCustomEntity               cache.Store
 
 	l *sync.RWMutex
 }
@@ -76,6 +77,7 @@ func NewCacheStores() CacheStores {
 		IngressClassParametersV1alpha1: cache.NewStore(namespacedKeyFunc),
 		KongServiceFacade:              cache.NewStore(namespacedKeyFunc),
 		KongVault:                      cache.NewStore(clusterWideKeyFunc),
+		KongCustomEntity:               cache.NewStore(namespacedKeyFunc),
 
 		l: &sync.RWMutex{},
 	}
@@ -133,6 +135,8 @@ func (c CacheStores) Get(obj runtime.Object) (item interface{}, exists bool, err
 		return c.KongServiceFacade.Get(obj)
 	case *kongv1alpha1.KongVault:
 		return c.KongVault.Get(obj)
+	case *kongv1alpha1.KongCustomEntity:
+		return c.KongCustomEntity.Get(obj)
 	}
 	return nil, false, fmt.Errorf("%T is not a supported cache object type", obj)
 }
@@ -190,6 +194,8 @@ func (c CacheStores) Add(obj runtime.Object) error {
 		return c.KongServiceFacade.Add(obj)
 	case *kongv1alpha1.KongVault:
 		return c.KongVault.Add(obj)
+	case *kongv1alpha1.KongCustomEntity:
+		return c.KongCustomEntity.Add(obj)
 	}
 	return fmt.Errorf("cannot add unsupported kind %q to the store", obj.GetObjectKind().GroupVersionKind())
 }
@@ -247,6 +253,8 @@ func (c CacheStores) Delete(obj runtime.Object) error {
 		return c.KongServiceFacade.Delete(obj)
 	case *kongv1alpha1.KongVault:
 		return c.KongVault.Delete(obj)
+	case *kongv1alpha1.KongCustomEntity:
+		return c.KongCustomEntity.Delete(obj)
 	}
 	return fmt.Errorf("cannot delete unsupported kind %q from the store", obj.GetObjectKind().GroupVersionKind())
 }
@@ -277,6 +285,7 @@ func (c CacheStores) ListAllStores() []cache.Store {
 		c.IngressClassParametersV1alpha1,
 		c.KongServiceFacade,
 		c.KongVault,
+		c.KongCustomEntity,
 	}
 }
 
@@ -306,5 +315,6 @@ func (c CacheStores) SupportedTypes() []client.Object {
 		&kongv1alpha1.IngressClassParameters{},
 		&incubatorv1alpha1.KongServiceFacade{},
 		&kongv1alpha1.KongVault{},
+		&kongv1alpha1.KongCustomEntity{},
 	}
 }


Test patch:
diff --git a/internal/dataplane/configfetcher/kongrawstate_test.go b/internal/dataplane/configfetcher/kongrawstate_test.go
index 338a162b2..efed5783b 100644
--- a/internal/dataplane/configfetcher/kongrawstate_test.go
+++ b/internal/dataplane/configfetcher/kongrawstate_test.go
@@ -344,6 +344,9 @@ func ensureAllKongStateFieldsAreTested(t *testing.T, testedFields []string) {
 		"Plugins",
 		// Licenses are injected from the license getter rather than extracted from the last state.
 		"Licenses",
+		// CustomEntities are not supported yet because go-database-reconciler does not include custom entities.
+		// TODO: support custom entities: https://github.com/Kong/kubernetes-ingress-controller/issues/6054
+		"CustomEntities",
 	}
 	allKongStateFields := func() []string {
 		var fields []string
diff --git a/internal/dataplane/kongstate/customentity_test.go b/internal/dataplane/kongstate/customentity_test.go
index ff754de6d..64ff4d049 100644
--- a/internal/dataplane/kongstate/customentity_test.go
+++ b/internal/dataplane/kongstate/customentity_test.go
@@ -4,7 +4,11 @@ import (
 	"testing"
 
 	"github.com/kong/go-kong/kong"
+	"github.com/kong/go-kong/kong/custom"
 	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 )
 
 func TestExtractEntityFieldDefinitions(t *testing.T) {
@@ -98,3 +102,109 @@ func TestExtractEntityFieldDefinitions(t *testing.T) {
 		})
 	}
 }
+
+func TestSortCustomEntities(t *testing.T) {
+	tesCases := []struct {
+		name                    string
+		customEntityCollections map[string]*KongCustomEntityCollection
+		sortedCollections       map[string]*KongCustomEntityCollection
+	}{
+		{
+			name: "custom entities in multiple namespaces",
+			customEntityCollections: map[string]*KongCustomEntityCollection{
+				"foo": {
+					Entities: []CustomEntity{
+						{
+							Object: custom.Object{
+								"name": "e1",
+								"key":  "value1",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "aab",
+									Namespace: "bbb",
+								},
+							},
+						},
+						{
+							Object: custom.Object{
+								"name": "e2",
+								"key":  "value2",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "abc",
+									Namespace: "bbb",
+								},
+							},
+						},
+						{
+							Object: custom.Object{
+								"name": "e3",
+								"key":  "value3",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "abc",
+									Namespace: "aaa",
+								},
+							},
+						},
+					},
+				},
+			},
+			sortedCollections: map[string]*KongCustomEntityCollection{
+				"foo": {
+					Entities: []CustomEntity{
+						{
+							Object: custom.Object{
+								"name": "e3",
+								"key":  "value3",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "abc",
+									Namespace: "aaa",
+								},
+							},
+						},
+						{
+							Object: custom.Object{
+								"name": "e1",
+								"key":  "value1",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "aab",
+									Namespace: "bbb",
+								},
+							},
+						},
+						{
+							Object: custom.Object{
+								"name": "e2",
+								"key":  "value2",
+							},
+							K8sKongCustomEntity: &kongv1alpha1.KongCustomEntity{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "abc",
+									Namespace: "bbb",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tesCases {
+		t.Run(tc.name, func(t *testing.T) {
+			ks := &KongState{
+				CustomEntities: tc.customEntityCollections,
+			}
+			ks.sortCustomEntities()
+			require.Equal(t, tc.sortedCollections, ks.CustomEntities)
+		})
+	}
+}
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index b9bbcf21b..6ab9c89c6 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -1,6 +1,7 @@
 package kongstate
 
 import (
+	"context"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -12,6 +13,7 @@ import (
 	"github.com/go-logr/logr/testr"
 	"github.com/go-logr/zapr"
 	"github.com/kong/go-kong/kong"
+	"github.com/kong/go-kong/kong/custom"
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -58,10 +60,7 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 				Upstreams:      []Upstream{{Upstream: kong.Upstream{ID: kong.String("1")}}},
 				Certificates:   []Certificate{{Certificate: kong.Certificate{ID: kong.String("1"), Key: kong.String("secret")}}},
 				CACertificates: []kong.CACertificate{{ID: kong.String("1")}},
-				Plugins: []Plugin{{
-					SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{WholeConfigIsSensitive: true},
-					Plugin:              kong.Plugin{ID: kong.String("1"), Config: kong.Configuration{"secret": "secretValue"}},
-				}},
+				Plugins:        []Plugin{{Plugin: kong.Plugin{ID: kong.String("1"), Config: map[string]interface{}{"key": "secret"}}}},
 				Consumers: []Consumer{{
 					KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: kong.String("secret")}}},
 				}},
@@ -76,16 +75,32 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 						},
 					},
 				},
+				CustomEntities: map[string]*KongCustomEntityCollection{
+					"test_entities": {
+						Schema: EntitySchema{
+							Fields: map[string]EntityField{
+								"name": {
+									Type:     EntityFieldTypeString,
+									Required: true,
+								},
+							},
+						},
+						Entities: []CustomEntity{
+							{
+								Object: map[string]interface{}{
+									"name": "foo",
+								},
+							},
+						},
+					},
+				},
 			},
 			want: KongState{
 				Services:       []Service{{Service: kong.Service{ID: kong.String("1")}}},
 				Upstreams:      []Upstream{{Upstream: kong.Upstream{ID: kong.String("1")}}},
 				Certificates:   []Certificate{{Certificate: kong.Certificate{ID: kong.String("1"), Key: redactedString}}},
 				CACertificates: []kong.CACertificate{{ID: kong.String("1")}},
-				Plugins: []Plugin{{
-					SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{WholeConfigIsSensitive: true},
-					Plugin:              kong.Plugin{ID: kong.String("1"), Config: kong.Configuration{"secret": *redactedString}},
-				}},
+				Plugins:        []Plugin{{Plugin: kong.Plugin{ID: kong.String("1"), Config: map[string]interface{}{"key": "secret"}}}}, // We don't redact plugins' config.
 				Consumers: []Consumer{{
 					KeyAuths: []*KeyAuth{{kong.KeyAuth{ID: kong.String("1"), Key: kong.String("{vault://52fdfc07-2182-454f-963f-5f0f9a621d72}")}}},
 				}},
@@ -100,6 +115,25 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 						},
 					},
 				},
+				CustomEntities: map[string]*KongCustomEntityCollection{
+					"test_entities": {
+						Schema: EntitySchema{
+							Fields: map[string]EntityField{
+								"name": {
+									Type:     EntityFieldTypeString,
+									Required: true,
+								},
+							},
+						},
+						Entities: []CustomEntity{
+							{
+								Object: map[string]interface{}{
+									"name": "foo",
+								},
+							},
+						},
+					},
+				},
 			},
 		},
 	} {
@@ -450,9 +484,13 @@ func TestGetPluginRelations(t *testing.T) {
 				"ns1:foo":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Service: []string{"foo-service"}},
 				"ns1:bar":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Service: []string{"foo-service"}},
 				"ns1:foobar": {Consumer: []string{"bar-consumer"}},
-				"ns2:foo":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Route: []string{"foo-route"}},
-				"ns2:bar":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group", "bar-consumer-group"}, Route: []string{"foo-route", "bar-route"}},
-				"ns2:baz":    {Route: []string{"bar-route"}, ConsumerGroup: []string{"bar-consumer-group"}},
+				"ns2:foo": {
+					Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Route: []string{"foo-route"},
+				},
+				"ns2:bar": {
+					Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group", "bar-consumer-group"}, Route: []string{"foo-route", "bar-route"},
+				},
+				"ns2:baz": {Route: []string{"bar-route"}, ConsumerGroup: []string{"bar-consumer-group"}},
 			},
 		},
 	}
@@ -1475,3 +1513,301 @@ func TestFillOverrides_ServiceFailures(t *testing.T) {
 		})
 	}
 }
+
+type fakeSchemaGetter struct {
+	schemas map[string]kong.Schema
+}
+
+var _ SchemaGetter = &fakeSchemaGetter{}
+
+func (s *fakeSchemaGetter) Get(_ context.Context, entityType string) (kong.Schema, error) {
+	schema, ok := s.schemas[entityType]
+	if !ok {
+		return nil, fmt.Errorf("schema not found")
+	}
+	return schema, nil
+}
+
+func TestKongState_FillCustomEntities(t *testing.T) {
+	customEntityTypeMeta := metav1.TypeMeta{
+		APIVersion: kongv1alpha1.GroupVersion.Group + "/" + kongv1alpha1.GroupVersion.Version,
+		Kind:       "KongCustomEntity",
+	}
+	kongService1 := kong.Service{
+		Name: kong.String("service1"),
+	}
+	getKongServiceID := func(s *kong.Service) string {
+		err := s.FillID("")
+		require.NoError(t, err)
+		return *s.ID
+	}
+
+	testCases := []struct {
+		name                        string
+		initialState                *KongState
+		customEntities              []*kongv1alpha1.KongCustomEntity
+		plugins                     []*kongv1.KongPlugin
+		schemas                     map[string]kong.Schema
+		expectedCustomEntities      map[string][]custom.Object
+		expectedTranslationFailures map[k8stypes.NamespacedName]string
+	}{
+		{
+			name:         "single custom entity",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session1"}`),
+						},
+					},
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"sessions": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"name": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"sessions": {
+					{
+						"name": "session1",
+					},
+				},
+			},
+		},
+		{
+			name:         "custom entity with unknown type",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session1"}`),
+						},
+					},
+				},
+			},
+			expectedTranslationFailures: map[k8stypes.NamespacedName]string{
+				{
+					Namespace: "default",
+					Name:      "session-foo",
+				}: "failed to fetch entity schema for entity type sessions: schema not found",
+			},
+		},
+		{
+			name:         "multiple custom entities with same type",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-foo"}`),
+						},
+					},
+				},
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-bar",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-bar"}`),
+						},
+					},
+				},
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default-1",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-foo-1"}`),
+						},
+					},
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"sessions": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"name": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				// Should be sorted by original KCE namespace/name.
+				"sessions": {
+					{
+						// from default/bar
+						"name": "session-bar",
+					},
+					{
+						// from default/foo
+						"name": "session-foo",
+					},
+					{
+						// from default-1/foo
+						"name": "session-foo-1",
+					},
+				},
+			},
+		},
+		{
+			name: "custom entities with reference to other entities (services)",
+			initialState: &KongState{
+				Services: []Service{
+					{
+						Service: kongService1,
+						K8sServices: map[string]*corev1.Service{
+							"default/service1": {
+								ObjectMeta: metav1.ObjectMeta{
+									Namespace: "default",
+									Name:      "service1",
+									Annotations: map[string]string{
+										annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
+									},
+								},
+							},
+						},
+					}, // Service: service1
+				}, // Services
+			},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "degraphql_routes",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"uri":"/api/me"}`),
+						},
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Group: kong.String(kongv1.GroupVersion.Group),
+							Kind:  kong.String("KongPlugin"),
+							Name:  "degraphql-1",
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					PluginName: "degraphql",
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"degraphql_routes": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"uri": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+						map[string]interface{}{
+							"service": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "services",
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"degraphql_routes": {
+					{
+						"uri": "/api/me",
+						"service": map[string]interface{}{
+							// ID generated from Kong service "service1" in workspace "".
+							"id": getKongServiceID(&kongService1),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			s, err := store.NewFakeStore(store.FakeObjects{
+				KongCustomEntities: tc.customEntities,
+				KongPlugins:        tc.plugins,
+			})
+			require.NoError(t, err)
+			failuresCollector := failures.NewResourceFailuresCollector(logr.Discard())
+
+			ks := tc.initialState
+			ks.FillCustomEntities(
+				logr.Discard(), s,
+				failuresCollector,
+				&fakeSchemaGetter{schemas: tc.schemas}, "",
+			)
+			for entityType, expectedObjectList := range tc.expectedCustomEntities {
+				require.NotNil(t, ks.CustomEntities[entityType])
+				objectList := lo.Map(ks.CustomEntities[entityType].Entities, func(e CustomEntity, _ int) custom.Object {
+					return e.Object
+				})
+				require.Equal(t, expectedObjectList, objectList)
+			}
+
+			translationFailures := failuresCollector.PopResourceFailures()
+			for nsName, message := range tc.expectedTranslationFailures {
+				hasError := lo.ContainsBy(translationFailures, func(f failures.ResourceFailure) bool {
+					fmt.Println(f.Message())
+					return f.Message() == message && lo.ContainsBy(f.CausingObjects(), func(o client.Object) bool {
+						return o.GetNamespace() == nsName.Namespace && o.GetName() == nsName.Name
+					})
+				})
+				require.Truef(t, hasError, "translation error for KongCustomEntity %s not found", nsName)
+			}
+		})
+	}
+}
diff --git a/internal/dataplane/kongstate/plugin_test.go b/internal/dataplane/kongstate/plugin_test.go
index 5faf399c2..fed7b3f1c 100644
--- a/internal/dataplane/kongstate/plugin_test.go
+++ b/internal/dataplane/kongstate/plugin_test.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/kong/go-kong/kong"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -15,6 +14,7 @@ import (
 )
 
 func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
+	assert := assert.New(t)
 	store, _ := store.NewFakeStore(store.FakeObjects{
 		Secrets: []*corev1.Secret{
 			{
@@ -32,284 +32,277 @@ func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
 			},
 		},
 	})
-
+	type args struct {
+		plugin kongv1.KongClusterPlugin
+	}
 	tests := []struct {
 		name    string
-		plugin  kongv1.KongClusterPlugin
-		want    Plugin
+		args    args
+		want    kong.Plugin
 		wantErr bool
 	}{
 		{
 			name: "basic configuration",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:    []kongv1.KongProtocol{"http"},
-				PluginName:   "correlation-id",
-				InstanceName: "example",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:    []kongv1.KongProtocol{"http"},
+					PluginName:   "correlation-id",
+					InstanceName: "example",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-					},
-					Protocols:    kong.StringSlice("http"),
-					InstanceName: kong.String("example"),
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{JSONPaths: []string{}},
+				Protocols:    kong.StringSlice("http"),
+				InstanceName: kong.String("example"),
 			},
 			wantErr: false,
 		},
 		{
 			name: "secret configuration",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				ConfigFrom: &kongv1.NamespacedConfigSource{
-					SecretValue: kongv1.NamespacedSecretValueFromSource{
-						Key:       "correlation-id-config",
-						Secret:    "conf-secret",
-						Namespace: "default",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "conf-secret",
+							Namespace: "default",
+						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					WholeConfigIsSensitive: true,
-					JSONPaths:              []string{},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 			wantErr: false,
 		},
 		{
 			name: "missing secret configuration",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				ConfigFrom: &kongv1.NamespacedConfigSource{
-					SecretValue: kongv1.NamespacedSecretValueFromSource{
-						Key:       "correlation-id-config",
-						Secret:    "missing",
-						Namespace: "default",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "missing",
+							Namespace: "default",
+						},
 					},
 				},
 			},
-			want:    Plugin{},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "non-JSON configuration",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{{}`),
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{{}`),
+					},
 				},
 			},
-			want:    Plugin{},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "both Config and ConfigFrom set",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigFrom: &kongv1.NamespacedConfigSource{
-					SecretValue: kongv1.NamespacedSecretValueFromSource{
-						Key:       "correlation-id-config",
-						Secret:    "conf-secret",
-						Namespace: "default",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigFrom: &kongv1.NamespacedConfigSource{
+						SecretValue: kongv1.NamespacedSecretValueFromSource{
+							Key:       "correlation-id-config",
+							Secret:    "conf-secret",
+							Namespace: "default",
+						},
 					},
 				},
 			},
-			want:    Plugin{},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "Config and ConfigPatches set",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Key:       "correlation-id-generator",
-								Secret:    "conf-secret",
-								Namespace: "default",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Key:       "correlation-id-generator",
+									Secret:    "conf-secret",
+									Namespace: "default",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-						"generator":   "uuid",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/generator"},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "configPatch on subpath of non-exist path",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "response-transformer",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
-				},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/add/headers",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "response-transformer-add-headers",
-								Secret:    "conf-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "response-transformer",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/add/headers",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "response-transformer-add-headers",
+									Secret:    "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("response-transformer"),
-					Config: kong.Configuration{
-						"replace": map[string]interface{}{
-							"headers": []interface{}{
-								"foo:bar",
-							},
+			want: kong.Plugin{
+				Name: kong.String("response-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]interface{}{
+						"headers": []interface{}{
+							"foo:bar",
 						},
-						"add": map[string]interface{}{
-							"headers": []interface{}{
-								"h1:v1",
-								"h2:v2",
-							},
+					},
+					"add": map[string]interface{}{
+						"headers": []interface{}{
+							"h1:v1",
+							"h2:v2",
 						},
 					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/add/headers"},
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "empty config and configPatch for particular paths",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config:     apiextensionsv1.JSON{},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/header_name",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-headername",
-								Secret:    "conf-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/header_name",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-headername",
+									Secret:    "conf-secret",
+								},
 							},
 						},
-					},
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-generator",
-								Secret:    "conf-secret",
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-generator",
+									Secret:    "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-						"generator":   "uuid",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/header_name", "/generator"},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "empty config and configPatch for whole object",
-			plugin: kongv1.KongClusterPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config:     apiextensionsv1.JSON{},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-config",
-								Secret:    "conf-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-config",
+									Secret:    "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{""},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "missing secret in configPatches",
-			plugin: kongv1.KongClusterPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "test",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-generator",
-								Secret:    "missing-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-generator",
+									Secret:    "missing-secret",
+								},
 							},
 						},
 					},
@@ -319,23 +312,25 @@ func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
 		},
 		{
 			name: "missing key of secret in cofigPatches",
-			plugin: kongv1.KongClusterPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "test",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-missing",
-								Secret:    "conf-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-missing",
+									Secret:    "conf-secret",
+								},
 							},
 						},
 					},
@@ -345,23 +340,25 @@ func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
 		},
 		{
 			name: "invalid value in configPatches",
-			plugin: kongv1.KongClusterPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "test",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.NamespacedConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.NamespacedConfigSource{
-							SecretValue: kongv1.NamespacedSecretValueFromSource{
-								Namespace: "default",
-								Key:       "correlation-id-invalid",
-								Secret:    "conf-secret",
+			args: args{
+				plugin: kongv1.KongClusterPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.NamespacedConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.NamespacedConfigSource{
+								SecretValue: kongv1.NamespacedSecretValueFromSource{
+									Namespace: "default",
+									Key:       "correlation-id-invalid",
+									Secret:    "conf-secret",
+								},
 							},
 						},
 					},
@@ -372,18 +369,19 @@ func TestKongPluginFromK8SClusterPlugin(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := kongPluginFromK8SClusterPlugin(store, tt.plugin)
-			if tt.wantErr {
-				require.Error(t, err)
+			got, err := kongPluginFromK8SClusterPlugin(store, tt.args.plugin)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("kongPluginFromK8SClusterPlugin error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
-			tt.want.K8sParent = tt.plugin.DeepCopy()
-			assert.Equal(t, tt.want, got)
+			assert.Equal(tt.want, got.Plugin)
+			assert.NotEmpty(t, got.K8sParent)
 		})
 	}
 }
 
 func TestKongPluginFromK8SPlugin(t *testing.T) {
+	assert := assert.New(t)
 	store, _ := store.NewFakeStore(store.FakeObjects{
 		Secrets: []*corev1.Secret{
 			{
@@ -401,296 +399,293 @@ func TestKongPluginFromK8SPlugin(t *testing.T) {
 			},
 		},
 	})
+	type args struct {
+		plugin kongv1.KongPlugin
+	}
 	tests := []struct {
 		name    string
-		plugin  kongv1.KongPlugin
-		want    Plugin
+		args    args
+		want    kong.Plugin
 		wantErr bool
 	}{
 		{
 			name: "basic configuration",
-			plugin: kongv1.KongPlugin{
-				Protocols:    []kongv1.KongProtocol{"http"},
-				PluginName:   "correlation-id",
-				InstanceName: "example",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:    []kongv1.KongProtocol{"http"},
+					PluginName:   "correlation-id",
+					InstanceName: "example",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-					},
-					Protocols:    kong.StringSlice("http"),
-					InstanceName: kong.String("example"),
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{JSONPaths: []string{}},
+				Protocols:    kong.StringSlice("http"),
+				InstanceName: kong.String("example"),
 			},
 			wantErr: false,
 		},
 		{
 			name: "secret configuration",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "foo",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				ConfigFrom: &kongv1.ConfigSource{
-					SecretValue: kongv1.SecretValueFromSource{
-						Key:    "correlation-id-config",
-						Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foo",
+						Namespace: "default",
 					},
-				},
-			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "conf-secret",
+						},
 					},
-					Protocols: kong.StringSlice("http"),
 				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					WholeConfigIsSensitive: true,
-					JSONPaths:              []string{},
+			},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 			wantErr: false,
 		},
 		{
 			name: "missing secret configuration",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "foo",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				ConfigFrom: &kongv1.ConfigSource{
-					SecretValue: kongv1.SecretValueFromSource{
-						Key:    "correlation-id-config",
-						Secret: "missing",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foo",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "missing",
+						},
 					},
 				},
 			},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "non-JSON configuration",
-			plugin: kongv1.KongPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{{}`),
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{{}`),
+					},
 				},
 			},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "both Config and ConfigFrom set",
-			plugin: kongv1.KongPlugin{
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigFrom: &kongv1.ConfigSource{
-					SecretValue: kongv1.SecretValueFromSource{
-						Key:    "correlation-id-config",
-						Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigFrom: &kongv1.ConfigSource{
+						SecretValue: kongv1.SecretValueFromSource{
+							Key:    "correlation-id-config",
+							Secret: "conf-secret",
+						},
 					},
 				},
 			},
+			want:    kong.Plugin{},
 			wantErr: true,
 		},
 		{
 			name: "config and configPatches set",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-generator",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-						"generator":   "uuid",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/generator"},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "configPatch on subpath of non-exist path",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "response-transformer",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
-				},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/add/headers",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "response-transformer-add-headers",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "response-transformer",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"replace":{"headers":["foo:bar"]}}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/add/headers",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "response-transformer-add-headers",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("response-transformer"),
-					Config: kong.Configuration{
-						"replace": map[string]interface{}{
-							"headers": []interface{}{
-								"foo:bar",
-							},
+			want: kong.Plugin{
+				Name: kong.String("response-transformer"),
+				Config: kong.Configuration{
+					"replace": map[string]interface{}{
+						"headers": []interface{}{
+							"foo:bar",
 						},
-						"add": map[string]interface{}{
-							"headers": []interface{}{
-								"h1:v1",
-								"h2:v2",
-							},
+					},
+					"add": map[string]interface{}{
+						"headers": []interface{}{
+							"h1:v1",
+							"h2:v2",
 						},
 					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/add/headers"},
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "empty config and configPatch for particular paths",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config:     apiextensionsv1.JSON{},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/header_name",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-headername",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/header_name",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-headername",
+									Secret: "conf-secret",
+								},
 							},
 						},
-					},
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-generator",
-								Secret: "conf-secret",
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-						"generator":   "uuid",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{"/header_name", "/generator"},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
+					"generator":   "uuid",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "empty config and configPatch for whole object",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config:     apiextensionsv1.JSON{},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-config",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config:     apiextensionsv1.JSON{},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-config",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
 				},
 			},
-			want: Plugin{
-				Plugin: kong.Plugin{
-					Name: kong.String("correlation-id"),
-					Config: kong.Configuration{
-						"header_name": "foo",
-					},
-					Protocols: kong.StringSlice("http"),
-				},
-				SensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-					JSONPaths: []string{""},
+			want: kong.Plugin{
+				Name: kong.String("correlation-id"),
+				Config: kong.Configuration{
+					"header_name": "foo",
 				},
+				Protocols: kong.StringSlice("http"),
 			},
 		},
 		{
 			name: "missing secret in configPatches",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-generator",
-								Secret: "missing-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-generator",
+									Secret: "missing-secret",
+								},
 							},
 						},
 					},
@@ -700,23 +695,25 @@ func TestKongPluginFromK8SPlugin(t *testing.T) {
 		},
 		{
 			name: "missing key of secret in configPatches",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-missing",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-missing",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
@@ -726,23 +723,25 @@ func TestKongPluginFromK8SPlugin(t *testing.T) {
 		},
 		{
 			name: "invalid value in configPatches",
-			plugin: kongv1.KongPlugin{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "default",
-				},
-				Protocols:  []kongv1.KongProtocol{"http"},
-				PluginName: "correlation-id",
-				Config: apiextensionsv1.JSON{
-					Raw: []byte(`{"header_name": "foo"}`),
-				},
-				ConfigPatches: []kongv1.ConfigPatch{
-					{
-						Path: "/generator",
-						ValueFrom: kongv1.ConfigSource{
-							SecretValue: kongv1.SecretValueFromSource{
-								Key:    "correlation-id-invalid",
-								Secret: "conf-secret",
+			args: args{
+				plugin: kongv1.KongPlugin{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test",
+						Namespace: "default",
+					},
+					Protocols:  []kongv1.KongProtocol{"http"},
+					PluginName: "correlation-id",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{"header_name": "foo"}`),
+					},
+					ConfigPatches: []kongv1.ConfigPatch{
+						{
+							Path: "/generator",
+							ValueFrom: kongv1.ConfigSource{
+								SecretValue: kongv1.SecretValueFromSource{
+									Key:    "correlation-id-invalid",
+									Secret: "conf-secret",
+								},
 							},
 						},
 					},
@@ -753,108 +752,15 @@ func TestKongPluginFromK8SPlugin(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := kongPluginFromK8SPlugin(store, tt.plugin)
-			if tt.wantErr {
-				require.Error(t, err)
+			got, err := kongPluginFromK8SPlugin(store, tt.args.plugin)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("kongPluginFromK8SPlugin error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			// don't care about tags in this test
 			got.Tags = nil
-			tt.want.K8sParent = tt.plugin.DeepCopy()
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
-
-func TestPlugin_SanitizedCopy(t *testing.T) {
-	testCases := []struct {
-		name                    string
-		config                  kong.Configuration
-		sensitiveFieldsMeta     PluginSensitiveFieldsMetadata
-		expectedSanitizedConfig kong.Configuration
-	}{
-		{
-			name: "sensitive fields are redacted with JSONPaths",
-			config: kong.Configuration{
-				"secret": "secret-value",
-				"object": map[string]interface{}{
-					"secretObjectField": "secret-object-field-value",
-				},
-			},
-			sensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-				JSONPaths: []string{
-					"/secret",
-					"/object/secretObjectField",
-				},
-			},
-			expectedSanitizedConfig: kong.Configuration{
-				"secret": "{vault://redacted-value}",
-				"object": map[string]interface{}{
-					"secretObjectField": "{vault://redacted-value}",
-				},
-			},
-		},
-		{
-			name: "invalid JSONPath doesn't panic and redacts whole config as fallback",
-			config: kong.Configuration{
-				"secret": "secret-value",
-				"object": map[string]interface{}{
-					"secretObjectField": "secret-object-field-value",
-				},
-			},
-			sensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-				JSONPaths: []string{
-					"/not-existing-path",
-				},
-			},
-			expectedSanitizedConfig: kong.Configuration{
-				"secret": "{vault://redacted-value}",
-				"object": "{vault://redacted-value}",
-			},
-		},
-		{
-			name: "whole config to sanitize",
-			config: kong.Configuration{
-				"secret": "secret-value",
-				"object": map[string]interface{}{
-					"secretObjectField": "secret-object-field-value",
-				},
-			},
-			sensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-				WholeConfigIsSensitive: true,
-			},
-			expectedSanitizedConfig: kong.Configuration{
-				"secret": "{vault://redacted-value}",
-				"object": "{vault://redacted-value}",
-			},
-		},
-		{
-			name: "single empty JSON path - whole config is redacted",
-			config: kong.Configuration{
-				"secret": "secret-value",
-				"object": map[string]interface{}{
-					"secretObjectField": "secret-object-field-value",
-				},
-			},
-			sensitiveFieldsMeta: PluginSensitiveFieldsMetadata{
-				JSONPaths: []string{""},
-			},
-			expectedSanitizedConfig: kong.Configuration{
-				"secret": "{vault://redacted-value}",
-				"object": "{vault://redacted-value}",
-			},
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			p := Plugin{
-				Plugin: kong.Plugin{
-					Config: tc.config,
-				},
-				SensitiveFieldsMeta: tc.sensitiveFieldsMeta,
-			}
-			sanitized := p.SanitizedCopy()
-			assert.Equal(t, tc.expectedSanitizedConfig, sanitized.Config)
+			assert.Equal(tt.want, got.Plugin)
+			assert.NotEmpty(t, got.K8sParent)
 		})
 	}
 }
diff --git a/internal/dataplane/translator/golden_test.go b/internal/dataplane/translator/golden_test.go
index b787f6e28..79977fb16 100644
--- a/internal/dataplane/translator/golden_test.go
+++ b/internal/dataplane/translator/golden_test.go
@@ -12,6 +12,7 @@ import (
 	"testing"
 
 	"github.com/go-logr/zapr"
+	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
@@ -49,6 +50,12 @@ const (
 	settingsFileSuffix = "_settings.yaml"
 )
 
+type fakeSchemaServiceProvier struct{}
+
+func (p fakeSchemaServiceProvier) GetSchemaService() kong.AbstractSchemaService {
+	return translator.UnavailableSchemaService{}
+}
+
 // TestTranslator_GoldenTests runs the golden tests for the translator.
 //
 // Command to update the golden files:
@@ -223,7 +230,7 @@ func runTranslatorGoldenTest(t *testing.T, tc translatorGoldenTestCase) {
 
 	// Create the translator.
 	s := store.New(cacheStores, "kong", logger)
-	p, err := translator.NewTranslator(logger, s, "", tc.featureFlags)
+	p, err := translator.NewTranslator(logger, s, "", tc.featureFlags, fakeSchemaServiceProvier{})
 	require.NoError(t, err, "failed creating translator")
 
 	// MustBuild the Kong configuration.
diff --git a/internal/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go
index 03f6171d3..8fca543c9 100644
--- a/internal/dataplane/translator/translator_test.go
+++ b/internal/dataplane/translator/translator_test.go
@@ -5019,13 +5019,20 @@ func TestTranslator_ConfiguredKubernetesObjects(t *testing.T) {
 	}
 }
 
+type fakeSchemaServiceProvier struct{}
+
+func (p fakeSchemaServiceProvier) GetSchemaService() kong.AbstractSchemaService {
+	return UnavailableSchemaService{}
+}
+
 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,
-	})
+	}, fakeSchemaServiceProvier{},
+	)
 	require.NoError(t, err)
 	return p
 }
diff --git a/internal/manager/featuregates/feature_gates_test.go b/internal/manager/featuregates/feature_gates_test.go
index a4fcdf9e1..416368f46 100644
--- a/internal/manager/featuregates/feature_gates_test.go
+++ b/internal/manager/featuregates/feature_gates_test.go
@@ -1,10 +1,12 @@
 package featuregates
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/go-logr/zapr"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
 )
 
@@ -23,11 +25,17 @@ func TestFeatureGates(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, fgs[GatewayAlphaFeature])
 
+	t.Log("Verifying feature gates setup will return error when settings has conflicts")
+	featureGates = map[string]bool{KongCustomEntity: true, FillIDsFeature: false}
+	_, err = New(setupLog, featureGates)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), fmt.Sprintf("%s is required if %s is enabled", FillIDsFeature, KongCustomEntity))
+
 	t.Log("Configuring several invalid feature gates options")
 	featureGates = map[string]bool{"invalidGateway": true}
 
 	t.Log("Verifying feature gates setup results when invalid feature gates options are present")
 	_, err = New(setupLog, featureGates)
-	assert.Error(t, err)
+	require.Error(t, err)
 	assert.Contains(t, err.Error(), "invalidGateway is not a valid feature")
 }
diff --git a/internal/store/zz_generated_cache_stores_test.go b/internal/store/zz_generated_cache_stores_test.go
index eae0631ae..58a66b1bb 100644
--- a/internal/store/zz_generated_cache_stores_test.go
+++ b/internal/store/zz_generated_cache_stores_test.go
@@ -139,6 +139,11 @@ func TestCacheStores(t *testing.T) {
 			name:          "KongVault",
 			objectToStore: &kongv1alpha1.KongVault{},
 		},
+
+		{
+			name:          "KongCustomEntity",
+			objectToStore: &kongv1alpha1.KongCustomEntity{},
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
index fe7fef574..24b46aba6 100644
--- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
index a9ec33108..f40da8654 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
index ffabcb919..f0aa713bf 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml
index f3c479de2..c16755515 100644
--- a/test/e2e/manifests/all-in-one-dbless.yaml
+++ b/test/e2e/manifests/all-in-one-dbless.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
index a418f204d..ca7ab78a5 100644
--- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
index 2069d66e1..8fdc8e9be 100644
--- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml
index ce07f1a49..5a8a3e498 100644
--- a/test/e2e/manifests/all-in-one-postgres.yaml
+++ b/test/e2e/manifests/all-in-one-postgres.yaml
@@ -710,6 +710,203 @@ spec:
 ---
 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: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.15.0
@@ -3094,6 +3291,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongcustomentities/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/envtest/admission_webhook_envtest_test.go b/test/envtest/admission_webhook_envtest_test.go
index d13deb277..76a095964 100644
--- a/test/envtest/admission_webhook_envtest_test.go
+++ b/test/envtest/admission_webhook_envtest_test.go
@@ -1296,7 +1296,8 @@ func TestAdmissionWebhook_KongCustomEntities(t *testing.T) {
 					},
 				},
 			},
-			valid: true,
+			requireEnterpriseLicense: true,
+			valid:                    true,
 		},
 		{
 			name: "invalid degraphql_route entity",
@@ -1312,7 +1313,8 @@ func TestAdmissionWebhook_KongCustomEntities(t *testing.T) {
 					},
 				},
 			},
-			valid: false,
+			requireEnterpriseLicense: true,
+			valid:                    false,
 		},
 		{
 			name: "KongCustomEntity not controlled by the current controller",
diff --git a/test/envtest/telemetry_test.go b/test/envtest/telemetry_test.go
index a3aa8d31c..05754f80a 100644
--- a/test/envtest/telemetry_test.go
+++ b/test/envtest/telemetry_test.go
@@ -360,6 +360,7 @@ func verifyTelemetryReport(t *testing.T, k8sVersion *version.Info, report string
 			"feature-fillids=true;"+
 			"feature-gateway-service-discovery=false;"+
 			"feature-gatewayalpha=false;"+
+			"feature-kongcustomentity=false;"+
 			"feature-kongservicefacade=false;"+
 			"feature-konnect-sync=false;"+
 			"feature-rewriteuris=false;"+