Install External Secrets Operator and Sync AWS Secrets Manager into Kubernetes
Install the External Secrets Operator, configure an AWS Secrets Manager SecretStore using IAM Roles for Service Accounts (IRSA), and sync secrets into Kubernetes automatically — so your pods always have the latest credentials without storing anything in Git.
Before you begin
- An EKS cluster with OIDC provider enabled
- kubectl configured with cluster-admin access
- Helm 3 installed
- AWS CLI configured with admin credentials
- Your cluster's OIDC issuer URL and ID
The pattern of storing secrets in Kubernetes Secrets and committing encrypted versions to Git works, but it creates a coupling between your cluster and your secret storage that becomes a liability at scale. External Secrets Operator (ESO) flips this: secrets live in AWS Secrets Manager (or Vault, GCP Secret Manager, etc.), and ESO syncs them into Kubernetes Secrets automatically. Rotation in AWS propagates to the cluster within your configured refresh interval.
This tutorial installs ESO, configures AWS Secrets Manager access via IRSA, and creates an ExternalSecret that syncs into a usable Kubernetes Secret.
Step 1: Find your cluster OIDC information
You'll need these values for IAM trust policies. Replace CLUSTER_NAME and REGION:
1export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
2export AWS_REGION=us-east-1
3export CLUSTER_NAME=my-cluster
4
5# Get the OIDC issuer URL
6export OIDC_URL=$(aws eks describe-cluster \
7 --name $CLUSTER_NAME \
8 --region $AWS_REGION \
9 --query "cluster.identity.oidc.issuer" \
10 --output text)
11
12# Extract just the ID (last path segment)
13export OIDC_ID=$(echo $OIDC_URL | sed 's|.*/||')
14
15echo "OIDC ID: $OIDC_ID"Step 2: Add the External Secrets Helm repository
helm repo add external-secrets https://charts.external-secrets.io
helm repo updateStep 3: Install the External Secrets Operator
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespaceVerify all three pods are running:
kubectl get pods -n external-secretsExpected output:
NAME READY STATUS
external-secrets-7d9f8c6b5-xk4p2 1/1 Running
external-secrets-cert-controller-6b9d7b8d8c-r4n7l 1/1 Running
external-secrets-webhook-5c9b9b9b8c-k2j9m 1/1 Running
Step 4: Create an IAM policy for Secrets Manager access
Scope the policy to only the secrets ESO needs to read:
1cat <<EOF > eso-policy.json
2{
3 "Version": "2012-10-17",
4 "Statement": [
5 {
6 "Effect": "Allow",
7 "Action": [
8 "secretsmanager:GetSecretValue",
9 "secretsmanager:DescribeSecret",
10 "secretsmanager:GetResourcePolicy",
11 "secretsmanager:ListSecretVersionIds"
12 ],
13 "Resource": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:prod/*"
14 }
15 ]
16}
17EOF
18
19aws iam create-policy \
20 --policy-name ExternalSecretsPolicy \
21 --policy-document file://eso-policy.jsonStep 5: Create an IAM role with IRSA trust
IRSA (IAM Roles for Service Accounts) lets the ESO pod authenticate to AWS via its Kubernetes service account token — no static AWS credentials in the cluster:
1cat <<EOF > eso-trust.json
2{
3 "Version": "2012-10-17",
4 "Statement": [{
5 "Effect": "Allow",
6 "Principal": {
7 "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}"
8 },
9 "Action": "sts:AssumeRoleWithWebIdentity",
10 "Condition": {
11 "StringEquals": {
12 "oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:sub": "system:serviceaccount:external-secrets:external-secrets",
13 "oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:aud": "sts.amazonaws.com"
14 }
15 }
16 }]
17}
18EOF
19
20aws iam create-role \
21 --role-name ExternalSecretsRole \
22 --assume-role-policy-document file://eso-trust.json
23
24aws iam attach-role-policy \
25 --role-name ExternalSecretsRole \
26 --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/ExternalSecretsPolicyStep 6: Annotate the ESO service account
Link the Kubernetes service account to the IAM role:
kubectl annotate serviceaccount external-secrets \
-n external-secrets \
eks.amazonaws.com/role-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:role/ExternalSecretsRoleRestart the ESO deployment to pick up the new annotation:
kubectl rollout restart deployment/external-secrets -n external-secretsStep 7: Create a test secret in AWS Secrets Manager
aws secretsmanager create-secret \
--name prod/app/database \
--region $AWS_REGION \
--secret-string '{"username":"appuser","password":"supersecret"}'Step 8: Create a ClusterSecretStore
A ClusterSecretStore is cluster-scoped and can serve ExternalSecret resources in any namespace:
1cat <<EOF | kubectl apply -f -
2apiVersion: external-secrets.io/v1
3kind: ClusterSecretStore
4metadata:
5 name: aws-secrets-manager
6spec:
7 provider:
8 aws:
9 service: SecretsManager
10 region: ${AWS_REGION}
11 auth:
12 jwt:
13 serviceAccountRef:
14 name: external-secrets
15 namespace: external-secrets # required for ClusterSecretStore
16EOFVerify it's connected:
kubectl get clustersecretstore aws-secrets-managerExpected:
NAME AGE STATUS CAPABILITIES READY
aws-secrets-manager 30s Valid ReadOnly True
Step 9: Create an ExternalSecret
This syncs specific keys from the AWS secret into a Kubernetes Secret named app-secrets:
1cat <<EOF | kubectl apply -f -
2apiVersion: external-secrets.io/v1
3kind: ExternalSecret
4metadata:
5 name: app-secrets
6 namespace: default
7spec:
8 refreshInterval: 1h
9 secretStoreRef:
10 name: aws-secrets-manager
11 kind: ClusterSecretStore
12 target:
13 name: app-secrets
14 creationPolicy: Owner
15 data:
16 - secretKey: DB_PASSWORD
17 remoteRef:
18 key: prod/app/database
19 property: password
20 - secretKey: DB_USERNAME
21 remoteRef:
22 key: prod/app/database
23 property: username
24EOFStep 10: Verify the secret was created
kubectl get secret app-secrets -n default
kubectl get externalsecret app-secrets -n defaultExpected output:
NAME TYPE DATA AGE
app-secrets Opaque 2 30s
NAME STORE REFRESH INTERVAL STATUS READY
app-secrets aws-secrets-manager 1h SecretSynced True
Decode to confirm the values:
kubectl get secret app-secrets -n default \
-o jsonpath='{.data.DB_PASSWORD}' | base64 -dStep 11: Test secret rotation
Update the secret in AWS:
aws secretsmanager update-secret \
--secret-id prod/app/database \
--region $AWS_REGION \
--secret-string '{"username":"appuser","password":"newpassword"}'Force an immediate sync (instead of waiting for the 1-hour refresh):
kubectl annotate externalsecret app-secrets \
-n default \
force-sync=$(date +%s) --overwriteCheck the updated value:
kubectl get secret app-secrets -n default \
-o jsonpath='{.data.DB_PASSWORD}' | base64 -dWhat you built
ESO is syncing secrets from AWS Secrets Manager into Kubernetes Secrets automatically. When a secret rotates in AWS, ESO picks up the new value on the next refresh cycle and updates the Kubernetes Secret. Pods consuming the secret via envFrom or volume mounts receive the new value on their next restart. No static AWS credentials in the cluster — authentication is entirely via IRSA.
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.