A production-ready Helm chart for synchronizing Azure Key Vault secrets across multiple regions and subscriptions, enabling disaster recovery and high availability strategies.
Note: This code was automatically generated using AI (Claude Sonnet 4.5) and reviewed/adjusted by a human (me). Please review carefully and test in a safe environment before deploying to production.
- Specific Vaults: Sync only named Key Vaults
- All Vaults: Sync all accessible vaults in a subscription or resource group
- All Except: Sync all vaults except explicitly excluded ones
- Cross-Subscription Sync: Sync between different Azure subscriptions
- Same Subscription: Sync within same subscription to different regions
- Automatic Context Switching: Seamlessly handles subscription changes
- Workload Identity: Azure Workload Identity (recommended, no credentials stored)
- Service Principal: Traditional SP authentication for clusters without Workload Identity
- Email: SMTP-based notifications with TLS support
- Slack: Webhook-based alerts to Slack channels
- Microsoft Teams: Native Teams webhook integration
- Telegram: Bot-based notifications
- No Stored Credentials: Azure Workload Identity support
- RBAC Integration: Least-privilege access with separate read/write roles
- Pod Security: Non-root execution, read-only filesystem, dropped capabilities
- Azure subscription with Key Vaults
- AKS cluster (with Workload Identity enabled for recommended auth method)
- Helm 3.x
- Azure CLI
- Docker (for building the image)
# Set variables
export SUBSCRIPTION_ID="your-subscription-id"
export RESOURCE_GROUP="your-resource-group"
export IDENTITY_NAME="akv-sync-identity"
export SOURCE_KV="your-source-keyvault"
export DEST_KV="your-dest-keyvault"
# Create User-Assigned Managed Identity
az identity create \
--name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP
# Get identity details
export CLIENT_ID=$(az identity show \
--name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--query clientId -o tsv)
export TENANT_ID=$(az account show --query tenantId -o tsv)
# Assign read permissions on source vault
az role assignment create \
--assignee $CLIENT_ID \
--role "Key Vault Secrets User" \
--scope $(az keyvault show --name $SOURCE_KV --query id -o tsv)
# Assign write permissions on destination vault
az role assignment create \
--assignee $CLIENT_ID \
--role "Key Vault Secrets Officer" \
--scope $(az keyvault show --name $DEST_KV --query id -o tsv)
# Configure Workload Identity (for AKS)
# See docs/INSTALLATION.md for complete setup# Build from repo root
docker build -t yourregistry.azurecr.io/akv-sync:latest .
docker push yourregistry.azurecr.io/akv-sync:latest# my-values.yaml
image:
repository: yourregistry.azurecr.io/akv-sync
tag: "latest"
azureIdentity:
clientId: "your-managed-identity-client-id"
tenantId: "your-tenant-id"
enabled: true
authentication:
method: "workload-identity"
source:
selectionMode: "specific"
keyvaults:
- name: "your-source-kv"
destination:
region: "northeurope"
namingPattern: "{source_name}-replica"
autoCreate: true
sync:
dryRun: true # Start with dry run
logLevel: "INFO"
cronjob:
schedule: "*/5 * * * *"
notifications:
enabled: true
events:
onFailure: true
onWarning: true
slack:
enabled: true
webhookSecret:
name: "slack-webhook"
key: "url"# Create namespace
kubectl create namespace akv-sync
# Create Slack webhook secret (if using notifications)
kubectl create secret generic slack-webhook \
--from-literal=url='https://hooks.slack.com/services/YOUR/WEBHOOK/URL' \
-n akv-sync
# Install the chart
helm install akv-sync ./helm-chart \
--namespace akv-sync \
--values my-values.yaml# Trigger manual test
kubectl create job --from=cronjob/akv-sync test-$(date +%s) -n akv-sync
# Watch logs
kubectl logs -n akv-sync -l app.kubernetes.io/name=akv-sync --tail=100 -f
# Check that dry-run worked
# Then disable dry-run for production
helm upgrade akv-sync ./helm-chart \
--namespace akv-sync \
--values my-values.yaml \
--set sync.dryRun=falseSpecific vaults:
source:
selectionMode: "specific"
keyvaults:
- name: "vault1"
- name: "vault2"
# Optional: specify explicit destination name
- name: "vault3"
destinationName: "custom-dest-vault"All vaults:
source:
selectionMode: "all"
resourceGroup: "production-rg" # OptionalAll except excluded:
source:
selectionMode: "allExcept"
excludeKeyvaults:
- "dev-vault"
- "test-vault"Same subscription (different regions):
azure:
sourceSubscriptionId: "11111111-1111-1111-1111-111111111111"
# destinationSubscriptionId not specified - uses source subscriptionCross-subscription sync:
azure:
sourceSubscriptionId: "11111111-1111-1111-1111-111111111111"
destinationSubscriptionId: "22222222-2222-2222-2222-222222222222"Workload Identity (Recommended):
authentication:
method: "workload-identity"
azureIdentity:
clientId: "managed-identity-client-id"
tenantId: "your-tenant-id"
enabled: trueService Principal:
authentication:
method: "service-principal"
servicePrincipal:
clientId: "service-principal-app-id"
tenantId: "your-tenant-id"
secretRef:
name: "service-principal-secret"
key: "client-secret"All notification channels support event filtering:
notifications:
enabled: true
events:
onSuccess: false # Don't spam on success
onFailure: true # Alert on failures
onWarning: true # Alert on warningsSlack:
notifications:
slack:
enabled: true
webhookSecret:
name: "slack-webhook"
key: "url"
channel: "#alerts"Email:
notifications:
email:
enabled: true
smtpServer: "smtp.gmail.com"
smtpPort: 587
smtpUser: "notifications@example.com"
smtpPasswordSecret:
name: "smtp-credentials"
key: "password"
from: "akv-sync@example.com"
to:
- "ops@example.com"Microsoft Teams:
notifications:
teams:
enabled: true
webhookSecret:
name: "teams-webhook"
key: "url"Telegram:
notifications:
telegram:
enabled: true
botTokenSecret:
name: "telegram-credentials"
key: "token"
chatIdSecret:
name: "telegram-credentials"
key: "chatId"# Check status
kubectl get cronjob,jobs -n akv-sync
# View logs
kubectl logs -n akv-sync -l app.kubernetes.io/name=akv-sync --tail=100
# Trigger manual sync
kubectl create job --from=cronjob/akv-sync manual-$(date +%s) -n akv-sync
# Upgrade configuration
helm upgrade akv-sync ./helm-chart -n akv-sync -f my-values.yaml
# Suspend/resume
helm upgrade akv-sync ./helm-chart -n akv-sync --reuse-values --set cronjob.suspend=true
# Change schedule
helm upgrade akv-sync ./helm-chart -n akv-sync --reuse-values --set cronjob.schedule="*/10 * * * *"Flexible Naming Options:
- Use naming pattern - Automatically generate names based on placeholders
- Keep same name - Use
{source_name}pattern to keep the same name - Explicit names - Specify
destinationNameper vault for full control
# Option 1: Keep same name (useful for cross-region in same subscription)
destination:
namingPattern: "{source_name}"
# Option 2: Add suffix
destination:
namingPattern: "{source_name}-replica"
# Option 3: Mix of pattern and explicit names
source:
keyvaults:
- name: "prod-vault-1" # Will use naming pattern
- name: "prod-vault-2" # Will use naming pattern
- name: "special-vault"
destinationName: "custom-target-name" # Explicit override
# Option 4: All explicit names
source:
keyvaults:
- name: "source-vault-a"
destinationName: "dest-vault-a"
- name: "source-vault-b"
destinationName: "dest-vault-b"Other Features:
- Auto-Create: Automatically create missing destination vaults
- Validation: Alerts if destination doesn't exist
- Custom Resource Group: Specify different RG for destinations
Replicate vaults to different region within same subscription for disaster recovery.
Example: See helm-chart/examples/same-subscription-workload-identity.yaml
Sync from one subscription to another (e.g., shared services to business units).
Example: See helm-chart/examples/cross-subscription-service-principal.yaml
Sync all production vaults except dev/test.
Example: See helm-chart/examples/all-except.yaml
Critical infrastructure with all notification channels.
Example: See helm-chart/examples/multiple-vaults-full-notifications.yaml
| Feature | Workload Identity | Service Principal |
|---|---|---|
| Setup complexity | Simple (on Azure) | Moderate |
| Credential management | Automatic | Manual rotation needed |
| Security | Excellent | Good (if managed properly) |
| Cross-subscription | ✅ Yes (same tenant) | ✅ Yes (any tenant) |
| Cross-tenant | ❌ No | ✅ Yes |
| Non-Azure clusters | ❌ No | ✅ Yes |
| Recommended for | Azure AKS | Legacy/cross-tenant/non-Azure |
Recommendation: Use Workload Identity when available. Use Service Principal only when:
- Your AKS cluster doesn't have Workload Identity enabled
- You need cross-tenant synchronization
- Running on non-Azure Kubernetes clusters
- docs/INSTALLATION.md - Complete installation guide with troubleshooting
- docs/ADVANCED.md - Advanced scenarios (cross-subscription, service principal, security)
- helm-chart/values.yaml - All configuration options with detailed comments
- helm-chart/examples/ - Ready-to-use configurations for common scenarios
- Use separate managed identities for different environments
- Assign least-privilege roles (Secrets User on source, Secrets Officer on destination)
- Enable diagnostic logging on Key Vaults
- Regular access reviews
- Store credentials securely: Always use Kubernetes Secrets, never in values.yaml
- Rotate credentials regularly: Set up rotation schedule (e.g., quarterly)
- Monitor access: Enable audit logging and set up alerts
- Least privilege: Only assign necessary permissions on specific vaults
- Migrate when possible: Move to Workload Identity when cluster supports it
- Never commit webhook URLs or tokens to source control
- Store all sensitive notification credentials in Kubernetes Secrets
- Use secret references in values.yaml, not direct values
Workload Identity:
# Check service account annotations
kubectl get sa akv-sync-sa -n akv-sync -o yaml
# Check federated credential
az identity federated-credential list \
--identity-name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUPService Principal:
# Test SP login manually
az login --service-principal \
--username $SP_APP_ID \
--password $SP_PASSWORD \
--tenant $TENANT_ID- Enable
destination.autoCreate: truein values - Or create destination vault manually
- Check logs for warnings about missing vaults
# Check exclusion patterns
kubectl get configmap -n akv-sync akv-sync-config -o yaml | grep EXCLUDE_SECRETS
# Verify dry-run is disabled
kubectl get configmap -n akv-sync akv-sync-config -o yaml | grep DRY_RUN
# Check RBAC permissions
az keyvault secret list --vault-name $SOURCE_KV- Verify subscription IDs in ConfigMap
- Check role assignments in both subscriptions
- Ensure identity has access to both subscriptions
akv-sync/
├── README.md # This file
├── Dockerfile # Container image
├── akv-sync.sh # Sync script
├── .dockerignore # Docker build exclusions
├── .gitignore # Git exclusions
├── docs/
│ ├── INSTALLATION.md # Detailed installation guide
│ └── ADVANCED.md # Advanced scenarios
│
└── helm-chart/ # Helm Chart
├── Chart.yaml # Chart metadata
├── values.yaml # Configuration options
├── templates/ # Kubernetes manifests
│ ├── _helpers.tpl
│ ├── namespace.yaml
│ ├── serviceaccount.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── cronjob.yaml
│ └── NOTES.txt
└── examples/ # Example configurations
├── same-subscription-workload-identity.yaml
├── cross-subscription-service-principal.yaml
├── single-vault.yaml
├── all-except.yaml
└── multiple-vaults-full-notifications.yaml
- Subscription Context Switching: Minimal overhead (<1 second per switch)
- Service Principal Login: Initial login adds 1-2 seconds to job startup
- Network Latency: Depends on regions
- API Rate Limits: Apply per subscription
Consider sync frequency based on RPO requirements:
- Every 5 minutes = ~8,640 syncs/month
- Every 30 minutes = ~1,440 syncs/month
This Helm chart is provided as-is for use in your Azure environment.
For issues and questions:
- Check logs:
kubectl logs -n akv-sync <pod> - Verify configuration:
helm get values akv-sync -n akv-sync - Review documentation in
docs/directory - Check Azure permissions and audit logs