Helm Secrets: Managing Sensitive Data
Helm Secrets: Managing Sensitive Data
Securely manage secrets in Helm charts — from Helm Secrets plugin and SOPS to Azure Key Vault and External Secrets Operator.
Table of Contents
- The Problem
- Helm Secrets + SOPS
- External Secrets Operator
- Sealed Secrets
- Azure Key Vault
- Best Practices
The Problem
Helm values files are plain text — storing password: secret123 in values-prod.yaml means secrets live in Git forever. Even with .gitignore, the file is still on disk.
You need one of these approaches:
| Approach | How | Pros | Cons |
|---|---|---|---|
| Helm Secrets + SOPS | Encrypt values files | Git-safe, offline | Extra tooling |
| External Secrets | Sync from cloud vaults | Native secrets, auto-rotation | Requires operator |
| Sealed Secrets | Encrypt into git-safe CRDs | Declarative, GitOps-friendly | Controller required |
| Manual | kubectl create secret before deploy | Simple | Manual, error-prone |
Helm Secrets + SOPS
Encrypt secrets in values files using SOPS (by Mozilla, now maintained at getsops).
Setup
# Install Helm Secrets plugin
helm plugin install https://github.com/jkroepke/helm-secrets
# Install SOPS
brew install sops # or download from GitHub releases
Create Encrypted Values
# Create a SOPS-encrypted file
sops secrets.prod.yaml
This opens your editor with the decrypted content — saves encrypted.
# secrets.prod.yaml — encrypted on disk
db:
password: ENC[AES256_GCM,data:abc123...,type:str]
api:
key: ENC[AES256_GCM,data:def456...,type:str]
Deploy with Encrypted Secrets
# Helm Secrets decrypts on the fly during template/install
helm secrets upgrade myapp ./chart \
-f secrets.prod.yaml \
--namespace prod
In CI/CD
# Decrypt with a key stored in CI secrets
export SOPS_AGE_KEY=$SOPS_AGE_KEY # from CI secret store
sops -d secrets.prod.yaml > decrypted.yaml
helm upgrade myapp ./chart -f decrypted.yaml
rm decrypted.yaml
External Secrets Operator
Sync secrets from cloud vaults (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, GCP Secret Manager) into Kubernetes Secrets.
Install
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets-system \
--set installCRDs=true
Configure SecretStore
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-keyvault
spec:
provider:
azurekv:
tenantId: <tenant-id>
vaultUrl: https://my-vault.vault.azure.net/
authSecretRef:
clientId:
name: azure-creds
key: ClientID
clientSecret:
name: azure-creds
key: ClientSecret
ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
spec:
refreshInterval: 15s
secretStoreRef:
name: azure-keyvault
kind: SecretStore
target:
name: db-secret
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: database-password
- secretKey: apiKey
remoteRef:
key: api-key
Creates a Kubernetes Secret named db-secret synced from Key Vault every 15 seconds.
Sealed Secrets
Encrypt secrets into a SealedSecret CRD that can be safely committed to Git.
Install
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml
Seal a Secret
# Install kubeseal CLI
brew install kubeseal
# Create a sealed secret
kubectl create secret generic db-secret \
--from-literal=password=mysecret \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# sealed-secret.yaml — safe to commit
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-secret
spec:
encryptedData:
password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEQ... # encrypted
# Apply — controller creates the real Secret
kubectl apply -f sealed-secret.yaml
kubectl get secret db-secret # now exists
Azure Key Vault
Using Azure Key Vault CSI Driver
helm install csi-secrets-store-provider-azure \
https://azure.github.io/secrets-store-csi-driver-provider-azure/charts/csi-secrets-store-provider-azure.tgz \
--namespace kube-system
# SecretProviderClass — mounts Key Vault as volume
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-kv
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: <identity-client-id>
keyvaultName: my-vault
objects: |
array:
- |
objectName: db-password
objectType: secret
- |
objectName: api-key
objectType: secret
# Pod mounts it
spec:
containers:
- name: app
volumeMounts:
- name: secrets-store
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: azure-kv
Best Practices
1. Never Commit Plain Secrets
# Bad — values-prod.yaml in git
db:
password: supersecret123
# Good — values-prod.yaml references a pre-existing secret
db:
password: {{ .Values.db.password }} # from secrets store at deploy time
2. Separate Secret Management from App Deployment
# Step 1: Create secrets (by admin or CI with elevated perms)
kubectl create secret generic db-secret \
--from-literal=password=$DB_PASSWORD \
-n production
# Step 2: Deploy app referencing existing secret
helm upgrade myapp ./chart \
--set db.existingSecret=db-secret \
-n production
3. Use existingSecret Pattern in Charts
# In your chart's deployment.yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.db.existingSecret }}
key: password
4. Rotate Secrets Regularly
# With External Secrets — rotate in the vault, auto-syncs
# With Helm Secrets — re-encrypt with new values
# With Sealed Secrets — re-seal and re-apply
Quick Comparison
| Method | Git-Safe | Auto-Rotation | Setup Complexity | Best For |
|---|---|---|---|---|
| Helm Secrets + SOPS | ✅ | ❌ | Medium | Small teams, simple needs |
| External Secrets | ✅ | ✅ | Medium | Production, cloud-native |
| Sealed Secrets | ✅ | ❌ | Low | GitOps workflows |
| Azure KV CSI | ✅ | ✅ | Medium | Azure AKS clusters |
| Manual kubectl | ❌ | ❌ | None | Quick testing only |
Related Articles
Last Updated: October 2026
Author: CloudOpsGuide Team
Difficulty: Intermediate
Estimated Reading Time: 15 minutes