Set up External Secrets Operator to sync secrets from HashiCorp Vault and AWS Secrets Manager into your ArgoCD GitOps workflow, enabling secure automated secret management across multiple environments without storing sensitive data in Git repositories.
Prerequisites
- Kubernetes cluster with kubectl access
- ArgoCD installed
- HashiCorp Vault or AWS Secrets Manager access
- Helm 3.x installed
What this solves
Managing secrets in GitOps workflows presents a security challenge: you need secrets in Kubernetes but can't store them in Git repositories. The External Secrets Operator (ESO) bridges this gap by automatically synchronizing secrets from external systems like HashiCorp Vault and AWS Secrets Manager into your cluster. This tutorial shows you how to integrate ESO with ArgoCD for secure, automated secret management across multiple environments.
Prerequisites
You'll need a working Kubernetes cluster with ArgoCD installed and either HashiCorp Vault or AWS Secrets Manager access. If you haven't set up ArgoCD yet, check our ArgoCD installation guide.
Step-by-step installation
Install External Secrets Operator
Deploy the External Secrets Operator using Helm, which provides the most flexible installation method.
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
Verify operator installation
Check that all ESO components are running correctly before proceeding.
kubectl get pods -n external-secrets-system
kubectl get crd | grep external-secrets
Create HashiCorp Vault SecretStore
Configure a SecretStore that connects to your HashiCorp Vault instance. This example uses Kubernetes authentication.
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: argocd
spec:
provider:
vault:
server: "https://vault.example.com:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets-sa"
Create AWS Secrets Manager SecretStore
Alternative SecretStore configuration for AWS Secrets Manager using IAM roles.
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-backend
namespace: argocd
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
Set up service account and RBAC
Create the service account and necessary permissions for External Secrets Operator.
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-secrets-sa
namespace: argocd
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/ExternalSecretsRole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: external-secrets-role
namespace: argocd
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "update", "get", "list", "watch"]
Apply the configurations
Deploy all the External Secrets Operator configurations to your cluster.
kubectl apply -f rbac.yaml
kubectl apply -f vault-secret-store.yaml
OR if using AWS
kubectl apply -f aws-secret-store.yaml
Step-by-step secret synchronization
Create an ExternalSecret resource
Define which secrets to sync from your external provider into Kubernetes.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: argocd-repo-creds
namespace: argocd
spec:
refreshInterval: 30s
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: argocd-repo-server-tls-certs-secret
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: argocd/repo-credentials
property: username
- secretKey: password
remoteRef:
key: argocd/repo-credentials
property: password
Configure ArgoCD repository credentials
Create an ExternalSecret that manages ArgoCD's private repository credentials.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: private-repo-creds
namespace: argocd
spec:
refreshInterval: 60s
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: private-repo
creationPolicy: Owner
template:
type: Opaque
metadata:
labels:
argocd.argoproj.io/secret-type: repository
data:
type: git
url: https://github.com/example/private-repo
username: "{{ .username }}"
password: "{{ .password }}"
data:
- secretKey: username
remoteRef:
key: github/credentials
property: username
- secretKey: password
remoteRef:
key: github/credentials
property: token
Set up application secrets
Create ExternalSecrets for application-specific secrets that ArgoCD will deploy.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 300s
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: postgres-credentials
creationPolicy: Owner
data:
- secretKey: POSTGRES_USER
remoteRef:
key: database/production
property: username
- secretKey: POSTGRES_PASSWORD
remoteRef:
key: database/production
property: password
- secretKey: POSTGRES_DB
remoteRef:
key: database/production
property: database
Apply secret configurations
Deploy the ExternalSecret resources to start automatic synchronization.
kubectl apply -f external-secret.yaml
kubectl apply -f argocd-repo-secret.yaml
kubectl apply -f app-secrets.yaml
ArgoCD GitOps integration
Create ArgoCD Application with External Secrets
Configure ArgoCD to manage ExternalSecret resources as part of your GitOps workflow.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets-config
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example/k8s-config
targetRevision: HEAD
path: external-secrets/
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Configure sync waves for proper ordering
Use ArgoCD sync waves to ensure ExternalSecrets are created before applications that depend on them.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
namespace: production
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
refreshInterval: 300s
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-config-secret
creationPolicy: Owner
data:
- secretKey: api-key
remoteRef:
key: application/config
property: api-key
Deploy application with dependency
Create an application deployment that uses the synchronized secrets.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: nginx:1.21
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-config-secret
key: api-key
- name: DB_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: POSTGRES_USER
Multi-environment configuration
Create ClusterSecretStore for shared access
Use ClusterSecretStore when multiple namespaces need access to the same secret backend.
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-cluster-backend
spec:
provider:
vault:
server: "https://vault.example.com:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets-cluster"
serviceAccountRef:
name: "external-secrets-sa"
namespace: "external-secrets-system"
Configure environment-specific secrets
Create ExternalSecrets that pull different values based on environment using path templating.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: env-database-creds
namespace: staging
spec:
refreshInterval: 300s
secretStoreRef:
name: vault-cluster-backend
kind: ClusterSecretStore
target:
name: database-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: database/staging/postgres
property: username
- secretKey: password
remoteRef:
key: database/staging/postgres
property: password
Apply multi-environment configuration
Deploy the cluster-wide and environment-specific configurations.
kubectl apply -f cluster-secret-store.yaml
kubectl apply -f env-specific-secret.yaml
Verify your setup
Test that External Secrets Operator is properly synchronizing secrets and ArgoCD can access them.
# Check ESO operator status
kubectl get pods -n external-secrets-system
Verify SecretStore connection
kubectl describe secretstore vault-backend -n argocd
Check ExternalSecret synchronization
kubectl get externalsecrets -n argocd
kubectl describe externalsecret argocd-repo-creds -n argocd
Verify secrets were created
kubectl get secrets -n argocd | grep argocd-repo
kubectl get secrets -n production | grep postgres-credentials
Check ArgoCD can access private repositories
kubectl get applications -n argocd
kubectl describe application external-secrets-config -n argocd
kubectl get secret -o yaml to view secret contents in production logs or CI/CD output, as this exposes sensitive data in plaintext.Advanced configuration
Configure secret rotation and refresh
Set up automatic secret rotation with shorter refresh intervals for critical secrets.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rotating-api-key
namespace: production
spec:
refreshInterval: 30s
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: api-credentials
creationPolicy: Owner
deletionPolicy: Retain
data:
- secretKey: api-key
remoteRef:
key: rotating/api-credentials
property: current-key
Set up monitoring and alerts
Configure monitoring for External Secrets Operator to track synchronization health.
apiVersion: v1
kind: ServiceMonitor
metadata:
name: external-secrets-metrics
namespace: external-secrets-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: external-secrets
endpoints:
- port: metrics
interval: 30s
path: /metrics
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| ExternalSecret stuck in "SecretSyncError" | Invalid credentials or vault path | Check SecretStore configuration and vault permissions |
| ArgoCD can't access private repository | Repository secret not properly labeled | Add argocd.argoproj.io/secret-type: repository label |
| Secrets not refreshing | Service account lacks permissions | Verify RBAC configuration and vault policies |
| Application pods failing with secret mount errors | Secret not synchronized before pod creation | Use ArgoCD sync waves to order resource creation |
| ClusterSecretStore connection failing | Service account in wrong namespace | Ensure service account exists in external-secrets-system namespace |
Security best practices
Follow these security guidelines when integrating External Secrets Operator with ArgoCD for production use.
- Configure vault policies to restrict access to specific secret paths per environment
- Use different service accounts for different environments and applications
- Set appropriate refresh intervals based on secret sensitivity
- Monitor External Secrets Operator logs for failed authentication attempts
- Regularly rotate service account tokens and vault authentication credentials
Next steps
- Advanced Vault integration with Kubernetes
- ArgoCD ApplicationSets for multi-environment GitOps workflows
- Integrate ArgoCD with SonarQube for deployment validation
- Implement secrets backup and disaster recovery strategies
- Configure External Secrets Operator with Azure Key Vault
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
NAMESPACE="${NAMESPACE:-argocd}"
SECRET_BACKEND="${SECRET_BACKEND:-vault}"
VAULT_URL="${VAULT_URL:-}"
AWS_REGION="${AWS_REGION:-us-east-1}"
AWS_ROLE_ARN="${AWS_ROLE_ARN:-}"
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR]${NC} Installation failed. Rolling back..."
kubectl delete namespace external-secrets-system --ignore-not-found=true
kubectl delete -f /tmp/eso-rbac.yaml --ignore-not-found=true 2>/dev/null || true
kubectl delete -f /tmp/eso-secretstore.yaml --ignore-not-found=true 2>/dev/null || true
rm -f /tmp/eso-*.yaml
exit 1
}
trap cleanup ERR
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Install External Secrets Operator and integrate with ArgoCD"
echo ""
echo "Options:"
echo " -b, --backend BACKEND Secret backend (vault|aws) [default: vault]"
echo " -n, --namespace NS ArgoCD namespace [default: argocd]"
echo " -v, --vault-url URL Vault server URL (required for vault backend)"
echo " -r, --aws-region REGION AWS region [default: us-east-1]"
echo " -a, --aws-role ARN AWS IAM role ARN (required for aws backend)"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 --backend vault --vault-url https://vault.example.com:8200"
echo " $0 --backend aws --aws-region us-west-2 --aws-role arn:aws:iam::123456789012:role/ExternalSecretsRole"
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-b|--backend)
SECRET_BACKEND="$2"
shift 2
;;
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-v|--vault-url)
VAULT_URL="$2"
shift 2
;;
-r|--aws-region)
AWS_REGION="$2"
shift 2
;;
-a|--aws-role)
AWS_ROLE_ARN="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
exit 1
;;
esac
done
# Validate arguments
if [[ "$SECRET_BACKEND" == "vault" && -z "$VAULT_URL" ]]; then
echo -e "${RED}Error: Vault URL is required when using vault backend${NC}"
usage
exit 1
fi
if [[ "$SECRET_BACKEND" == "aws" && -z "$AWS_ROLE_ARN" ]]; then
echo -e "${RED}Error: AWS IAM role ARN is required when using aws backend${NC}"
usage
exit 1
fi
if [[ "$SECRET_BACKEND" != "vault" && "$SECRET_BACKEND" != "aws" ]]; then
echo -e "${RED}Error: Backend must be 'vault' or 'aws'${NC}"
usage
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian) PKG_MGR="apt"; PKG_INSTALL="apt install -y"; PKG_UPDATE="apt update" ;;
almalinux|rocky|centos|rhel|ol|fedora) PKG_MGR="dnf"; PKG_INSTALL="dnf install -y"; PKG_UPDATE="dnf check-update || true" ;;
amzn) PKG_MGR="yum"; PKG_INSTALL="yum install -y"; PKG_UPDATE="yum check-update || true" ;;
*) echo -e "${RED}Unsupported distro: $ID${NC}"; exit 1 ;;
esac
else
echo -e "${RED}Cannot detect OS distribution${NC}"
exit 1
fi
echo -e "${BLUE}ArgoCD External Secrets Operator Integration${NC}"
echo -e "${BLUE}==========================================${NC}"
# Check if running as root or with sudo
if [[ $EUID -eq 0 ]]; then
SUDO=""
elif command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
echo -e "${YELLOW}[INFO]${NC} Running with sudo privileges"
else
echo -e "${RED}[ERROR]${NC} This script requires root privileges or sudo"
exit 1
fi
echo -e "${GREEN}[1/8]${NC} Checking prerequisites..."
# Check for required tools
for tool in kubectl helm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo -e "${YELLOW}[INFO]${NC} Installing $tool..."
if [[ "$tool" == "kubectl" ]]; then
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
$SUDO install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
elif [[ "$tool" == "helm" ]]; then
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
$SUDO ./get_helm.sh
rm get_helm.sh
fi
fi
done
# Verify kubectl connectivity
if ! kubectl cluster-info >/dev/null 2>&1; then
echo -e "${RED}[ERROR]${NC} Cannot connect to Kubernetes cluster"
exit 1
fi
# Check if ArgoCD namespace exists
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
echo -e "${RED}[ERROR]${NC} ArgoCD namespace '$NAMESPACE' not found"
echo -e "${YELLOW}[INFO]${NC} Please install ArgoCD first or specify correct namespace with --namespace"
exit 1
fi
echo -e "${GREEN}[2/8]${NC} Adding External Secrets Operator Helm repository..."
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
echo -e "${GREEN}[3/8]${NC} Installing External Secrets Operator..."
helm install external-secrets external-secrets/external-secrets \
-n external-secrets-system \
--create-namespace \
--wait \
--timeout=300s
echo -e "${GREEN}[4/8]${NC} Verifying External Secrets Operator installation..."
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=external-secrets -n external-secrets-system --timeout=300s
# Verify CRDs
kubectl get crd | grep external-secrets || {
echo -e "${RED}[ERROR]${NC} External Secrets CRDs not found"
exit 1
}
echo -e "${GREEN}[5/8]${NC} Creating service account and RBAC..."
# Create RBAC configuration
cat > /tmp/eso-rbac.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-secrets-sa
namespace: $NAMESPACE
$(if [[ "$SECRET_BACKEND" == "aws" ]]; then
echo " annotations:"
echo " eks.amazonaws.com/role-arn: $AWS_ROLE_ARN"
fi)
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: external-secrets-role
namespace: $NAMESPACE
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "update", "get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: external-secrets-rolebinding
namespace: $NAMESPACE
subjects:
- kind: ServiceAccount
name: external-secrets-sa
namespace: $NAMESPACE
roleRef:
kind: Role
name: external-secrets-role
apiGroup: rbac.authorization.k8s.io
EOF
kubectl apply -f /tmp/eso-rbac.yaml
echo -e "${GREEN}[6/8]${NC} Creating SecretStore for $SECRET_BACKEND..."
# Create SecretStore configuration
if [[ "$SECRET_BACKEND" == "vault" ]]; then
cat > /tmp/eso-secretstore.yaml << EOF
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: $NAMESPACE
spec:
provider:
vault:
server: "$VAULT_URL"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets-sa"
EOF
elif [[ "$SECRET_BACKEND" == "aws" ]]; then
cat > /tmp/eso-secretstore.yaml << EOF
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-backend
namespace: $NAMESPACE
spec:
provider:
aws:
service: SecretsManager
region: $AWS_REGION
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
EOF
fi
kubectl apply -f /tmp/eso-secretstore.yaml
echo -e "${GREEN}[7/8]${NC} Creating example ExternalSecret..."
# Create example ExternalSecret
cat > /tmp/eso-externalsecret.yaml << EOF
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: argocd-repo-creds-example
namespace: $NAMESPACE
spec:
refreshInterval: 60s
secretStoreRef:
name: $(if [[ "$SECRET_BACKEND" == "vault" ]]; then echo "vault-backend"; else echo "aws-backend"; fi)
kind: SecretStore
target:
name: argocd-repo-creds
creationPolicy: Owner
template:
type: Opaque
metadata:
labels:
argocd.argoproj.io/secret-type: repository
data:
- secretKey: username
remoteRef:
key: $(if [[ "$SECRET_BACKEND" == "vault" ]]; then echo "argocd/repo-credentials"; else echo "argocd-repo-credentials"; fi)
property: username
- secretKey: password
remoteRef:
key: $(if [[ "$SECRET_BACKEND" == "vault" ]]; then echo "argocd/repo-credentials"; else echo "argocd-repo-credentials"; fi)
property: password
EOF
kubectl apply -f /tmp/eso-externalsecret.yaml
echo -e "${GREEN}[8/8]${NC} Verifying installation..."
# Wait for SecretStore to be ready
sleep 10
# Check SecretStore status
STORE_NAME=$(if [[ "$SECRET_BACKEND" == "vault" ]]; then echo "vault-backend"; else echo "aws-backend"; fi)
if kubectl get secretstore "$STORE_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo -e "${GREEN}✓${NC} SecretStore '$STORE_NAME' created successfully"
else
echo -e "${RED}✗${NC} SecretStore '$STORE_NAME' creation failed"
fi
# Check ExternalSecret status
if kubectl get externalsecret argocd-repo-creds-example -n "$NAMESPACE" >/dev/null 2>&1; then
echo -e "${GREEN}✓${NC} ExternalSecret 'argocd-repo-creds-example' created successfully"
else
echo -e "${RED}✗${NC} ExternalSecret 'argocd-repo-creds-example' creation failed"
fi
# Cleanup temporary files
rm -f /tmp/eso-*.yaml
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo "1. Configure your $SECRET_BACKEND with the required secrets"
if [[ "$SECRET_BACKEND" == "vault" ]]; then
echo "2. Set up Vault Kubernetes authentication for the 'external-secrets' role"
echo "3. Store secrets in Vault at path: secret/argocd/repo-credentials"
else
echo "2. Configure AWS IAM role '$AWS_ROLE_ARN' with SecretsManager permissions"
echo "3. Create secrets in AWS Secrets Manager named 'argocd-repo-credentials'"
fi
echo "4. Update ExternalSecret resources to match your secret paths"
echo "5. Monitor secret synchronization: kubectl get externalsecrets -n $NAMESPACE"
Review the script before running. Execute with: bash install.sh