Create a Vault backend for ESO¶
In this tutorial, we will connect a Kubernetes cluster to Vault for secret management using the External Secrets Operator. We will create a ClusterSecretStore with token authentication, configure Kubernetes RBAC so that namespaces have read-only access to their assigned secrets, and set up a Kubernetes CronJob to renew the Vault token.
Along the way, we will create a Vault policy and token, sync secrets as an admin, and verify secure access.
What you need¶
A running Kubernetes cluster.
External Secrets Operator installed (for example, in the
external-secretsnamespace).An existing Vault server, accessible from the cluster (for example, at
https://vault.example.com:8200).Vault CLI installed on your local machine, with admin access to Vault.
Kubernetes CLI (
kubectl) with admin access.Vault’s KV secrets engine (v2) enabled at the
secret/path.
Now, create a dedicated namespace for credentials.
Run this command:
kubectl create namespace external-secrets-credentials
You will see output like this:
namespace/external-secrets-credentials created
Create a Vault policy¶
First, create a policy file named eso-readonly-policy.hcl with this content:
path "secret/data/*" {
capabilities = ["read", "list"]
}
Now, apply the policy in Vault.
Run this command:
vault policy write eso-readonly eso-readonly-policy.hcl
You will see output like this:
Success! Uploaded policy: eso-readonly
Create a periodic Vault token¶
Now, create a renewable token attached to the policy.
Run this command:
vault token create \
-policy=eso-readonly \
-period=30d \
-explicit-max-ttl=90d \
-display-name="eso-cluster-token"
You will see output like this. Copy the token value (it starts with hvs. or s.).
Key Value
--- -----
token hvs.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
token_accessor XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
token_duration 720h
token_renewable true
token_policies ["eso-readonly"]
...
See also
Store the token in Kubernetes¶
Now, store the token in a Kubernetes Secret.
Run this command, replacing <YOUR_TOKEN> with the token value:
kubectl create secret generic vault-token \
--namespace external-secrets-credentials \
--from-literal=token=<YOUR_TOKEN>
You will see output like this:
secret/vault-token created
Create the ClusterSecretStore¶
Create a file named cluster-secret-store.yaml with this content. Replace https://vault.example.com:8200 with your Vault server URL.
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-cluster-store
spec:
provider:
vault:
server: "https://vault.example.com:8200"
path: "secret"
version: "v2"
auth:
tokenSecretRef:
name: vault-token
key: token
namespace: external-secrets-credentials
Now, apply it.
Run this command:
kubectl apply -f cluster-secret-store.yaml
You will see output like this:
clustersecretstore.external-secrets.io/vault-cluster-store created
Check that it is ready.
Run this command:
kubectl get clustersecretstore vault-cluster-store -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
You will see:
True
If it shows False, run kubectl describe clustersecretstore vault-cluster-store to check for errors.
See also
Set up Kubernetes RBAC for namespaces¶
Create a file named namespace-developer-role.yaml with this content:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: namespace-developer
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
Now, apply it.
Run this command:
kubectl apply -f namespace-developer-role.yaml
You will see output like this:
clusterrole.rbac.authorization.k8s.io/namespace-developer created
Now, create an example namespace.
Run this command:
kubectl create namespace my-app-ns
You will see:
namespace/my-app-ns created
Bind the role to a user in the namespace. Replace dev-user with a real user name.
Create a file named role-binding.yaml with this content:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: my-app-ns
subjects:
- kind: User
name: dev-user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: namespace-developer
apiGroup: rbac.authorization.k8s.io
Apply it:
kubectl apply -f role-binding.yaml
You will see:
rolebinding.rbac.authorization.k8s.io/developer-binding created
Notice that this allows read-only access to secrets but no creation of ExternalSecret objects.
Create a CronJob to renew the token¶
Create a ServiceAccount for the Job.
Run this command:
kubectl create serviceaccount token-renewer -n external-secrets-credentials
You will see:
serviceaccount/token-renewer created
No additional RBAC is needed since we are not updating the Secret—the renewal extends the existing token.
Now, create a file named token-renew-cronjob.yaml with this content. Replace https://vault.example.com:8200 with your Vault URL.
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-token-renewer
namespace: external-secrets-credentials
spec:
schedule: "0 */12 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: renewer
image: hashicorp/vault:1.15
env:
- name: VAULT_ADDR
value: "https://vault.example.com:8200"
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-token
key: token
command:
- /bin/sh
- -c
- |
vault token renew $VAULT_TOKEN
restartPolicy: OnFailure
serviceAccountName: token-renewer
See also
Apply it:
kubectl apply -f token-renew-cronjob.yaml
You will see:
cronjob.batch/vault-token-renewer created
To test, run the Job manually.
Run this command:
kubectl create job --from=cronjob/vault-token-renewer test-renew -n external-secrets-credentials
Then check the pod logs.
Find the pod name with:
kubectl get pods -n external-secrets-credentials
Then:
kubectl logs <pod-name> -n external-secrets-credentials
You will see output like:
Success! Token renewed.
Create and sync a secret as an admin¶
Now, create a secret in Vault.
Run this command:
vault kv put secret/my-app-ns/my-app-secret db_password=supersecure
You will see:
Success! Data written to: secret/my-app-ns/my-app-secret
Create a file named external-secret.yaml with this content:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-app-external-secret
namespace: my-app-ns
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-cluster-store
kind: ClusterSecretStore
target:
name: my-app-secret
creationPolicy: Owner
data:
- secretKey: db_password
remoteRef:
key: my-app-ns/my-app-secret
property: db_password
Apply it:
kubectl apply -f external-secret.yaml
You will see:
externalsecret.external-secrets.io/my-app-external-secret created
Check the synced secret:
kubectl get secret my-app-secret -n my-app-ns -o jsonpath='{.data.db_password}' | base64 --decode
You will see:
supersecure
As a namespace user (switch context to dev-user if needed), you can get the secret, but trying to create one will fail.
You have set up secure, Vault-backed secret management in Kubernetes with automatic token renewal and controlled access.