External Secret Managers
External secret managers provide centralized, auditable secret storage with automated rotation and fine-grained access control. This guide shows how to integrate Expanso Edge with popular enterprise secret management systems.
:::info Assumption This guide assumes your secret manager (Vault, AWS Secrets Manager, etc.) is already provisioned and configured. Examples focus on fetching and using secrets, not on secret manager setup. :::
Why Use External Secret Managers?
Benefits
- Automated Rotation: Credentials expire and renew without manual intervention
- Audit Trails: Track who accessed which secrets and when
- Centralized Management: Single source of truth for all credentials
- Dynamic Secrets: Generate short-lived credentials on demand
- Access Control: Fine-grained policies per service/user
- Compliance: Meet SOC 2, HIPAA, PCI-DSS requirements
When to Use
- Production deployments with compliance requirements
- Multi-environment setups (dev/staging/prod)
- Large-scale deployments (10+ edge nodes)
- Regulated industries (finance, healthcare)
- Environments requiring secret rotation
Integration Patterns
All external secret managers follow a similar pattern:
- Fetch secrets at edge agent startup or runtime
- Inject as environment variables or write to local files
- Reference in pipeline config using interpolation (
${VAR_NAME})
HashiCorp Vault
Architecture
┌─────────────────┐
│ Vault Server │
│ (Pre-existing) │
└────────┬────────┘
│ Vault Agent (sidecar)
│ fetches secrets
↓
┌─────────────────┐
│ Edge Agent │
│ (reads from │
│ env vars) │
└─────────────────┘
Using Vault Agent Sidecar
Vault Agent runs alongside the edge agent, automatically fetching and refreshing secrets.
Vault Agent config:
pid_file = "/tmp/vault-agent.pid"
vault {
address = "https://vault.example.com:8200"
}
auto_auth {
method {
type = "kubernetes" # or "aws", "gcp", "azure"
config = {
role = "expanso-edge"
}
}
sink {
type = "file"
config = {
path = "/etc/expanso/secrets.env"
mode = 0600
}
}
}
template {
source = "/etc/vault/templates/pipeline.env.tpl"
destination = "/etc/expanso/pipeline.env"
perms = "0600"
}
Template file:
{{ with secret "secret/data/expanso/pipeline" }}
KAFKA_USERNAME={{ .Data.data.kafka_username }}
KAFKA_PASSWORD={{ .Data.data.kafka_password }}
API_TOKEN={{ .Data.data.api_token }}
DB_PASSWORD={{ .Data.data.db_password }}
{{ end }}
Docker Compose example:
version: '3.8'
services:
vault-agent:
image: hashicorp/vault:latest
command: agent -config=/vault/config/vault-agent.hcl
volumes:
- ./vault-agent.hcl:/vault/config/vault-agent.hcl:ro
- ./pipeline.env.tpl:/etc/vault/templates/pipeline.env.tpl:ro
- secrets:/etc/expanso
environment:
VAULT_ADDR: https://vault.example.com:8200
expanso-edge:
image: ghcr.io/expanso-io/expanso-edge:latest
command: run --config /etc/expanso/pipeline.yaml
volumes:
- ./pipeline.yaml:/etc/expanso/pipeline.yaml:ro
- secrets:/etc/expanso
depends_on:
- vault-agent
# Reload env vars on change
env_file:
- /etc/expanso/pipeline.env
volumes:
secrets:
Using Vault Init Container (Kubernetes)
Kubernetes example:
Create the ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: expanso-edge
namespace: expanso-system
Create the Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: expanso-edge
namespace: expanso-system
spec:
replicas: 1
selector:
matchLabels:
app: expanso-edge
template:
metadata:
labels:
app: expanso-edge
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-pipeline.env: "secret/data/expanso/pipeline"
vault.hashicorp.com/agent-inject-template-pipeline.env: |
{{- with secret "secret/data/expanso/pipeline" -}}
export KAFKA_USERNAME="{{ .Data.data.kafka_username }}"
export KAFKA_PASSWORD="{{ .Data.data.kafka_password }}"
export API_TOKEN="{{ .Data.data.api_token }}"
{{- end }}
vault.hashicorp.com/role: "expanso-edge"
spec:
serviceAccountName: expanso-edge
containers:
- name: expanso-edge
image: ghcr.io/expanso-io/expanso-edge:latest
command:
- /bin/sh
- -c
- |
source /vault/secrets/pipeline.env
expanso-edge run --config /etc/expanso/pipeline.yaml
volumeMounts:
- name: pipeline-config
mountPath: /etc/expanso
readOnly: true
volumes:
- name: pipeline-config
configMap:
name: expanso-pipeline
AWS Secrets Manager
Architecture
┌────────────────────┐
│ AWS Secrets Mgr │
│ (Pre-existing) │
└──────────┬─────────┘
│ IAM Role + SDK/CLI
│ fetches secrets
↓
┌──────────────┐
│ Edge Agent │
│ (uses SDK) │
└──────────────┘
Using Init Script
Fetch secrets at startup and inject as environment variables.
Startup script:
#!/bin/bash
set -e
# Fetch secrets from AWS Secrets Manager
SECRET_JSON=$(aws secretsmanager get-secret-value \
--secret-id expanso/pipeline/credentials \
--region us-east-1 \
--query SecretString \
--output text)
# Parse JSON and export environment variables
export KAFKA_USERNAME=$(echo $SECRET_JSON | jq -r '.kafka_username')
export KAFKA_PASSWORD=$(echo $SECRET_JSON | jq -r '.kafka_password')
export API_TOKEN=$(echo $SECRET_JSON | jq -r '.api_token')
export DB_PASSWORD=$(echo $SECRET_JSON | jq -r '.db_password')
# Run edge agent with injected secrets
exec expanso-edge run --config /etc/expanso/pipeline.yaml
Docker example:
FROM ghcr.io/expanso-io/expanso-edge:latest
# Install AWS CLI and jq
RUN apt-get update && apt-get install -y awscli jq && rm -rf /var/lib/apt/lists/*
# Copy startup script
COPY fetch-secrets.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/fetch-secrets.sh
ENTRYPOINT ["/usr/local/bin/fetch-secrets.sh"]
Run with IAM role:
docker run -d \
--name expanso-edge \
-v ~/.aws:/root/.aws:ro \
-v $(pwd)/pipeline.yaml:/etc/expanso/pipeline.yaml:ro \
expanso-edge-aws:latest
Using Kubernetes External Secrets Operator
Install External Secrets Operator:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets-system \
--create-namespace
Configure AWS SecretStore:
Create the SecretStore:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: expanso-system
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: expanso-edge
Create the ExternalSecret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: pipeline-credentials
namespace: expanso-system
spec:
refreshInterval: 1h # Refresh every hour
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: pipeline-credentials
creationPolicy: Owner
data:
- secretKey: KAFKA_USERNAME
remoteRef:
key: expanso/pipeline/credentials
property: kafka_username
- secretKey: KAFKA_PASSWORD
remoteRef:
key: expanso/pipeline/credentials
property: kafka_password
- secretKey: API_TOKEN
remoteRef:
key: expanso/pipeline/credentials
property: api_token
The operator creates a Kubernetes Secret named pipeline-credentials that auto-syncs from AWS Secrets Manager.
Google Secret Manager
Using Init Container Pattern
Kubernetes example:
Create the ServiceAccount with GCP workload identity:
apiVersion: v1
kind: ServiceAccount
metadata:
name: expanso-edge
namespace: expanso-system
annotations:
Create the Deployment with init container:
apiVersion: apps/v1
kind: Deployment
metadata:
name: expanso-edge
namespace: expanso-system
spec:
template:
spec:
serviceAccountName: expanso-edge
initContainers:
- name: fetch-secrets
image: google/cloud-sdk:slim
command:
- /bin/bash
- -c
- |
# Fetch secrets from Google Secret Manager
gcloud secrets versions access latest \
--secret=expanso-pipeline-credentials \
--format=json > /secrets/credentials.json
# Parse and write environment file
cat /secrets/credentials.json | jq -r '
"KAFKA_USERNAME=\(.kafka_username)\n" +
"KAFKA_PASSWORD=\(.kafka_password)\n" +
"API_TOKEN=\(.api_token)"
' > /secrets/pipeline.env
chmod 600 /secrets/pipeline.env
volumeMounts:
- name: secrets
mountPath: /secrets
containers:
- name: expanso-edge
image: ghcr.io/expanso-io/expanso-edge:latest
command:
- /bin/sh
- -c
- |
set -a
source /secrets/pipeline.env
set +a
expanso-edge run --config /etc/expanso/pipeline.yaml
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: pipeline-config
mountPath: /etc/expanso
readOnly: true
volumes:
- name: secrets
emptyDir:
medium: Memory # Use tmpfs for security
- name: pipeline-config
configMap:
name: expanso-pipeline
Azure Key Vault
Using CSI Secret Store Driver
Install Secrets Store CSI Driver:
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system
Install Azure Key Vault provider:
helm repo add csi-secrets-store-provider-azure https://azure.github.io/secrets-store-csi-driver-provider-azure/charts
helm install azure-provider csi-secrets-store-provider-azure/csi-secrets-store-provider-azure \
--namespace kube-system
Configure SecretProviderClass:
Create the SecretProviderClass:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: expanso-azure-keyvault
namespace: expanso-system
spec:
provider: azure
parameters:
usePodIdentity: "true"
keyvaultName: "expanso-secrets"
cloudName: "AzurePublicCloud"
objects: |
array:
- |
objectName: kafka-username
objectType: secret
- |
objectName: kafka-password
objectType: secret
- |
objectName: api-token
objectType: secret
tenantId: "your-tenant-id"
secretObjects:
- secretName: pipeline-credentials
type: Opaque
data:
- objectName: kafka-username
key: KAFKA_USERNAME
- objectName: kafka-password
key: KAFKA_PASSWORD
- objectName: api-token
key: API_TOKEN
Create the Deployment using the secret:
apiVersion: apps/v1
kind: Deployment
metadata:
name: expanso-edge
namespace: expanso-system
spec:
template:
spec:
containers:
- name: expanso-edge
image: ghcr.io/expanso-io/expanso-edge:latest
envFrom:
- secretRef:
name: pipeline-credentials
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "expanso-azure-keyvault"
Best Practices
1. Use Short-Lived Credentials
Configure secrets to expire and auto-rotate:
# Vault: Generate dynamic database credentials
path "database/creds/expanso-readonly" {
capabilities = ["read"]
}
2. Implement Least Privilege
Grant only necessary permissions:
// AWS IAM policy
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:*:*:secret:expanso/pipeline/*"
}]
}
3. Enable Audit Logging
Track all secret access:
# Vault: Enable audit log
vault audit enable file file_path=/vault/logs/audit.log
4. Use Namespaces/Paths
Organize secrets by environment:
vault/
├── expanso/
│ ├── dev/
│ │ └── pipeline
│ ├── staging/
│ │ └── pipeline
│ └── prod/
│ └── pipeline
5. Implement Secret Rotation
Automate credential rotation:
# AWS: Enable automatic rotation
aws secretsmanager rotate-secret \
--secret-id expanso/pipeline/credentials \
--rotation-lambda-arn arn:aws:lambda:region:account:function:rotate-secret \
--rotation-rules AutomaticallyAfterDays=30
6. Monitor Secret Access
Set up alerts for:
- Unauthorized access attempts
- Secrets nearing expiration
- Unusual access patterns
- Failed authentication
Comparison
| Feature | Vault | AWS SM | GCP SM | Azure KV |
|---|---|---|---|---|
| Dynamic Secrets | ✅ | ❌ | ❌ | ❌ |
| Auto-Rotation | ✅ | ✅ | ✅ | ✅ |
| Free Tier | ✅ (OSS) | ❌ | ✅ | ❌ |
| Multi-Cloud | ✅ | ❌ | ❌ | ❌ |
| K8s Integration | ✅ | ✅ (ESO) | ✅ (ESO) | ✅ (CSI) |
| Learning Curve | High | Low | Low | Medium |
ESO = External Secrets Operator CSI = Container Storage Interface
Troubleshooting
Authentication Failures
Symptom: Error: permission denied fetching secret
Solutions:
- Verify IAM roles/policies are attached
- Check service account annotations (Kubernetes)
- Confirm secret paths are correct
- Review audit logs for specific error
Secrets Not Refreshing
Symptom: Old credentials still in use after rotation
Solutions:
- Check refresh interval configuration
- Verify sidecar/init container restarts
- Restart edge agent to pick up new values
- Monitor rotation logs
High Latency
Symptom: Slow startup due to secret fetching
Solutions:
- Use sidecar pattern instead of init containers
- Cache secrets locally with appropriate TTL
- Use regional endpoints (AWS/GCP/Azure)
- Implement retry logic with exponential backoff
Next Steps
- Getting Started? Start with Local Secrets for simpler setups
- Need bootstrap help? See Bootstrapping Secrets
- Production deployment? See Edge Deployment for deployment best practices