New to KubeDB? Please start here.
Virtual Secrets For Postgres: Secure Kubernetes Secrets
KubeDB’s Virtual Secrets feature enhances the security of your database credentials by allowing you to use external secret management systems instead of storing sensitive information directly in Kubernetes Secrets. This guide will walk you through the steps to set up and use Virtual Secrets with your Postgres database in KubeDB.
Virtual Secrets Design
Virtual Secrets extends Kubernetes by introducing a new Secret resource under the virtual-secrets.dev API group. From a user perspective, it behaves similarly to the native Kubernetes Secret
resource, providing familiar workflows for managing sensitive data. Unlike standard Kubernetes Secrets, Virtual Secrets does not store secret data in etcd. Instead, it securely stores the
actual secret data in an external secret manager, ensuring enhanced security and compliance.
The Virtual Secret resource is structured into two distinct components:
Secret Data– The sensitive information itself, stored externally to protect against unauthorized access.
Secret Metadata – Non-sensitive information retained within the Kubernetes cluster to improve performance and support standard API operations.
This design ensures a seamless Kubernetes experience while providing enterprise-grade security for managing secrets.
Prerequisites
Before you begin, ensure you have the following prerequisites in place:
At first, you need to have a Kubernetes cluster, and the
kubectlcommand-line tool must be configured to communicate with your cluster. If you do not already have a cluster, you can create one by using kind.Install
KubeDBCommunity and Enterprise operator in your cluster following the steps here.A running vault server with kubeVault operator installed. Follow the installation guide here.
You should be familiar with the following
KubeDBconcepts:
To keep everything isolated, we are going to use a separate namespace called demo throughout this tutorial.
$ kubectl create ns demo
namespace/demo created
How to use Virtual Secrets
Install Virtual Secrets Server
First, install the virtual-secret-server which is a custom api server for the secrets.virtual-secrets.dev resource.
$ helm repo add appscode https://charts.appscode.com/stable/
$ helm repo update
$ helm search repo appscode/virtual-secrets-server --version=v2025.3.14
$ helm upgrade -i virtual-secrets-server appscode/virtual-secrets-server \
--version=v2025.3.14 -n kubevault --create-namespace
Deploy and Configure Vault Server
Before we create a custom Secret, we need to deploy a vault server where the secret data will be stored. Also, it needs to be configured to grant necessary permissions like create, update,
read, list, delete and delete in a kv secret engine named virtual-secrets.dev to the virtual-secrets-server.
Now let’s configure the vault server with following commands:
# enable kv secret engine in the path virtual-secrets.dev
$ vault secrets enable -path=virtual-secrets.dev -version=2 kv
Success! Enabled the kv secrets engine at: virtual-secrets.dev/
# creates a policy with the permission to create, update, read, list and delete
$ vault policy write virtual-secrets-policy - <<EOF
path "virtual-secrets.dev/*" {
capabilities = ["create", "update", "read", "list", "delete"]
}
EOF
Success! Uploaded policy: virtual-secrets-policy
# binds this policy with a service account of the virtual-secrets server
$ vault write auth/kubernetes/role/virtual-secrets-role \
bound_service_account_names=virtual-secrets-server \
bound_service_account_namespaces=kubevault \
policies="virtual-secrets-policy"
Success! Data written to: auth/kubernetes/role/virtual-secrets-role
Create SecretStore
We need to create another resource called SecretStore which will contain the connection information to the external secret manager where the secrets will be stored.
apiVersion: config.virtual-secrets.dev/v1alpha1
kind: SecretStore
metadata:
name: vault
spec:
vault:
url: http://vault.vault-demo.svc:8200
roleName: virtual-secrets-role
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2025.10.17/docs/examples/vault/secretstore.yaml
secretstore.config.virtual-secrets.dev/vault configured
Here,
spec.vault- section describes the connection information for vault.spec.url- contains the connection url to the vault server.spec.roleName- contains the role name we specified when binding the policy to the service account earlier.
Note:
spec.aws,spec.azureandspec.gcpcan be used to specify the connection information of the corresponding secret manager.
Create Virtual Secret
Now, we are going to create a Virtual Secret resource that will store the Postgres credentials in the vault server.
apiVersion: virtual-secrets.dev/v1alpha1
kind: Secret
metadata:
name: virtual-secret
namespace: demo
stringData:
username: appscode
password: virtual-secret
secretStoreName: vault
Here,
secretStoreName- specifies the SecretStore we just created.- Other than that, everything else is similar to a core Kubernetes Secret. Let’s go ahead and apply the Secret,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2025.10.17/docs/examples/vault/virtualsecret.yaml
secret.virtual-secrets.dev/virtual-secret created
Let’s list the Secrets to see if it is created or not,
kubectl get secrets.virtual-secrets.dev -n demo
NAME TYPE DATA AGE
virtual-secret Opaque 2 2d19h
We can also get the whole definition of the Secret,
$ kubectl get secrets.virtual-secrets.dev -n demo virtual-secret -oyaml
apiVersion: virtual-secrets.dev/v1alpha1
data:
password: dmlydHVhbC1zZWNyZXQ=
username: YXBwc2NvZGU=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"virtual-secrets.dev/v1alpha1","kind":"Secret","metadata":{"annotations":{},"name":"virtual-secret","namespace":"demo"},"secretStoreName":"vault","stringData":{"password":"virtual-secret","username":"appscode"}}
creationTimestamp: "2025-12-30T11:13:54Z"
generation: 1
name: virtual-secret
namespace: demo
resourceVersion: "12922"
uid: c7887183-6885-4435-a3c9-c741b28130a3
secretStoreName: vault
type: Opaque
We can see that this Secretactually behaves identical of the core Secret. But the data is not stored in the etcd and it is way more secure than using the native k8s Secret.
Check server secret existence in Vault
We will connect to the Vault by using Vault CLI. Therefore, we need to export the necessary environment variables and port-forward the service.
In one terminal port-forward the vault server service,
$ kubectl port-forward -n vault-demo service/vault 8200
Forwarding from 127.0.0.1:8200 -> 8200
Forwarding from [::1]:8200 -> 8200
$ export VAULT_ADDR=http://127.0.0.1:8200
$ export VAULT_TOKEN=(kubectl vault root-token get vaultserver vault -n demo --value-only)
$ vault kv get virtual-secrets.dev/demo/virtual-secret
================ Secret Path ================
virtual-secrets.dev/data/demo/virtual-secret
======= Metadata =======
Key Value
--- -----
created_time 2025-12-30T11:13:54.455334654Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password virtual-secret
username appscode
We can see that the secret data is stored in the virtual-secrets.dev/demo/virtual-secret path where,
virtual-secret.devis the secret engine name.demois the namespace.virtual-secretis the name of the secret.
Mount Virtual Secret in Postgres
Secrets are not that useful if we can not mount them to pods. We can mount the virtual secrets using Secrets Store CSI Driver .
Virtual Secrets comes with a custom provider of Secrets Store CSI Driver, named secrets-store-csi-driver-provider-virtual-secrets which leverages virtual-secrets-server to read secret
data from virtual secrets and uses the Secrets Store CSI Driver to mount those into to the pods.
Let’s go ahead and install Secrets Store CSI Driver and secrets-store-csi-driver-provider-virtual-secrets into our cluster,
$ helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
$ helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver --namespace kube-system
$ helm search repo appscode/secrets-store-csi-driver-provider-virtual-secrets --version=v2025.3.14
$ helm upgrade -i secrets-store-csi-driver-provider-virtual-secrets appscode/secrets-store-csi-driver-provider-virtual-secrets -n kube-system --create-namespace --version=v2025.3.14
If both of them are deployed we should see two new pods in the kube-system namespace.
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
csi-secrets-store-secrets-store-csi-driver-rvpvm 3/3 Running 0 61s
secrets-store-csi-driver-provider-virtual-secrets-m78gv 1/1 Running 0 34s
The Secrets Store CSI Driver uses a custom resource named SecretProviderClass to mount the secret. Let’s go ahead and create that,
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: virtual-secret
namespace: demo
spec:
provider: virtual-secrets
parameters:
secretName: virtual-secret
Here,
spec.provider- specifies the provider for Secrets Store CSI Driver to communicate and use.parameters.secretName- specifies the name of the virtual secret we want to mount.
Note: -We can also call the mount subresource of the virtual secret to create the SecretProviderClass for us. -The namespace and the name of SecretProviderClass should be same as the Virtual Secret it is being used for. Let’s create the SecretProviderClass,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2025.10.17/docs/examples/vault/secretProviderClass.yaml
secretproviderclass.secrets-store.csi.x-k8s.io/virtual-secret created
With the custom secret created, the authentication configured and role created, the provider-virtual-secrets extension installed and the SecretProviderClass defined it is finally time to
create a pod that mounts the desired secret.
kind: Pod
apiVersion: v1
metadata:
name: webapp
namespace: demo
spec:
containers:
- image: jweissig/app:0.0.1
name: webapp
volumeMounts:
- name: virtual-secrets-store
mountPath: "/mnt/virtual-secrets"
readOnly: true
volumes:
- name: virtual-secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "virtual-secret"
Here,
- In
spec.volumes[0], a volume with namevirtual-secrets-storewith necessary configs is specified. - In
spec.containers[0].volumeMounts, the volume is referred to be mounted in the/mnt/virtual-secretspath.
Let’s create the pod,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2025.10.17/docs/examples/vault/webapp.yaml
pod/webapp created
If we get the pod we will see that it will get to the Running state after some period,
$ kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
webapp 1/1 Running 0 6m45s
Now, check the secret data written to the file system at /mnt/virtual-secrets on the webapp pod.
$ kubectl exec -n demo webapp -- cat /mnt/virtual-secrets/username
appscode⏎
$ kubectl exec -n demo webapp -- cat /mnt/virtual-secrets/password
virtual-secret⏎
The value displayed matches the username and password value for the custom secret named virtual-secret we created earlier.
Use Virtual Secrets with Postgres
Virtual Secrets is integrated with KubeDB from the v2025.3.24 and it can be used to store KubeDB’s database credential. Now, the support has been added for Postgres.
We can proceed with deploying a Postgres which will use virtual-secrets to create custom secret for the database authentication credential.
apiVersion: kubedb.com/v1
kind: Postgres
metadata:
name: pg
namespace: demo
spec:
authSecret:
apiGroup: "virtual-secrets.dev"
secretStoreName: vault
replicas: 3
storage:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageType: Durable
deletionPolicy: WipeOut
version: "17.5"
Here,
spec.authSecret.apiGroup- specifies that we want to use virtual secrets instead of native k8s secret.spec.authSecret.secretStoreName- specifies theSecretStoreresource that contains the connection information for external secret store to store the secret data.
We can now apply the Postgres custom resource,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2025.10.17/docs/examples/postgres/virtual_secret/postgres.yaml
Postgres.kubedb.com/rd created
Now, wait until pg has status Ready. i.e. ,
$ kubectl get Postgres -n demo
NAME VERSION STATUS AGE
pg 8.2.2 Ready 2m5s
Now, lets go ahead and check what secret it is using,
$ kubectl get secrets.virtual-secrets.dev -n demo
NAME TYPE DATA AGE
pg-auth kubernetes.io/basic-auth 2 1m53s
We can see that a virtual-secret named pg-auth has been created by the KubeDB operator. Let’s get the whole definition of the virtual secret,
kubectl get secrets.virtual-secrets.dev -n demo pg-auth -oyaml
apiVersion: virtual-secrets.dev/v1alpha1
data:
password: RUdKbCF0SEVHelpvWXdNaQ==
username: cG9zdGdyZXM=
kind: Secret
metadata:
annotations:
kubedb.com/auth-active-from: "2026-01-02T09:02:17Z"
creationTimestamp: "2026-01-02T09:02:18Z"
generation: 1
labels:
app.kubernetes.io/component: database
app.kubernetes.io/instance: pg
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: postgreses.kubedb.com
name: pg-auth
namespace: demo
ownerReferences:
- apiVersion: kubedb.com/v1
blockOwnerDeletion: true
controller: true
kind: Postgres
name: pg
uid: def8f647-0613-43ec-8d5d-ba63e9a12113
resourceVersion: "90389"
uid: 5c3c2a39-698d-44e7-8c63-8c14593581ad
secretStoreName: vault
type: kubernetes.io/basic-auth
In our vault server, we can check if this data exists or not,
vault kv get virtual-secrets.dev/demo/pg-auth
============ Secret Path ============
virtual-secrets.dev/data/demo/pg-auth
======= Metadata =======
Key Value
--- -----
created_time 2026-01-02T09:02:17.998634474Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
====== Data ======
Key Value
--- -----
password EGJl!tHEGzZoYwMi
username postgres
We can see that the Postgres user password is stored in the vault server. Now let’s go ahead and connect to the database using the psql client to check whether it is working or not.
kubectl exec -it -n demo pg-0 -- bash
Defaulted container "postgres" out of: postgres, pg-coordinator, postgres-init-container (init)
pg-0:/$ PGPASSWORD='EGJl!tHEGzZoYwMi' psql -U postgres -d postgres -p 5432 -h pg.demo.svc
psql (17.5)
Type "help" for help.
postgres=# CREATE DATABASE my_database;
CREATE DATABASE
postgres=# \c my_database
You are now connected to database "my_database" as user "postgres".
my_database=# CREATE TABLE users (
my_database(# id SERIAL PRIMARY KEY,
my_database(# name VARCHAR(100) NOT NULL,
my_database(# email VARCHAR(150) UNIQUE NOT NULL,
my_database(# created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
my_database(# );
CREATE TABLE
my_database=# \dt
List of relations
Schema | Name | Type | Owner
--------+-------+-------+----------
public | users | table | postgres
(1 row)
We can see that we are able to connect to the database and create a database and a table successfully.
Cleanup
To clean up the resources created in this guide, run the following commands:
$ kubectl delete -n demo postgres pg
$ kubectl delete -n demo webapp,virtual-secret,secretproviderclass.virtual-secrets.dev/virtual-secret
$ kubectl delete ns demo
$ helm uninstall virtual-secrets-server -n kubevault
$ helm uninstall secrets-store-csi-driver-provider-virtual-secrets -n kube-system
$ helm uninstall csi-secrets-store -n kube-system
If you want to uninstall the KubeVault, run:
$ helm uninstall kubevault --namespace kubevault
Next Steps
- Learn about backup and restore PostgreSQL database using Stash.
- Learn about initializing PostgreSQL with Script.
- Learn about custom PostgresVersions.
- Want to setup PostgreSQL cluster? Check how to configure Highly Available PostgreSQL Cluster
- Monitor your PostgreSQL database with KubeDB using built-in Prometheus.
- Monitor your PostgreSQL database with KubeDB using Prometheus operator.
- Detail concepts of Postgres object.
- Use private Docker registry to deploy PostgreSQL with KubeDB.
- Want to hack on KubeDB? Check our contribution guidelines.































