Repository: Kong/kubernetes-ingress-controller
Pull request #5410 (2024-01-08 14:07:51)

chore(deps): bump github.com/docker/go-connections from 0.4.0 to 0.5.0
Bumps [github.com/docker/go-connections](https://github.com/docker/go-connections) from 0.4.0 to 0.5.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/docker/go-connections/commit/fa09c952e3eadbffaf8afc5b8a1667158ba38ace"><code>fa09c95</code></a> Merge pull request <a href="https://redirect.github.com/docker/go-connections/issues/108">#108</a> from thaJeztah/carry_6</li>
<li><a href="https://github.com/docker/go-connections/commit/7a67a58690ec00e92abd9fedb3b1b99c95bbbd59"><code>7a67a58</code></a> Swap CloseRead and CloseWrite</li>
<li><a href="https://github.com/docker/go-connections/commit/481d3d26b3705da4932f3fb11d3b47a5b5abcb46"><code>481d3d2</code></a> Merge pull request <a href="https://redirect.github.com/docker/go-connections/issues/107">#107</a> from thaJeztah/drop_legacy_go</li>
<li><a href="https://github.com/docker/go-connections/commit/9548f9f7bdd4e95eb9d3a20c430517f69f5c62cf"><code>9548f9f</code></a> tlsconfig: remove deprecated io/ioutil</li>
<li><a href="https://github.com/docker/go-connections/commit/c564c210e1404800194d27aad617dde3c4f4f831"><code>c564c21</code></a> drop support for go1.17 and older</li>
<li><a href="https://github.com/docker/go-connections/commit/7cbebcf931b3107a81a923ce6e1874380c65c436"><code>7cbebcf</code></a> gha: update actions</li>
<li><a href="https://github.com/docker/go-connections/commit/2cf423f0ad95e0e87577d9740c647015c00ed8fd"><code>2cf423f</code></a> tlsconfig: move allTLSVersions var</li>
<li><a href="https://github.com/docker/go-connections/commit/dca283b66503865dfa3869d7976a925105b82211"><code>dca283b</code></a> tlsconfig: drop support for go1.12 and older</li>
<li><a href="https://github.com/docker/go-connections/commit/21876c5afda7f3fe2b7505a72a04eb4a021b64f1"><code>21876c5</code></a> tlsconfig: drop support for go1.6 and older</li>
<li><a href="https://github.com/docker/go-connections/commit/4d174dba226cc3a556fa2f158b209f4d54c64281"><code>4d174db</code></a> tlsconfig: drop support for go1.4 and older</li>
<li>Additional commits viewable in <a href="https://github.com/docker/go-connections/compare/v0.4.0...v0.5.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/docker/go-connections&package-manager=go_modules&previous-version=0.4.0&new-version=0.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

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


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


```diff
@@           Coverage Diff           @@
##            main   #5410     +/-   ##
=======================================
- Coverage   69.5%   68.6%   -0.9%     
=======================================
  Files        174     174             
  Lines      21934   21934             
=======================================
- Hits       15251   15065    -186     
- Misses      5785    6013    +228     
+ Partials     898     856     -42     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/5410?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 2860210a5..570dd36e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -129,9 +129,13 @@ Adding a new version? You'll need three changes:
 - Added functionality to the `KongUpstreamPolicy` controller to properly set and
   enforce `KongUpstreamPolicy` status.
   [#5185](https://github.com/Kong/kubernetes-ingress-controller/pull/5185)
-- Add CRD `KongVault` to reperesent a custom Kong vault for storing senstive
-  data used in plugin configurations.
+- New CRD `KongVault` to reperesent a custom Kong vault for storing senstive
+  data used in plugin configurations. Now users can create a `KongVault` to
+  create a custom Kong vault and reference the values in configurations of
+  plugins. Reference of using Kong vaults: [Kong vault]
   [#5354](https://github.com/Kong/kubernetes-ingress-controller/pull/5354)
+  [#5384](https://github.com/Kong/kubernetes-ingress-controller/pull/5384)
+
 
 ### Fixed
 
@@ -165,6 +169,8 @@ Adding a new version? You'll need three changes:
   - `KongPlugin` and `KongClusterPlugin` now enforce `plugin` to be immutable.
     [#5142](https://github.com/Kong/kubernetes-ingress-controller/pull/5142)
 
+[Kong vault]: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/
+
 ## [3.0.1]
 
 > Release date: 2023-11-22
diff --git a/config/components/manager_dev_webhook/validating_webhook_configuration.yaml b/config/components/manager_dev_webhook/validating_webhook_configuration.yaml
index 5124fce76..d7ffe466f 100644
--- a/config/components/manager_dev_webhook/validating_webhook_configuration.yaml
+++ b/config/components/manager_dev_webhook/validating_webhook_configuration.yaml
@@ -23,9 +23,11 @@ webhooks:
     - UPDATE
     resources:
     - kongconsumers
+    - kongconsumergroups
     - kongplugins
     - kongclusterplugins
     - kongingresses
+    - kongvaults
     scope: '*'
   - apiGroups:
     - ""
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 1dafe9063..12ac6ec30 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -13,6 +13,7 @@ resources:
 - bases/configuration.konghq.com_kongplugins.yaml
 - bases/configuration.konghq.com_ingressclassparameterses.yaml
 - bases/configuration.konghq.com_kongupstreampolicies.yaml
+- bases/configuration.konghq.com_kongvaults.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 dc6f6f02b..43e48dd44 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -153,6 +153,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md
index 79036e756..86a0abec4 100644
--- a/docs/cli-arguments.md
+++ b/docs/cli-arguments.md
@@ -27,6 +27,7 @@
 | `--enable-controller-ingress-networkingv1` | `bool` | Enable the networking.k8s.io/v1 Ingress controller. | `true` |
 | `--enable-controller-kong-service-facade` | `bool` | Enable the KongServiceFacade controller. | `true` |
 | `--enable-controller-kong-upstream-policy` | `bool` | Enable the KongUpstreamPolicy controller. | `true` |
+| `--enable-controller-kong-vault` | `bool` | Enable the KongVault controller. | `true` |
 | `--enable-controller-kongclusterplugin` | `bool` | Enable the KongClusterPlugin controller. | `true` |
 | `--enable-controller-kongconsumer` | `bool` | Enable the KongConsumer controller. | `true` |
 | `--enable-controller-kongingress` | `bool` | Enable the KongIngress controller. | `true` |
diff --git a/go.mod b/go.mod
index cf257a5eb..c459d8d49 100644
--- a/go.mod
+++ b/go.mod
@@ -121,7 +121,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/docker/distribution v2.8.2+incompatible // indirect
 	github.com/docker/docker v24.0.7+incompatible
-	github.com/docker/go-connections v0.4.0
+	github.com/docker/go-connections v0.5.0
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/evanphx/json-patch v5.7.0+incompatible // indirect
diff --git a/go.sum b/go.sum
index 45578f241..a023c6ecd 100644
--- a/go.sum
+++ b/go.sum
@@ -83,8 +83,8 @@ github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m3
 github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
 github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
diff --git a/hack/deploy-admission-controller.sh b/hack/deploy-admission-controller.sh
index 5d7cf3aab..9731c9e31 100755
--- a/hack/deploy-admission-controller.sh
+++ b/hack/deploy-admission-controller.sh
@@ -59,6 +59,7 @@ webhooks:
     - kongplugins
     - kongclusterplugins
     - kongingresses
+    - kongvaults
   - apiGroups:
     - ''
     apiVersions:
diff --git a/hack/generators/controllers/networking/main.go b/hack/generators/controllers/networking/main.go
index a0b1cc75b..3adcc5ef1 100644
--- a/hack/generators/controllers/networking/main.go
+++ b/hack/generators/controllers/networking/main.go
@@ -247,6 +247,23 @@ var inputControllersNeeded = &typesNeeded{
 		AcceptsIngressClassNameAnnotation: true,
 		RBACVerbs:                         []string{"get", "list", "watch"},
 	},
+	typeNeeded{
+		Group:                            "configuration.konghq.com",
+		Version:                          "v1alpha1",
+		Kind:                             "KongVault",
+		PackageImportAlias:               "kongv1alpha1",
+		PackageAlias:                     "KongV1Alpha1",
+		Package:                          kongv1alpha1,
+		Plural:                           "kongvaults",
+		CacheType:                        "KongVault",
+		NeedsStatusPermissions:           true,
+		ConfigStatusNotificationsEnabled: true,
+		ProgrammedCondition: ProgrammedConditionConfiguration{
+			UpdatesEnabled: true,
+		},
+		AcceptsIngressClassNameAnnotation: true,
+		RBACVerbs:                         []string{"get", "list", "watch"},
+	},
 }
 
 var inputRBACPermissionsNeeded = &rbacsNeeded{
diff --git a/internal/admission/adminapi_provider.go b/internal/admission/adminapi_provider.go
index 051da681b..7868c3d67 100644
--- a/internal/admission/adminapi_provider.go
+++ b/internal/admission/adminapi_provider.go
@@ -61,6 +61,14 @@ func (p DefaultAdminAPIServicesProvider) GetRoutesService() (kong.AbstractRouteS
 	return c.Routes, true
 }
 
+func (p DefaultAdminAPIServicesProvider) GetVaultsService() (kong.AbstractVaultService, bool) {
+	c, ok := p.designatedAdminAPIClient()
+	if !ok {
+		return nil, ok
+	}
+	return c.Vaults, true
+}
+
 func (p DefaultAdminAPIServicesProvider) designatedAdminAPIClient() (*kong.Client, bool) {
 	gwClients := p.gatewayClientsProvider.GatewayClients()
 	if len(gwClients) == 0 {
diff --git a/internal/admission/errors.go b/internal/admission/errors.go
index bd20444e8..33917f8be 100644
--- a/internal/admission/errors.go
+++ b/internal/admission/errors.go
@@ -14,6 +14,9 @@ const (
 	ErrTextPluginConfigValidationFailed       = "unable to validate plugin schema"
 	ErrTextPluginConfigViolatesSchema         = "plugin failed schema validation: %s"
 	ErrTextPluginSecretConfigUnretrievable    = "could not load secret plugin configuration"
+	ErrTextVaultConfigUnmarshalFailed         = "failed to unmarshal vault configuration: %v"
+	ErrTextVaultUnableToValidate              = "unable to validate vault on Kong gateway"
+	ErrTextVaultConfigValidationResultInvalid = "vault configuration in invalid: %s"
 )
 
 const (
diff --git a/internal/admission/handler.go b/internal/admission/handler.go
index bb1b6b35a..01abf25d4 100644
--- a/internal/admission/handler.go
+++ b/internal/admission/handler.go
@@ -17,6 +17,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 )
 
@@ -96,6 +97,11 @@ var (
 		Version:  kongv1.SchemeGroupVersion.Version,
 		Resource: "kongingresses",
 	}
+	kongVaultGVResource = metav1.GroupVersionResource{
+		Group:    kongv1alpha1.SchemeGroupVersion.Group,
+		Version:  kongv1alpha1.SchemeGroupVersion.Version,
+		Resource: "kongvaults",
+	}
 	secretGVResource = metav1.GroupVersionResource{
 		Group:    corev1.SchemeGroupVersion.Group,
 		Version:  corev1.SchemeGroupVersion.Version,
@@ -135,6 +141,8 @@ func (h RequestHandler) handleValidation(ctx context.Context, request admissionv
 		return h.handleHTTPRoute(ctx, request, responseBuilder)
 	case kongIngressGVResource:
 		return h.handleKongIngress(ctx, request, responseBuilder)
+	case kongVaultGVResource:
+		return h.handleKongVault(ctx, request, responseBuilder)
 	case serviceGVResource:
 		return h.handleService(ctx, request, responseBuilder)
 	case ingressGVResource:
@@ -426,3 +434,17 @@ func (h RequestHandler) handleIngress(ctx context.Context, request admissionv1.A
 
 	return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil
 }
+
+func (h RequestHandler) handleKongVault(ctx context.Context, request admissionv1.AdmissionRequest, responseBuilder *ResponseBuilder) (*admissionv1.AdmissionResponse, error) {
+	kongVault := kongv1alpha1.KongVault{}
+	_, _, err := codecs.UniversalDeserializer().Decode(request.Object.Raw, nil, &kongVault)
+	if err != nil {
+		return nil, err
+	}
+	ok, message, err := h.Validator.ValidateVault(ctx, kongVault)
+	if err != nil {
+		return nil, err
+	}
+
+	return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil
+}
diff --git a/internal/admission/validator.go b/internal/admission/validator.go
index 4513ee6b1..80c62443d 100644
--- a/internal/admission/validator.go
+++ b/internal/admission/validator.go
@@ -26,6 +26,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 )
 
@@ -35,6 +36,7 @@ type KongValidator interface {
 	ValidateConsumerGroup(ctx context.Context, consumerGroup kongv1beta1.KongConsumerGroup) (bool, string, error)
 	ValidatePlugin(ctx context.Context, plugin kongv1.KongPlugin, overrideSecrets []*corev1.Secret) (bool, string, error)
 	ValidateClusterPlugin(ctx context.Context, plugin kongv1.KongClusterPlugin, overrideSecrets []*corev1.Secret) (bool, string, error)
+	ValidateVault(ctx context.Context, vault kongv1alpha1.KongVault) (bool, string, error)
 	ValidateCredential(ctx context.Context, secret corev1.Secret) (bool, string)
 	ValidateGateway(ctx context.Context, gateway gatewayapi.Gateway) (bool, string, error)
 	ValidateHTTPRoute(ctx context.Context, httproute gatewayapi.HTTPRoute) (bool, string, error)
@@ -49,6 +51,7 @@ type AdminAPIServicesProvider interface {
 	GetConsumerGroupsService() (kong.AbstractConsumerGroupService, bool)
 	GetInfoService() (kong.AbstractInfoService, bool)
 	GetRoutesService() (kong.AbstractRouteService, bool)
+	GetVaultsService() (kong.AbstractVaultService, bool)
 }
 
 // ConsumerGetter is an interface for retrieving KongConsumers.
@@ -520,6 +523,30 @@ func (noOpRoutesValidator) Validate(_ context.Context, _ *kong.Route) (bool, str
 	return true, "", nil
 }
 
+func (validator KongHTTPValidator) ValidateVault(ctx context.Context, k8sKongVault kongv1alpha1.KongVault) (bool, string, error) {
+	// Ignore KongVaults that are being managed by another controller.
+	if !validator.ingressClassMatcher(&k8sKongVault.ObjectMeta, annotations.IngressClassKey, annotations.ExactClassMatch) {
+		return true, "", nil
+	}
+	config, err := kongstate.RawConfigToConfiguration(k8sKongVault.Spec.Config.Raw)
+	if err != nil {
+		return false, fmt.Sprintf(ErrTextVaultConfigUnmarshalFailed, err), nil
+	}
+	kongVault := kong.Vault{
+		Name:        kong.String(k8sKongVault.Spec.Backend),
+		Prefix:      kong.String(k8sKongVault.Spec.Prefix),
+		Description: kong.String(k8sKongVault.Spec.Description),
+		Config:      config,
+	}
+	// TODO: /schemas/vaults/test does not check "unique" restraint on `prefix` field:
+	// https://github.com/Kong/kubernetes-ingress-controller/issues/5395
+	errText, err := validator.validateVaultAgainstGatewaySchema(ctx, kongVault)
+	if err != nil || errText != "" {
+		return false, errText, err
+	}
+	return true, "", nil
+}
+
 // -----------------------------------------------------------------------------
 // KongHTTPValidator - Private Methods
 // -----------------------------------------------------------------------------
@@ -582,6 +609,21 @@ func (validator KongHTTPValidator) validatePluginAgainstGatewaySchema(ctx contex
 	return "", nil
 }
 
+func (validator KongHTTPValidator) validateVaultAgainstGatewaySchema(ctx context.Context, vault kong.Vault) (string, error) {
+	vaultService, hasClient := validator.AdminAPIServicesProvider.GetVaultsService()
+	if !hasClient {
+		return "", nil
+	}
+	isValid, msg, err := vaultService.Validate(ctx, &vault)
+	if err != nil {
+		return ErrTextVaultUnableToValidate, err
+	}
+	if !isValid {
+		return fmt.Sprintf(ErrTextVaultConfigValidationResultInvalid, msg), nil
+	}
+	return "", nil
+}
+
 type managerClientSecretGetter struct {
 	managerClient client.Client
 }
diff --git a/internal/controllers/configuration/zz_generated_controllers.go b/internal/controllers/configuration/zz_generated_controllers.go
index ed4c3a898..fadccec52 100644
--- a/internal/controllers/configuration/zz_generated_controllers.go
+++ b/internal/controllers/configuration/zz_generated_controllers.go
@@ -1903,6 +1903,176 @@ func (r *IncubatorV1Alpha1KongServiceFacadeReconciler) Reconcile(ctx context.Con
 	return ctrl.Result{}, nil
 }
 
+// -----------------------------------------------------------------------------
+// KongV1Alpha1 KongVault - Reconciler
+// -----------------------------------------------------------------------------
+
+// KongV1Alpha1KongVaultReconciler reconciles KongVault resources
+type KongV1Alpha1KongVaultReconciler 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 = &KongV1Alpha1KongVaultReconciler{}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *KongV1Alpha1KongVaultReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	c, err := controller.New("KongV1Alpha1KongVault", mgr, controller.Options{
+		Reconciler: r,
+		LogConstructor: func(_ *reconcile.Request) logr.Logger {
+			return r.Log
+		},
+		CacheSyncTimeout: r.CacheSyncTimeout,
+	})
+	if err != nil {
+		return err
+	}
+	// if configured, start the status updater controller
+	if r.StatusQueue != nil {
+		if err := c.Watch(
+			&source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{
+				Group:   "configuration.konghq.com",
+				Version: "v1alpha1",
+				Kind:    "KongVault",
+			})},
+			&handler.EnqueueRequestForObject{},
+		); err != nil {
+			return err
+		}
+	}
+	if !r.DisableIngressClassLookups {
+		err = c.Watch(
+			source.Kind(mgr.GetCache(), &netv1.IngressClass{}),
+			handler.EnqueueRequestsFromMapFunc(r.listClassless),
+			predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass),
+		)
+		if err != nil {
+			return err
+		}
+	}
+	preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName)
+	return c.Watch(
+		source.Kind(mgr.GetCache(), &kongv1alpha1.KongVault{}),
+		&handler.EnqueueRequestForObject{},
+		preds,
+	)
+}
+
+// listClassless finds and reconciles all objects without ingress class information
+func (r *KongV1Alpha1KongVaultReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request {
+	resourceList := &kongv1alpha1.KongVaultList{}
+	if err := r.Client.List(ctx, resourceList); err != nil {
+		r.Log.Error(err, "Failed to list classless kongvaults")
+		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 *KongV1Alpha1KongVaultReconciler) SetLogger(l logr.Logger) {
+	r.Log = l
+}
+
+//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults,verbs=get;list;watch
+//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults/status,verbs=get;update;patch
+
+// Reconcile processes the watched objects
+func (r *KongV1Alpha1KongVaultReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("KongV1Alpha1KongVault", req.NamespacedName)
+
+	// get the relevant object
+	obj := new(kongv1alpha1.KongVault)
+
+	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", "KongVault", "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/dataplane/deckgen/generate.go b/internal/dataplane/deckgen/generate.go
index 382bb1f7f..8445c68e7 100644
--- a/internal/dataplane/deckgen/generate.go
+++ b/internal/dataplane/deckgen/generate.go
@@ -203,6 +203,18 @@ func ToDeckContent(
 	sort.SliceStable(content.Consumers, func(i, j int) bool {
 		return strings.Compare(*content.Consumers[i].Username, *content.Consumers[j].Username) > 0
 	})
+
+	// convert vaults.
+	for _, v := range k8sState.Vaults {
+		vault := file.FVault{
+			Vault: v.Vault,
+		}
+		content.Vaults = append(content.Vaults, vault)
+	}
+	sort.SliceStable(content.Vaults, func(i, j int) bool {
+		return (*content.Vaults[i].Prefix) > (*content.Vaults[j].Prefix)
+	})
+
 	if len(params.SelectorTags) > 0 {
 		content.Info = &file.Info{
 			SelectorTags: params.SelectorTags,
diff --git a/internal/dataplane/failures/failures.go b/internal/dataplane/failures/failures.go
index b628a6990..1ba4eaf2d 100644
--- a/internal/dataplane/failures/failures.go
+++ b/internal/dataplane/failures/failures.go
@@ -41,9 +41,11 @@ func NewResourceFailure(reason string, causingObjects ...client.Object) (Resourc
 		if obj.GetName() == "" {
 			return ResourceFailure{}, fmt.Errorf("one of causing objects (%s) has no name", gvk.String())
 		}
+
 		if obj.GetNamespace() == "" {
 			return ResourceFailure{}, fmt.Errorf("one of causing objects (%s) has no namespace", gvk.String())
 		}
+
 	}
 
 	return ResourceFailure{
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index e582c95d4..18290319b 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -31,6 +31,7 @@ type KongState struct {
 	Plugins        []Plugin
 	Consumers      []Consumer
 	ConsumerGroups []ConsumerGroup
+	Vaults         []Vault
 }
 
 // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort.
@@ -59,6 +60,7 @@ func (ks *KongState) SanitizedCopy() *KongState {
 			return
 		}(),
 		ConsumerGroups: ks.ConsumerGroups,
+		Vaults:         ks.Vaults,
 	}
 }
 
@@ -239,6 +241,34 @@ func (ks *KongState) FillUpstreamOverrides(
 	}
 }
 
+func (ks *KongState) FillVaults(
+	logger logr.Logger,
+	s store.Storer,
+	failuresCollector *failures.ResourceFailuresCollector,
+) {
+	for _, vault := range s.ListKongVaults() {
+		config, err := RawConfigToConfiguration(vault.Spec.Config.Raw)
+		if err != nil {
+			logger.Error(err, "failed to parse configuration of vault to JSON", "name", vault.Name)
+			failuresCollector.PushResourceFailure(
+				fmt.Sprintf("failed to parse configuration of vault %s to JSON: %v", vault.Name, err),
+				vault,
+			)
+			continue
+		}
+		ks.Vaults = append(ks.Vaults, Vault{
+			Vault: kong.Vault{
+				Name:        kong.String(vault.Spec.Backend),
+				Description: kong.String(vault.Spec.Description),
+				Prefix:      kong.String(vault.Spec.Prefix),
+				Config:      config,
+				Tags:        util.GenerateTagsForObject(vault),
+			},
+			K8sKongVault: vault.DeepCopy(),
+		})
+	}
+}
+
 func (ks *KongState) getPluginRelations() map[string]util.ForeignRelations {
 	// KongPlugin key (KongPlugin's name:namespace) to corresponding associations
 	pluginRels := map[string]util.ForeignRelations{}
@@ -487,6 +517,14 @@ func (ks *KongState) FillIDs(logger logr.Logger) {
 			ks.ConsumerGroups[consumerGroupIndex] = consumerGroup
 		}
 	}
+
+	for valutIndex, vault := range ks.Vaults {
+		if err := vault.FillID(); err != nil {
+			logger.Error(err, "Failed to fill ID for vault", "vault_name", vault.FriendlyName())
+		} else {
+			ks.Vaults[valutIndex] = vault
+		}
+	}
 }
 
 // maybeLogKongIngressDeprecationError iterates over services and logs a deprecation error if a service
diff --git a/internal/dataplane/kongstate/plugin.go b/internal/dataplane/kongstate/plugin.go
index 0c0de55bb..56b746588 100644
--- a/internal/dataplane/kongstate/plugin.go
+++ b/internal/dataplane/kongstate/plugin.go
@@ -223,7 +223,7 @@ func RawConfigurationWithPatchesToConfiguration(
 			return kong.Configuration{}, err
 		}
 	}
-	return rawConfigToConfiguration(raw)
+	return RawConfigToConfiguration(raw)
 }
 
 // RawConfigurationWithNamespacedPatchesToConfiguration converts config and add patches from configPatches of KongClusterPlugin.
@@ -253,21 +253,7 @@ func RawConfigurationWithNamespacedPatchesToConfiguration(
 			return kong.Configuration{}, err
 		}
 	}
-	return rawConfigToConfiguration(raw)
-}
-
-// rawConfigToConfiguration decodes raw JSON to the format of Kong configuration.
-// it is run after all patches applied to the initial config.
-func rawConfigToConfiguration(raw []byte) (kong.Configuration, error) {
-	if len(raw) == 0 {
-		return kong.Configuration{}, nil
-	}
-	var kongConfig kong.Configuration
-	err := json.Unmarshal(raw, &kongConfig)
-	if err != nil {
-		return kong.Configuration{}, err
-	}
-	return kongConfig, nil
+	return RawConfigToConfiguration(raw)
 }
 
 // NamespacedSecretToConfiguration fetches specified value from given namespace, secret and key,
diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go
index 471fde2e6..f5ef152a7 100644
--- a/internal/dataplane/kongstate/util.go
+++ b/internal/dataplane/kongstate/util.go
@@ -1,9 +1,11 @@
 package kongstate
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
+	"github.com/kong/go-kong/kong"
 	corev1 "k8s.io/api/core/v1"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
@@ -83,3 +85,17 @@ func prettyPrintServiceList(services []*corev1.Service) string {
 	}
 	return strings.Join(serviceList, ", ")
 }
+
+// RawConfigToConfiguration decodes raw JSON to the format of Kong configuration.
+// it is run after all patches applied to the initial config.
+func RawConfigToConfiguration(raw []byte) (kong.Configuration, error) {
+	if len(raw) == 0 {
+		return kong.Configuration{}, nil
+	}
+	var kongConfig kong.Configuration
+	err := json.Unmarshal(raw, &kongConfig)
+	if err != nil {
+		return kong.Configuration{}, err
+	}
+	return kongConfig, nil
+}
diff --git a/internal/dataplane/kongstate/vault.go b/internal/dataplane/kongstate/vault.go
new file mode 100644
index 000000000..4a53d9a0f
--- /dev/null
+++ b/internal/dataplane/kongstate/vault.go
@@ -0,0 +1,13 @@
+package kongstate
+
+import (
+	"github.com/kong/go-kong/kong"
+
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+)
+
+type Vault struct {
+	kong.Vault
+
+	K8sKongVault *kongv1alpha1.KongVault
+}
diff --git a/internal/dataplane/translator/translator.go b/internal/dataplane/translator/translator.go
index ba239733d..41eb09d3c 100644
--- a/internal/dataplane/translator/translator.go
+++ b/internal/dataplane/translator/translator.go
@@ -180,6 +180,12 @@ func (t *Translator) BuildKongConfig() KongConfigBuildingResult {
 		t.registerSuccessfullyTranslatedObject(&result.Consumers[i].K8sKongConsumer)
 	}
 
+	// generate vaults
+	result.FillVaults(t.logger, t.storer, t.failuresCollector)
+	for i := range result.Vaults {
+		t.registerSuccessfullyTranslatedObject(result.Vaults[i].K8sKongVault)
+	}
+
 	// process consumer groups
 	result.FillConsumerGroups(t.logger, t.storer)
 	for i := range result.ConsumerGroups {
diff --git a/internal/manager/config.go b/internal/manager/config.go
index 5122be093..ae40c81b9 100644
--- a/internal/manager/config.go
+++ b/internal/manager/config.go
@@ -109,6 +109,7 @@ type Config struct {
 	ServiceEnabled                bool
 	KongUpstreamPolicyEnabled     bool
 	KongServiceFacadeEnabled      bool
+	KongVaultEnabled              bool
 
 	// Gateway API toggling.
 	GatewayAPIGatewayController        bool
@@ -249,6 +250,7 @@ func (c *Config) FlagSet() *pflag.FlagSet {
 	flagSet.BoolVar(&c.GatewayAPIHTTPRouteController, "enable-controller-gwapi-httproute", true, "Enable the Gateway API HTTPRoute controller.")
 	flagSet.BoolVar(&c.GatewayAPIReferenceGrantController, "enable-controller-gwapi-reference-grant", true, "Enable the Gateway API ReferenceGrant controller.")
 	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.")
 
 	// 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 5c64fcded..ed201983e 100644
--- a/internal/manager/controllerdef.go
+++ b/internal/manager/controllerdef.go
@@ -285,6 +285,19 @@ func setupControllers(
 				StatusQueue:                kubernetesStatusQueue,
 			},
 		},
+		{
+			Enabled: c.KongVaultEnabled,
+			Controller: &configuration.KongV1Alpha1KongVaultReconciler{
+				Client:                     mgr.GetClient(),
+				Log:                        ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongVault"),
+				Scheme:                     mgr.GetScheme(),
+				DataplaneClient:            dataplaneClient,
+				CacheSyncTimeout:           c.CacheSyncTimeout,
+				IngressClassName:           c.IngressClassName,
+				DisableIngressClassLookups: !c.IngressClassNetV1Enabled,
+				StatusQueue:                kubernetesStatusQueue,
+			},
+		},
 		// ---------------------------------------------------------------------------
 		// Gateway API Controllers
 		// ---------------------------------------------------------------------------
diff --git a/internal/store/cache_stores.go b/internal/store/cache_stores.go
index 289e526d9..a4483fb2c 100644
--- a/internal/store/cache_stores.go
+++ b/internal/store/cache_stores.go
@@ -50,6 +50,7 @@ type CacheStores struct {
 	KongUpstreamPolicy             cache.Store
 	IngressClassParametersV1alpha1 cache.Store
 	KongServiceFacade              cache.Store
+	KongVault                      cache.Store
 
 	l *sync.RWMutex
 }
@@ -82,6 +83,7 @@ func NewCacheStores() CacheStores {
 		KongUpstreamPolicy:             cache.NewStore(keyFunc),
 		IngressClassParametersV1alpha1: cache.NewStore(keyFunc),
 		KongServiceFacade:              cache.NewStore(keyFunc),
+		KongVault:                      cache.NewStore(clusterResourceKeyFunc),
 
 		l: &sync.RWMutex{},
 	}
@@ -188,6 +190,8 @@ func (c CacheStores) Get(obj runtime.Object) (item interface{}, exists bool, err
 		return c.IngressClassParametersV1alpha1.Get(obj)
 	case *incubatorv1alpha1.KongServiceFacade:
 		return c.KongServiceFacade.Get(obj)
+	case *kongv1alpha1.KongVault:
+		return c.KongVault.Get(obj)
 	}
 	return nil, false, fmt.Errorf("%T is not a supported cache object type", obj)
 }
@@ -252,6 +256,8 @@ func (c CacheStores) Add(obj runtime.Object) error {
 		return c.IngressClassParametersV1alpha1.Add(obj)
 	case *incubatorv1alpha1.KongServiceFacade:
 		return c.KongServiceFacade.Add(obj)
+	case *kongv1alpha1.KongVault:
+		return c.KongVault.Add(obj)
 	default:
 		return fmt.Errorf("cannot add unsupported kind %q to the store", obj.GetObjectKind().GroupVersionKind())
 	}
@@ -317,6 +323,8 @@ func (c CacheStores) Delete(obj runtime.Object) error {
 		return c.IngressClassParametersV1alpha1.Delete(obj)
 	case *incubatorv1alpha1.KongServiceFacade:
 		return c.KongServiceFacade.Delete(obj)
+	case *kongv1alpha1.KongVault:
+		return c.KongVault.Delete(obj)
 	default:
 		return fmt.Errorf("cannot delete unsupported kind %q from the store", obj.GetObjectKind().GroupVersionKind())
 	}
diff --git a/internal/store/fake_store.go b/internal/store/fake_store.go
index 11e0ef134..8df30fd15 100644
--- a/internal/store/fake_store.go
+++ b/internal/store/fake_store.go
@@ -64,6 +64,7 @@ type FakeObjects struct {
 	KongConsumerGroups             []*kongv1beta1.KongConsumerGroup
 	KongUpstreamPolicies           []*kongv1beta1.KongUpstreamPolicy
 	KongServiceFacades             []*incubatorv1alpha1.KongServiceFacade
+	KongVaults                     []*kongv1alpha1.KongVault
 }
 
 // NewFakeStore creates a store backed by the objects passed in as arguments.
@@ -218,6 +219,13 @@ func NewFakeStore(
 			return nil, err
 		}
 	}
+	kongVaultStore := cache.NewStore(clusterResourceKeyFunc)
+	for _, v := range objects.KongVaults {
+		err := kongVaultStore.Add(v)
+		if err != nil {
+			return nil, err
+		}
+	}
 
 	s = Store{
 		stores: CacheStores{
@@ -243,6 +251,7 @@ func NewFakeStore(
 			IngressClassParametersV1alpha1: IngressClassParametersV1alpha1Store,
 			KongUpstreamPolicy:             kongUpstreamPolicyStore,
 			KongServiceFacade:              kongServiceFacade,
+			KongVault:                      kongVaultStore,
 		},
 		ingressClass:          annotations.DefaultIngressClass,
 		isValidIngressClass:   annotations.IngressClassValidatorFuncFromObjectMeta(annotations.DefaultIngressClass),
@@ -279,6 +288,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) {
 		reflect.TypeOf(&kongv1.KongIngress{}):                  kongv1.SchemeGroupVersion.WithKind("KongIngress"),
 		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),
 	}
 
 	out := &bytes.Buffer{}
@@ -321,6 +331,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) {
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongIngresses)...)
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumers)...)
 	allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumerGroups)...)
+	allObjects = append(allObjects, lo.ToAnySlice(objects.KongVaults)...)
 
 	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 b7dea5a34..932397e71 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -69,6 +69,7 @@ type Storer interface {
 	GetGateway(namespace string, name string) (*gatewayapi.Gateway, error)
 	GetKongUpstreamPolicy(namespace, name string) (*kongv1beta1.KongUpstreamPolicy, error)
 	GetKongServiceFacade(namespace, name string) (*incubatorv1alpha1.KongServiceFacade, error)
+	GetKongVault(name string) (*kongv1alpha1.KongVault, error)
 
 	ListIngressesV1() []*netv1.Ingress
 	ListIngressClassesV1() []*netv1.IngressClass
@@ -88,6 +89,7 @@ type Storer interface {
 	ListKongConsumers() []*kongv1.KongConsumer
 	ListKongConsumerGroups() []*kongv1beta1.KongConsumerGroup
 	ListCACerts() ([]*corev1.Secret, error)
+	ListKongVaults() []*kongv1alpha1.KongVault
 }
 
 // Store implements Storer and can be used to list Ingress, Services
@@ -571,6 +573,18 @@ func (s Store) GetGateway(namespace string, name string) (*gatewayapi.Gateway, e
 	return obj.(*gatewayapi.Gateway), nil
 }
 
+// GetKongVault returns kongvault resource having specified name.
+func (s Store) GetKongVault(name string) (*kongv1alpha1.KongVault, error) {
+	p, exists, err := s.stores.KongVault.GetByKey(name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, NotFoundError{fmt.Sprintf("KongVault %v not found", name)}
+	}
+	return p.(*kongv1alpha1.KongVault), nil
+}
+
 // ListKongConsumers returns all KongConsumers filtered by the ingress.class
 // annotation.
 func (s Store) ListKongConsumers() []*kongv1.KongConsumer {
@@ -670,6 +684,17 @@ func (s Store) ListCACerts() ([]*corev1.Secret, error) {
 	return secrets, nil
 }
 
+func (s Store) ListKongVaults() []*kongv1alpha1.KongVault {
+	var kongVaults []*kongv1alpha1.KongVault
+	for _, obj := range s.stores.KongVault.List() {
+		kongVault, ok := obj.(*kongv1alpha1.KongVault)
+		if ok && s.isValidIngressClass(&kongVault.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) {
+			kongVaults = append(kongVaults, kongVault)
+		}
+	}
+	return kongVaults
+}
+
 // 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 {


Test patch:
diff --git a/internal/admission/server_test.go b/internal/admission/server_test.go
index f927ffce8..e83593120 100644
--- a/internal/admission/server_test.go
+++ b/internal/admission/server_test.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 )
 
@@ -77,6 +78,10 @@ func (v KongFakeValidator) ValidateIngress(_ context.Context, _ netv1.Ingress) (
 	return v.Result, v.Message, v.Error
 }
 
+func (v KongFakeValidator) ValidateVault(_ context.Context, _ kongv1alpha1.KongVault) (bool, string, error) {
+	return v.Result, v.Message, v.Error
+}
+
 func TestServeHTTPBasic(t *testing.T) {
 	assert := assert.New(t)
 	res := httptest.NewRecorder()
diff --git a/internal/admission/validator_test.go b/internal/admission/validator_test.go
index 1bc011bdc..4bb6b4bda 100644
--- a/internal/admission/validator_test.go
+++ b/internal/admission/validator_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util/builder"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 	incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
 )
@@ -62,6 +63,7 @@ type fakeServicesProvider struct {
 	consumerGroupSvc kong.AbstractConsumerGroupService
 	infoSvc          kong.AbstractInfoService
 	routeSvc         kong.AbstractRouteService
+	vaultSvc         kong.AbstractVaultService
 }
 
 func (f fakeServicesProvider) GetConsumersService() (kong.AbstractConsumerService, bool) {
@@ -99,6 +101,13 @@ func (f fakeServicesProvider) GetRoutesService() (kong.AbstractRouteService, boo
 	return nil, false
 }
 
+func (f fakeServicesProvider) GetVaultsService() (kong.AbstractVaultService, bool) {
+	if f.vaultSvc != nil {
+		return f.vaultSvc, true
+	}
+	return nil, false
+}
+
 func TestKongHTTPValidator_ValidatePlugin(t *testing.T) {
 	store, _ := store.NewFakeStore(store.FakeObjects{
 		Secrets: []*corev1.Secret{
@@ -1227,3 +1236,90 @@ func (f *fakeRouteSvc) Validate(context.Context, *kong.Route) (bool, string, err
 	}
 	return true, "", nil
 }
+
+func TestValidator_ValidateVault(t *testing.T) {
+	testCases := []struct {
+		name            string
+		kongVault       kongv1alpha1.KongVault
+		validateSvcFail bool
+		expectedOK      bool
+		expectedMessage string
+		expectedError   string
+	}{
+		{
+			name: "valid vault",
+			kongVault: kongv1alpha1.KongVault{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "vault-1",
+				},
+				Spec: kongv1alpha1.KongVaultSpec{
+					Backend: "env",
+					Prefix:  "env-1",
+				},
+			},
+			expectedOK: true,
+		},
+		{
+			name: "vault with invalid(malformed) configuration",
+			kongVault: kongv1alpha1.KongVault{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "vault-1",
+				},
+				Spec: kongv1alpha1.KongVaultSpec{
+					Backend: "env",
+					Prefix:  "env-1",
+					Config: apiextensionsv1.JSON{
+						Raw: []byte(`{{}`),
+					},
+				},
+			},
+			expectedOK:      false,
+			expectedMessage: "failed to unmarshal vault configuration",
+		},
+		{
+			name: "vault with failure in validating service",
+			kongVault: kongv1alpha1.KongVault{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "vault-1",
+				},
+				Spec: kongv1alpha1.KongVaultSpec{
+					Backend: "env",
+					Prefix:  "env-1",
+				},
+			},
+			validateSvcFail: true,
+			expectedOK:      false,
+			expectedMessage: "something is wrong with the vault",
+		},
+	}
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			validator := KongHTTPValidator{
+				AdminAPIServicesProvider: fakeServicesProvider{
+					vaultSvc: &fakeVaultSvc{
+						shouldFail: tc.validateSvcFail,
+					},
+				},
+				ingressClassMatcher: fakeClassMatcher,
+				Logger:              logr.Discard(),
+			}
+			ok, msg, err := validator.ValidateVault(context.Background(), tc.kongVault)
+			require.NoError(t, err)
+			assert.Equal(t, tc.expectedOK, ok)
+			assert.Contains(t, msg, tc.expectedMessage)
+		})
+	}
+}
+
+type fakeVaultSvc struct {
+	kong.AbstractVaultService
+	shouldFail bool
+}
+
+func (s fakeVaultSvc) Validate(context.Context, *kong.Vault) (bool, string, error) {
+	if s.shouldFail {
+		return false, "something is wrong with the vault", nil
+	}
+	return true, "", nil
+}
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 90c069e38..bf2a7b2c9 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/go-logr/logr"
+	"github.com/go-logr/logr/testr"
 	"github.com/go-logr/zapr"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
@@ -25,6 +26,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 )
 
@@ -55,6 +57,13 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 				ConsumerGroups: []ConsumerGroup{{
 					ConsumerGroup: kong.ConsumerGroup{ID: kong.String("1"), Name: kong.String("consumer-group")},
 				}},
+				Vaults: []Vault{
+					{
+						Vault: kong.Vault{
+							Name: kong.String("test-vault"), Prefix: kong.String("test-vault"),
+						},
+					},
+				},
 			},
 			want: KongState{
 				Services:       []Service{{Service: kong.Service{ID: kong.String("1")}}},
@@ -69,6 +78,13 @@ func TestKongState_SanitizedCopy(t *testing.T) {
 				ConsumerGroups: []ConsumerGroup{{
 					ConsumerGroup: kong.ConsumerGroup{ID: kong.String("1"), Name: kong.String("consumer-group")},
 				}},
+				Vaults: []Vault{
+					{
+						Vault: kong.Vault{
+							Name: kong.String("test-vault"), Prefix: kong.String("test-vault"),
+						},
+					},
+				},
 			},
 		},
 	} {
@@ -909,6 +925,37 @@ func TestKongState_FillIDs(t *testing.T) {
 				require.NotEmpty(t, s.Consumers[0].ID)
 			},
 		},
+		{
+			name: "fills consumer, consumer group, vault IDs",
+			state: KongState{
+				Consumers: []Consumer{
+					{
+						Consumer: kong.Consumer{
+							Username: kong.String("user.0"),
+						},
+					},
+				},
+				ConsumerGroups: []ConsumerGroup{
+					{
+						ConsumerGroup: kong.ConsumerGroup{
+							Name: kong.String("cg.0"),
+						},
+					},
+				},
+				Vaults: []Vault{
+					{
+						Vault: kong.Vault{
+							Prefix: kong.String("vault.0"),
+						},
+					},
+				},
+			},
+			expect: func(t *testing.T, s KongState) {
+				require.NotEmpty(t, s.Consumers[0].ID)
+				require.NotEmpty(t, s.ConsumerGroups[0].ID)
+				require.NotEmpty(t, s.Vaults[0].ID)
+			},
+		},
 	}
 
 	for _, tc := range testCases {
@@ -1140,3 +1187,123 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 		})
 	}
 }
+
+func TestFillVaults(t *testing.T) {
+	kongVaultTypeMeta := metav1.TypeMeta{
+		APIVersion: kongv1alpha1.GroupVersion.String(),
+		Kind:       "KongVault",
+	}
+	testCases := []struct {
+		name                     string
+		kongVaults               []*kongv1alpha1.KongVault
+		expectedTranslatedVaults []Vault
+		// name of KongVault -> failure message
+		expectedTranslationFailures map[string]string
+	}{
+		{
+			name: "single valid KongVault",
+			kongVaults: []*kongv1alpha1.KongVault{
+				{
+					TypeMeta: kongVaultTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "",
+						Name:      "vault-1",
+						Annotations: map[string]string{
+							annotations.IngressClassKey: annotations.DefaultIngressClass,
+						},
+					},
+					Spec: kongv1alpha1.KongVaultSpec{
+						Backend: "env",
+						Prefix:  "env-1",
+					},
+				},
+			},
+			expectedTranslatedVaults: []Vault{
+				{
+					Vault: kong.Vault{
+						Name:   kong.String("env"),
+						Prefix: kong.String("env-1"),
+					},
+					K8sKongVault: &kongv1alpha1.KongVault{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "vault-1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "one valid KongVault with correct ingress class, and one KongVault with other ingress class",
+			kongVaults: []*kongv1alpha1.KongVault{
+				{
+					TypeMeta: kongVaultTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "vault-1",
+						Annotations: map[string]string{
+							annotations.IngressClassKey: annotations.DefaultIngressClass,
+						},
+					},
+					Spec: kongv1alpha1.KongVaultSpec{
+						Backend: "env",
+						Prefix:  "env-1",
+					},
+				},
+				{
+					TypeMeta: kongVaultTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "vault-2",
+						Annotations: map[string]string{
+							annotations.IngressClassKey: "other-ingress-class",
+						},
+					},
+					Spec: kongv1alpha1.KongVaultSpec{
+						Backend: "env",
+						Prefix:  "env-2",
+					},
+				},
+			},
+			expectedTranslatedVaults: []Vault{
+				{
+					Vault: kong.Vault{
+						Name:   kong.String("env"),
+						Prefix: kong.String("env-1"),
+					},
+					K8sKongVault: &kongv1alpha1.KongVault{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "vault-1",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			s, err := store.NewFakeStore(store.FakeObjects{
+				KongVaults: tc.kongVaults,
+			})
+
+			require.NoError(t, err)
+			logger := testr.New(t)
+			f := failures.NewResourceFailuresCollector(logger)
+			ks := &KongState{}
+			ks.FillVaults(logger, s, f)
+
+			assert.Len(t, ks.Vaults, len(tc.expectedTranslatedVaults), "should have expected number of translated vaults")
+			for _, expectedVault := range tc.expectedTranslatedVaults {
+				assert.Truef(t, lo.ContainsBy(ks.Vaults, func(v Vault) bool {
+					return (v.Name != nil && *v.Name == *expectedVault.Name) &&
+						(v.Prefix != nil && *v.Prefix == *expectedVault.Prefix) &&
+						(v.K8sKongVault != nil && v.K8sKongVault.Name == expectedVault.K8sKongVault.Name)
+				}),
+					"cannot find translated vault for KongVault %q", expectedVault.K8sKongVault.Name,
+				)
+			}
+
+			// TODO: check translation failures after we implement translation failure events for cluster scoped objects:
+			// https://github.com/Kong/kubernetes-ingress-controller/issues/5387
+		})
+	}
+}
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 117abe0bd..1acbbc511 100644
--- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 688c3f2d6..9c7f36a14 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 c52ce3400..81dc2c9d1 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 b96fde016..5ce0728a7 100644
--- a/test/e2e/manifests/all-in-one-dbless.yaml
+++ b/test/e2e/manifests/all-in-one-dbless.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 3ecd9a22b..e419a3535 100644
--- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 de95096ea..da4e0f37b 100644
--- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/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 9b239680f..060959a1e 100644
--- a/test/e2e/manifests/all-in-one-postgres.yaml
+++ b/test/e2e/manifests/all-in-one-postgres.yaml
@@ -1981,6 +1981,184 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: kongvaults.configuration.konghq.com
+spec:
+  group: configuration.konghq.com
+  names:
+    categories:
+    - kong-ingress-controller
+    kind: KongVault
+    listKind: KongVaultList
+    plural: kongvaults
+    shortNames:
+    - kv
+    singular: kongvault
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the backend of the vault
+      jsonPath: .spec.backend
+      name: Backend Type
+      type: string
+    - description: Prefix of vault URI to reference the values in the vault
+      jsonPath: .spec.prefix
+      name: Prefix
+      type: string
+    - description: Age
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Description
+      jsonPath: .spec.description
+      name: Description
+      priority: 1
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="Programmed")].status
+      name: Programmed
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: 'KongVault is the schema for kongvaults API which defines a custom
+          Kong vault. A Kong vault is a storage to store sensitive data, where the
+          values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/'
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: KongVaultSpec defines specification of a custom Kong vault.
+            properties:
+              backend:
+                description: 'Backend is the type of the backend storing the secrets
+                  in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/'
+                minLength: 1
+                type: string
+              config:
+                description: Config is the configuration of the vault. Varies for
+                  different backends.
+                x-kubernetes-preserve-unknown-fields: true
+              description:
+                description: Description is the additional information about the vault.
+                type: string
+              prefix:
+                description: Prefix is the prefix of vault URI for referencing values
+                  in the vault.
+                minLength: 1
+                type: string
+            required:
+            - backend
+            - prefix
+            type: object
+          status:
+            description: KongVaultStatus represents the current status of the KongVault
+              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 KongVaultStatus.
+                  \n Known condition types are: \n * \"Programmed\""
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  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
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.13.0
@@ -2538,6 +2716,22 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - configuration.konghq.com
+  resources:
+  - kongvaults/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - configuration.konghq.com
   resources:
diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go
index 69bc0251c..ec6a3e9d8 100644
--- a/test/integration/suite_test.go
+++ b/test/integration/suite_test.go
@@ -60,6 +60,8 @@ func TestMain(m *testing.M) {
 	if testenv.KongImage() != "" && testenv.KongTag() != "" {
 		fmt.Printf("INFO: custom kong image specified via env: %s:%s\n", testenv.KongImage(), testenv.KongTag())
 	}
+	// add env for vaults.
+	kongbuilder.WithProxyEnvVar("vault_test_add_header_1", "h1:v1")
 
 	// Pin the Helm chart version.
 	kongbuilder.WithHelmChartVersion(testenv.KongHelmChartVersion())
diff --git a/test/integration/vault_test.go b/test/integration/vault_test.go
new file mode 100644
index 000000000..3b346cb9f
--- /dev/null
+++ b/test/integration/vault_test.go
@@ -0,0 +1,141 @@
+//go:build integration_tests
+
+package integration
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/kong/go-kong/kong"
+	"github.com/kong/kubernetes-testing-framework/pkg/clusters"
+	"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators"
+	"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"
+
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
+	dpconf "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/config"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+	"github.com/kong/kubernetes-ingress-controller/v3/pkg/clientset"
+	"github.com/kong/kubernetes-ingress-controller/v3/test"
+	"github.com/kong/kubernetes-ingress-controller/v3/test/consts"
+	"github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers"
+)
+
+func TestCustomVault(t *testing.T) {
+	t.Parallel()
+
+	RunWhenKongEnterprise(t)
+	// TODO: run hcv vault to enable test with DBMode
+	RunWhenKongDBMode(t, dpconf.DBModeOff, "Skipping because DBMode cannot support env vault")
+
+	ctx := context.Background()
+	ns, cleaner := helpers.Setup(ctx, t, env)
+
+	t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
+	container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort)
+	deployment := generators.NewDeploymentForContainer(container)
+	deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(deployment)
+
+	t.Logf("exposing deployment %s via service", deployment.Name)
+	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
+	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
+	require.NoError(t, err)
+	cleaner.Add(service)
+
+	t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
+	ingress := generators.NewIngressForService("/test_custom_vault", map[string]string{
+		"konghq.com/strip-path": "true",
+	}, service)
+	ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
+	require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress))
+	cleaner.Add(ingress)
+
+	t.Log("waiting for routes from Ingress to be operational")
+	require.Eventually(t, func() bool {
+		resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_custom_vault", proxyURL))
+		if err != nil {
+			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
+			return false
+		}
+		defer resp.Body.Close()
+		if resp.StatusCode == http.StatusOK {
+			b := new(bytes.Buffer)
+			n, err := b.ReadFrom(resp.Body)
+			require.NoError(t, err)
+			require.True(t, n > 0)
+			return strings.Contains(b.String(), "<title>httpbin.org</title>")
+		}
+		return false
+	}, ingressWait, waitTick)
+
+	t.Logf("creating a Kong vault using env backend")
+
+	c, err := clientset.NewForConfig(env.Cluster().Config())
+	require.NoError(t, err)
+
+	_, err = c.ConfigurationV1alpha1().KongVaults().Create(ctx, &kongv1alpha1.KongVault{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-env-vault",
+			Annotations: map[string]string{
+				annotations.IngressClassKey: consts.IngressClass,
+			},
+		},
+		Spec: kongv1alpha1.KongVaultSpec{
+			Backend:     "env",
+			Prefix:      "test-env",
+			Description: "env vault for test",
+			Config: apiextensionsv1.JSON{
+				Raw: []byte(`{"prefix":"kong_vault_test_"}`),
+			},
+		},
+	}, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	t.Logf("create a request-transformer-advanced plugin referencing the value from the vault")
+	_, err = c.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, &kongv1.KongPlugin{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ns.Name,
+			Name:      "request-transformer-advanced",
+		},
+		PluginName: "request-transformer-advanced",
+		Config: apiextensionsv1.JSON{
+			Raw: []byte(`{"add":{"headers":["{vault://test-env/add-header-1}"]}}`),
+		},
+	}, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	t.Logf("attach plugin to ingress and check if the config from vault takes effect")
+	ingress, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{})
+	require.NoError(t, err)
+	ingress.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = "request-transformer-advanced"
+	_, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{})
+	require.NoError(t, err)
+	require.Eventuallyf(t, func() bool {
+		resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_custom_vault/headers", proxyURL))
+		if err != nil {
+			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
+			return false
+		}
+		defer resp.Body.Close()
+		if resp.StatusCode == http.StatusOK {
+			b := new(bytes.Buffer)
+			n, err := b.ReadFrom(resp.Body)
+			require.NoError(t, err)
+			require.True(t, n > 0)
+			return strings.Contains(b.String(), `"H1": "v1"`)
+		}
+		return false
+	},
+		ingressWait, waitTick,
+		"Cannot find added header in request",
+	)
+}