Table of Contents

Hashicorp Vault on Kubernetes

Summary: This wiki page shows how to install hashicorp vault in developer mode on kubernetes.
Date: 26 August 2026

Note that in this page we'll be deploying hashicorp Vault in β€œdev” mode. This requires no further setup, no state management, and no initialization. This is useful for experimenting with Vault without needing to unseal, store keys, etc. All data is lost on restart, so do not use dev mode for anything other than experimenting. See dev-server to know more.

This wiki page uses my public AKS cluster for as a target kubernetes cluster. However, it does not use aks specific items such as workload identity, as this page intends to do a kubernetes generic deployment.
For the local deployment I recommend to use this wsl setup, as it has all the required tooling such as kubectl and helm.
All the kubernetes manifests on this page are also available on github: hashicorp vault manifest

Deployment Vault

We'll use helm to do the installation. Make sure kubectl is connected to the correct cluster (KUBECONFIG=/mnt/c/Users/aks/.kube/aks_public_user.yaml), and add the hashicorp helm repository to your list of repositories.

helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
  --set "server.dev.enabled=true" \
  --set "ui.enabled=true" \
  --set "injector.enabled=false" \
  --set "csi.enabled=true" \
  --namespace ops

Deployment CSI provider

As we've enabled the csi-driver in the vault configuration we need to make sure csi is indeed installed. Depending on your specific kubernetes setup, chances are that the driver is already installed but if not, you can use helm again to do the installation:

helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
  --set "syncSecret.enabled=true"

Add Gateway API HTTP Route

To make sure we can access the vault without port forwarding we need to add a HTTP route resource to the Gateway.

Make sure to verify the service name and port for the vault and edit the manifest below if required: kubectl get service -n ops

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: vault
spec:
  parentRefs:
  - name: gateway
  hostnames: ["aks.getshifting.com"]
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /ui
    - path:
        type: PathPrefix
        value: /v1
    backendRefs:
    - name: vault-ui
      port: 8200

Apply the manifest: kubectl apply -f vault-http-route.yaml -n ops.

Add a Secret

Now we can add a secret to the vault using the interface:

- Go to https://aks.getshifting.com/ui. Dismiss the security warning about the certificate as a self signed certificate is created in AKS With Public Gateway API - The ui opens in your browser. Use 'token' as the authentication method and 'root' as the token itself - Go to Secrets Engines β†’ Secrets β†’ Create secret

  1. Name: vaultsecret
  2. Secret data:
    1. username: sjoerd
    2. password: testpass123!

Noe we will also create a secret in the vault using the CLI:

kubectl exec -it vault-0 -n ops -- /bin/sh
vault kv put secret/vaultsecretcli username=cli password="clipassword"
vault kv get secret/vaultsecretcli
exit

Configure Kubernetes Authentication

Now we need to configure the vault to use kubernetes authentication, so we can authenticate with Vault using a Kubernetes Service Account Token:

# Access the vault pod
kubectl exec -it vault-0 -n ops -- /bin/sh
# Enable the Kubernetes authentication method
vault auth enable kubernetes
# Configure the Kubernetes authentication method with the Kubernetes API address. It will automatically use the Vault pod's own service account token.
echo $KUBERNETES_PORT_443_TCP_ADDR
vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

Create a Policy and Role

Create a policy named shift-app. This will be used to give a service account permission to read the vault's secrets.

Note that there is an extra /data between the secrets store and the name of the secret
# Access the vault pod
kubectl exec -it vault-0 -n ops -- /bin/sh
# Create the policy
vault policy write shift-app - <<EOF
path "secret/data/vaultsecretcli" {
  capabilities = ["read"]
}
path "secret/data/vaultsecret" {
  capabilities = ["read"]
}
EOF

Create a Kubernetes authentication role named shiftrole that binds this policy with a Kubernetes service account named shift-sa.

# Access the vault pod
kubectl exec -it vault-0 -n ops -- /bin/sh
# Create the role
vault write auth/kubernetes/role/shiftrole \
  bound_service_account_names=shift-sa \
  bound_service_account_namespaces=ops \
  policies=shift-app \
  ttl=20m
