Managing Secrets in Kubernetes on AKS

Kubernetes Secrets look like a secure credential store but have a critical limitation: they are base64-encoded, not encrypted, by default. This page covers how native Kubernetes Secrets work, why that matters, and how to replace them with Azure Key Vault integration using the Secrets Store CSI Driver on AKS.

How Kubernetes Secrets work

A Kubernetes Secret is an API object that stores small amounts of sensitive data — passwords, tokens, TLS certificates, connection strings. Pods consume secrets either as environment variables or as files mounted into the container filesystem.

The data field in a Secret stores values as base64 strings. Base64 is encoding, not encryption. Anyone with read access to the Secret object can decode the value in one command. By default, Secrets are stored unencrypted in the etcd database that backs the Kubernetes control plane.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  username: cHJvZHVzZXI=      # base64 of "produser"
  password: U3VwM3JTM2NyZXQ=  # base64 of "Sup3rS3cret"

You apply this with kubectl apply -f secret.yaml. Any pod in the same namespace can then reference it. The problem is that this Secret object, including the encoded value, lives in etcd and is readable by anyone with RBAC permission to get or list Secrets in that namespace — including cluster admins and, in a misconfigured cluster, service accounts.

Warning

Never commit Secret YAML files to source control. The base64 encoding provides no security — it is trivially reversible. Use a secrets management tool to inject values at deploy time, or use the Key Vault CSI driver to keep secrets out of the cluster entirely.

Mounting a secret as environment variables is the most common pattern but has a further drawback: environment variables are visible in process listings and are often captured in debug dumps or crash reports. Mounting as files is marginally better because access is controlled by the filesystem, but the root problem — the secret lives in etcd — remains either way.

# Consuming a Secret as environment variables
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: password

# Consuming a Secret as a mounted file
volumeMounts:
  - name: db-secret-vol
    mountPath: /etc/secrets
    readOnly: true
volumes:
  - name: db-secret-vol
    secret:
      secretName: db-credentials

Azure Key Vault integration with the Secrets Store CSI Driver

The Secrets Store CSI Driver is the production solution for AKS secret management. Instead of storing credentials in Kubernetes, you store them in Azure Key Vault and configure the driver to mount them into pods at runtime. Secrets never land in etcd.

On AKS, the CSI driver is available as a managed add-on, which means Microsoft handles installation and upgrades. Enable it when creating a cluster or add it to an existing cluster:

# Enable on an existing cluster
az aks enable-addons \
  --resource-group myResourceGroup \
  --name myAKSCluster \
  --addons azure-keyvault-secrets-provider

# Verify the DaemonSet is running
kubectl get daemonset \
  -n kube-system \
  -l app=secrets-store-csi-driver

The add-on installs two DaemonSets: the CSI driver itself and the Azure Key Vault provider. Both run on every node. When a pod is scheduled that references a SecretProviderClass, the driver contacts Key Vault and mounts the requested secrets before the pod starts.

Authentication from the driver to Key Vault uses the cluster’s managed identity. The add-on creates a user-assigned managed identity for the Secrets Provider automatically, or you can configure Workload Identity for per-pod identity granularity.

Creating a SecretProviderClass

A SecretProviderClass is a custom resource that tells the CSI driver which Key Vault to connect to, which secrets to fetch, and how to map them. You create one per set of secrets your application needs.

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: app-secrets
  namespace: production
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "<client-id-of-managed-identity>"
    keyvaultName: "myapp-keyvault"
    tenantId: "<your-tenant-id>"
    objects: |
      array:
        - |
          objectName: db-password
          objectType: secret
          objectVersion: ""
        - |
          objectName: api-key
          objectType: secret
          objectVersion: ""
  secretObjects:
    - secretName: app-k8s-secret
      type: Opaque
      data:
        - objectName: db-password
          key: password
        - objectName: api-key
          key: apiKey

The objects array lists which Key Vault secrets to fetch. The secretObjects section is optional — it causes the driver to also create a regular Kubernetes Secret synced from Key Vault. This is useful for workloads that consume secrets as environment variables rather than mounted files.

Get the tenant ID and managed identity client ID with:

# Get tenant ID
az account show --query tenantId -o tsv

# Get the client ID of the secrets provider identity
az aks show \
  --resource-group myResourceGroup \
  --name myAKSCluster \
  --query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId \
  -o tsv

Grant the managed identity access to Key Vault secrets:

az keyvault set-policy \
  --name myapp-keyvault \
  --object-id <managed-identity-object-id> \
  --secret-permissions get list

If you are using the newer Azure RBAC model for Key Vault (recommended), assign the role instead:

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee <managed-identity-client-id> \
  --scope /subscriptions/<sub-id>/resourceGroups/myRG/providers/Microsoft.KeyVault/vaults/myapp-keyvault

Mounting secrets into pods

Reference the SecretProviderClass in your pod spec using a CSI volume. The secrets appear as files in the mount path. Each secret becomes a file named after the Key Vault object name.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: app
          image: myregistry.azurecr.io/myapp:1.4.0
          volumeMounts:
            - name: secrets-vol
              mountPath: /mnt/secrets
              readOnly: true
          env:
            # Reference the synced Kubernetes Secret for env vars
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: app-k8s-secret
                  key: password
      volumes:
        - name: secrets-vol
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: app-secrets

With this setup, /mnt/secrets/db-password contains the raw secret value fetched from Key Vault. The file is available only inside the container and is not stored in etcd. When a secret is rotated in Key Vault, the driver polls for changes (default every two minutes) and updates the mounted file. Applications that read the file on each use will pick up rotations automatically; applications that cache the value at startup will need to be restarted.

Tip

Set rotationPollInterval in the SecretProviderClass parameters to control how frequently the driver checks Key Vault for updated secret versions. The default is 2 minutes. For high-security environments, shorter intervals catch rotations faster but increase Key Vault API calls.

Files vs environment variables for secrets

The CSI driver mounts secrets as files. If your application reads configuration from environment variables, use the secretObjects field to sync a Kubernetes Secret, then reference that Secret in your env block.

MethodSecurityRotation supportEtcd exposure
Native Kubernetes Secret → env varWeak (base64 in etcd)Manual restart requiredYes
Native Kubernetes Secret → fileWeak (base64 in etcd)File updates, app must re-readYes
CSI Driver → file mountStrong (Key Vault backed)Auto-rotated in fileNo
CSI Driver → synced K8s Secret → env varMedium (Secret exists in etcd)Pod restart requiredYes (synced copy)

For maximum security, use file mounts exclusively and write your application to read secrets from files at runtime. This keeps credentials entirely out of etcd and supports rotation without pod restarts.

Common mistakes

  1. Committing Secret YAML to Git. Even with values redacted or base64-encoded, Secret manifests in source control create a history of sensitive data. Use a tool like Helm with externally provided values, or generate secrets through CI/CD pipelines that pull from Key Vault — never from YAML files checked into a repository.
  2. Granting overly broad Key Vault access. Giving the managed identity the “Key Vault Administrator” role or access to all secrets in a vault gives every pod using that identity access to every secret. Create separate vaults or use separate managed identities for different applications, and grant only the specific secrets each workload needs.
  3. Relying on env-var rotation without pod restarts. When using the synced Kubernetes Secret approach, the driver updates the Kubernetes Secret object when Key Vault changes, but environment variables in running containers are not updated. Applications reading rotated credentials from env vars will continue using the old value until the pod is restarted. Use file mounts if you need live rotation without restarts.

Frequently asked questions

Are Kubernetes Secrets encrypted at rest?

Not by default. Secrets are stored base64-encoded in etcd. AKS with the Azure Key Vault CSI driver keeps secrets out of etcd entirely, pulling them from Key Vault at mount time. You can also enable etcd encryption at rest on AKS, but the CSI driver approach is the stronger option.

What is the Secrets Store CSI Driver?

It is a Kubernetes CSI (Container Storage Interface) driver that mounts secrets from external stores — including Azure Key Vault — directly into pods as files or as synced Kubernetes Secrets. It runs as a DaemonSet on each node and pulls secrets on pod startup.

Do I need a service principal to use the Key Vault CSI driver on AKS?

No. The recommended approach is to use a Managed Identity (either the cluster's kubelet identity or a user-assigned identity with Workload Identity). This avoids storing service principal credentials and lets Azure handle authentication automatically.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.