How to Use Reloader with AWS Secrets Manager and External Secrets Operator#
This guide explains how to sync secrets from AWS Secrets Manager into Kubernetes using External Secrets Operator (ESO), and use Stakater Reloader to automatically restart pods when those secrets change.
Authentication Options#
ESO supports two authentication methods for AWS Secrets Manager:
| Method | Description | Use Case |
|---|---|---|
| IRSA | IAM role bound to a Kubernetes ServiceAccount via OIDC | Recommended for EKS — no static credentials |
| Static Credentials | AWS access key and secret stored in a K8s Secret | Any cluster — simpler but requires credential rotation |
How It Works#
sequenceDiagram
actor Ops as Operator / Rotation Job
participant SM as AWS Secrets Manager
participant STS as AWS STS
participant ESO as External Secrets Operator
participant K8s as Kubernetes Secret
participant RL as Reloader
participant Pod as Application Pod
Note over ESO: Authenticates via IRSA or static credentials
ESO->>STS: AssumeRoleWithWebIdentity (IRSA)
STS-->>ESO: Temporary credentials
Ops->>SM: Rotate secret
loop Every refreshInterval
ESO->>SM: GetSecretValue
SM-->>ESO: Updated secret value
end
ESO->>K8s: Update Secret data
Note over K8s: annotation: reloader.stakater.com/match: "true"
K8s-->>RL: Watch event (Secret changed)
RL->>Pod: Trigger rolling restart
Note over Pod: New pod starts with updated secret
Prerequisites#
- Kubernetes cluster (v1.19+)
- Helm v3+
- AWS account with Secrets Manager access
- AWS CLI configured locally
- Stakater Reloader installed
- External Secrets Operator installed
Install Stakater Reloader#
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader --namespace reloader --create-namespace
Install External Secrets Operator#
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace \
--wait
Verify:
kubectl get pods -n external-secrets
Create a secret in AWS Secrets Manager#
aws secretsmanager create-secret \
--name myapp/database \
--region us-east-1 \
--secret-string '{"username":"admin-user","password":"super-secret-password"}'
Option 1: IRSA (IAM Roles for Service Accounts)#
IRSA lets a Kubernetes ServiceAccount assume an IAM role without static credentials. It uses the EKS OIDC provider to exchange ServiceAccount tokens for temporary AWS credentials.
IRSA: Step 1 — Enable the OIDC provider for your EKS cluster#
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--region us-east-1 \
--approve
If you don't have eksctl, use the AWS CLI:
# Get the OIDC issuer URL
OIDC_URL=$(aws eks describe-cluster \
--name my-cluster \
--region us-east-1 \
--query "cluster.identity.oidc.issuer" \
--output text)
# Create the OIDC provider
aws iam create-open-id-connect-provider \
--url "$OIDC_URL" \
--client-id-list sts.amazonaws.com \
--thumbprint-list "$(openssl s_client -servername $(echo $OIDC_URL | cut -d/ -f3) \
-showcerts -connect $(echo $OIDC_URL | cut -d/ -f3):443 </dev/null 2>/dev/null \
| openssl x509 -fingerprint -noout | cut -d= -f2 | tr -d :)"
IRSA: Step 2 — Create an IAM policy for Secrets Manager access#
cat > eso-secrets-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:us-east-1:*:secret:myapp/*"
}
]
}
EOF
aws iam create-policy \
--policy-name ESOSecretsManagerPolicy \
--policy-document file://eso-secrets-policy.json
Note the policy ARN from the output — you need it in the next step.
IRSA: Step 3 — Create an IAM role with the OIDC trust policy#
# Get your AWS account ID and OIDC issuer
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
OIDC_ISSUER=$(aws eks describe-cluster \
--name my-cluster \
--region us-east-1 \
--query "cluster.identity.oidc.issuer" \
--output text | sed 's|https://||')
cat > eso-trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_ISSUER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_ISSUER}:sub": "system:serviceaccount:aws-eso-test:eso-sa",
"${OIDC_ISSUER}:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF
aws iam create-role \
--role-name ESOSecretsManagerRole \
--assume-role-policy-document file://eso-trust-policy.json
aws iam attach-role-policy \
--role-name ESOSecretsManagerRole \
--policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/ESOSecretsManagerPolicy"
IRSA: Step 4 — Create namespace and annotated ServiceAccount#
kubectl create namespace aws-eso-test
ROLE_ARN=$(aws iam get-role --role-name ESOSecretsManagerRole --query Role.Arn --output text)
kubectl create serviceaccount eso-sa -n aws-eso-test \
--dry-run=client -o yaml | kubectl annotate --local -f - \
"eks.amazonaws.com/role-arn=${ROLE_ARN}" --dry-run=client -o yaml | kubectl apply -f -
Or create the ServiceAccount manifest directly:
apiVersion: v1
kind: ServiceAccount
metadata:
name: eso-sa
namespace: aws-eso-test
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::<ACCOUNT_ID>:role/ESOSecretsManagerRole"
IRSA: Step 5 — Create SecretStore#
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secret-store
namespace: aws-eso-test
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-sa
Apply and verify:
kubectl apply -f secret-store.yaml
kubectl get secretstore -n aws-eso-test
Should show STATUS: Valid and READY: True.
Now proceed to Create ExternalSecret.
Option 2: Static Credentials#
Use an AWS access key and secret stored in a Kubernetes Secret. Works with any Kubernetes cluster, not just EKS.
Static: Step 1 — Create an IAM user and access key#
aws iam create-user --user-name eso-secrets-user
cat > eso-secrets-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:us-east-1:*:secret:myapp/*"
}
]
}
EOF
aws iam put-user-policy \
--user-name eso-secrets-user \
--policy-name SecretsManagerAccess \
--policy-document file://eso-secrets-policy.json
aws iam create-access-key --user-name eso-secrets-user
Save the AccessKeyId and SecretAccessKey from the output.
Static: Step 2 — Store credentials in a Kubernetes Secret#
kubectl create namespace aws-eso-test
kubectl create secret generic aws-credentials -n aws-eso-test \
--from-literal=access-key-id="<ACCESS_KEY_ID>" \
--from-literal=secret-access-key="<SECRET_ACCESS_KEY>"
Static: Step 3 — Create SecretStore#
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secret-store
namespace: aws-eso-test
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-credentials
key: access-key-id
secretAccessKeySecretRef:
name: aws-credentials
key: secret-access-key
Apply and verify:
kubectl apply -f secret-store.yaml
kubectl get secretstore -n aws-eso-test
Should show STATUS: Valid and READY: True.
Now proceed to Create ExternalSecret.
Common Configuration#
The following steps apply to both authentication methods.
Create ExternalSecret#
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: aws-eso-test
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secret-store
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
template:
metadata:
annotations:
reloader.stakater.com/match: "true"
data:
- secretKey: username
remoteRef:
key: myapp/database
property: username
- secretKey: password
remoteRef:
key: myapp/database
property: password
Apply and verify:
kubectl apply -f external-secret.yaml
kubectl get externalsecret -n aws-eso-test
Should show STATUS: SecretSynced and READY: True.
Deploy Application#
apiVersion: apps/v1
kind: Deployment
metadata:
name: aws-eso-test-app
namespace: aws-eso-test
annotations:
reloader.stakater.com/search: "true"
spec:
replicas: 1
selector:
matchLabels:
app: aws-eso-test-app
template:
metadata:
labels:
app: aws-eso-test-app
spec:
containers:
- name: app
image: busybox:latest
command:
- "sh"
- "-c"
- |
while true; do
echo "Username: $APP_USERNAME"
echo "Password: $APP_PASSWORD"
sleep 30
done
env:
- name: APP_USERNAME
valueFrom:
secretKeyRef:
name: app-secrets
key: username
- name: APP_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: password
Apply:
kubectl apply -f deployment.yaml
Verify the Setup#
# SecretStore
kubectl get secretstore -n aws-eso-test
# ExternalSecret
kubectl get externalsecret -n aws-eso-test
# Secret (created by ESO)
kubectl get secret app-secrets -n aws-eso-test
# Pod
kubectl get pods -n aws-eso-test
# Verify secret contents
kubectl get secret app-secrets -n aws-eso-test -o jsonpath='{.data.password}' | base64 -d
# Check app logs
kubectl logs -n aws-eso-test -l app=aws-eso-test-app
Test Secret Rotation#
Update the secret in AWS Secrets Manager#
aws secretsmanager put-secret-value \
--secret-id myapp/database \
--region us-east-1 \
--secret-string '{"username":"admin-user","password":"new-rotated-password"}'
Wait and verify#
ESO syncs on refreshInterval (1h by default). To test immediately, force a sync:
kubectl annotate externalsecret app-secrets -n aws-eso-test \
force-sync=$(date +%s) --overwrite
Then verify:
# Check secret was updated
kubectl get secret app-secrets -n aws-eso-test -o jsonpath='{.data.password}' | base64 -d
# Check pod was restarted (new pod name, age resets)
kubectl get pods -n aws-eso-test -l app=aws-eso-test-app
# Check app logs show new password
kubectl logs -n aws-eso-test -l app=aws-eso-test-app --tail=5
Configuration Reference#
SecretStore — IRSA#
| Field | Description |
|---|---|
provider.aws.service |
SecretsManager |
provider.aws.region |
AWS region where the secret lives |
provider.aws.auth.jwt.serviceAccountRef.name |
ServiceAccount annotated with the IAM role ARN |
SecretStore — Static Credentials#
| Field | Description |
|---|---|
provider.aws.service |
SecretsManager |
provider.aws.region |
AWS region where the secret lives |
provider.aws.auth.secretRef.accessKeyIDSecretRef |
K8s Secret and key containing the AWS access key ID |
provider.aws.auth.secretRef.secretAccessKeySecretRef |
K8s Secret and key containing the AWS secret access key |
ExternalSecret#
| Field | Description |
|---|---|
refreshInterval |
How often ESO polls AWS for changes (e.g. 1h, 5m) |
secretStoreRef |
Reference to the SecretStore |
target.name |
Name of the K8s Secret to create |
target.template.metadata.annotations |
Annotations to add to the created Secret — add reloader.stakater.com/match: "true" here |
data[].secretKey |
Key name in the K8s Secret |
data[].remoteRef.key |
AWS Secrets Manager secret name or ARN |
data[].remoteRef.property |
Key within a JSON-formatted secret |
Reloader Annotations#
| Resource | Annotation |
|---|---|
| Deployment | reloader.stakater.com/search: "true" |
| Secret (via ExternalSecret template) | reloader.stakater.com/match: "true" |
Comparison: IRSA vs Static Credentials#
| Aspect | IRSA | Static Credentials |
|---|---|---|
| Credentials | Temporary STS tokens via OIDC | Static access key + secret |
| Security | No secrets to rotate, tokens auto-expire | Credentials must be manually rotated |
| Cluster requirement | EKS with OIDC provider enabled | Any Kubernetes cluster |
| Setup complexity | Requires IAM role + OIDC trust policy | Just an IAM user + K8s Secret |
| Best for | Production on EKS | Development, non-EKS clusters |
Notes on refreshInterval#
AWS Secrets Manager charges per API call. A short refreshInterval (e.g. 1m) on many ExternalSecret resources across many namespaces can generate significant cost. For most use cases, 1h is a good default. If you need faster propagation, consider using AWS Secrets Manager rotation with Lambda and a shorter interval only on critical secrets.