exit

Create Secretprovider

Now we can create a secretprovider so we can serve a password from the vault to a pod later on:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: shift-app-spc
spec:
  provider: vault
  parameters:
    vaultAddress: "http://vault.ops:8200"
    roleName: "shiftrole"
    objects: |
      - objectName: "shift-password-cli"
        secretPath: "secret/data/vaultsecretcli"
        secretKey: "password"
      - objectName: "shift-username-cli"
        secretPath: "secret/data/vaultsecretcli"
        secretKey: "username"

Apply the secretprovider: kubectl apply -f shift-app-spc.yaml -n ops

Use the Secretprovider

To use the secretprovider we first need to create a serviceaccount with the name and in the namespace we configured earlier while making the authentication role for the vault: kubectl create serviceaccount shift-sa -n ops. Alternatively you can use a manifest:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: shift-sa
  namespace: ops

Then we can create a pod manifest in which we use the secretprovider to mount the secrets as a volume into the pod:

kind: Pod
apiVersion: v1
metadata:
  name: alpine-spc
spec:
  serviceAccountName: shift-sa
  restartPolicy: Never
  containers:
    - name: alpine-addon
      image: alpine:3.24.1
      args:
        - |-
          apk update
          apk add --no-cache curl busybox-extras bash
          sleep infinity
      command:
        - "/bin/sh"
        - "-c"
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          cpu: 250m
          memory: 256Mi
      volumeMounts:
      - name: secrets-store-inline
        mountPath: "/mnt/secrets-store"
        readOnly: true
  volumes:
    - name: secrets-store-inline
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "shift-app-spc"

Apply the pod: kubectl apply -f alpine-spc.yaml -n ops

Now we can check if the secrets from the vault are indeed available inside of the pod:

kubectl exec alpine-spc -n ops -- cat /mnt/secrets-store/shift-password-cli
kubectl exec alpine-spc -n ops -- cat /mnt/secrets-store/shift-username-cli

Sync the SecretProvider to a Kubernetes Secret

The Secrets Store CSI Driver also supports syncing to Kubernetes secret objects. To add secret syncing, update the SecretProviderClass to add a secretObjects entry:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: shift-app-spc-sync
spec:
  provider: vault
  secretObjects:
  - data:
    - key: password
      objectName: shift-password
    - key: username
      objectName: shift-username
    secretName: k8svaultsecret
    type: Opaque
  parameters:
    vaultAddress: "http://vault.ops:8200"
    roleName: "shiftrole"
    objects: |
      - objectName: "shift-password"
        secretPath: "secret/data/vaultsecret"
        secretKey: "password"
      - objectName: "shift-username"
        secretPath: "secret/data/vaultsecret"
        secretKey: "username"

Now, when you reference the SecretProviderClass, k8s secrets will also be created, which can also be loaded as an environment variable:

kind: Pod
apiVersion: v1
metadata:
  name: alpine-spc-sync
spec:
  serviceAccountName: shift-sa
  restartPolicy: Never
  containers:
    - name: alpine-addon
      image: alpine:3.24.1
      args:
        - |-
          apk update
          apk add --no-cache curl busybox-extras bash
          sleep infinity
      command:
        - "/bin/sh"
        - "-c"
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          cpu: 250m
          memory: 256Mi
      env:
      - name: SHIFT_PASSWORD
        valueFrom:
          secretKeyRef:
            name: k8svaultsecret
            key: password
      volumeMounts:
      - name: secrets-store-inline
        mountPath: "/mnt/secrets-store"
        readOnly: true
  volumes:
    - name: secrets-store-inline
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "shift-app-spc-sync"

Apply the manifests to verify:

kubectl apply -f shift-app-spc-sync.yaml -n ops
kubectl apply -f alpine-spc-sync.yaml -n ops
kubectl get secret k8svaultsecret -n ops
kubectl exec alpine-spc-sync -n ops -- env | grep SHIFT_PASSWORD

This wiki has been made possible by: