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.
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-credentialsAzure 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-driverThe 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: apiKeyThe 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 tsvGrant the managed identity access to Key Vault secrets:
az keyvault set-policy \
--name myapp-keyvault \
--object-id <managed-identity-object-id> \
--secret-permissions get listIf 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-keyvaultMounting 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-secretsWith 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.
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.
| Method | Security | Rotation support | Etcd exposure |
|---|---|---|---|
| Native Kubernetes Secret → env var | Weak (base64 in etcd) | Manual restart required | Yes |
| Native Kubernetes Secret → file | Weak (base64 in etcd) | File updates, app must re-read | Yes |
| CSI Driver → file mount | Strong (Key Vault backed) | Auto-rotated in file | No |
| CSI Driver → synced K8s Secret → env var | Medium (Secret exists in etcd) | Pod restart required | Yes (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
- 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.
- 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.
- 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.
Summary
- Kubernetes Secrets are base64-encoded, not encrypted, and are stored in etcd by default — treat them as plaintext for security planning purposes.
- The Secrets Store CSI Driver add-on for AKS connects pods to Azure Key Vault, mounting secrets as files without storing them in etcd.
- A SecretProviderClass defines which vault, which secrets, and which managed identity to use; the pod spec references it via a CSI volume.
- File mounts support automatic rotation polling; environment variable-backed secrets require a pod restart to pick up new values.
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.