Part ofSecrets Management·Step 3 of 3
Security

Install External Secrets Operator and Sync AWS Secrets Manager into Kubernetes

Intermediate25 min to complete16 min readMay 12, 2026Updated August 19, 2026

Quick answer

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.

intermediate · 25 min

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
External Secrets
AWS
Secrets Manager
IRSA
Kubernetes
Helm
Security

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. If you're still deciding between ESO and Vault, our Vault vs External Secrets Operator comparison covers when each one fits.

Step 1: Find your cluster OIDC information

You'll need these values for IAM trust policies. Replace CLUSTER_NAME and REGION:

bash
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

bash
helm repo add external-secrets https://charts.external-secrets.io
helm repo update

Step 3: Install the External Secrets Operator

bash
helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace

Verify all three pods are running:

bash
kubectl get pods -n external-secrets

Expected 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:

bash
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.json

Step 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:

bash
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/ExternalSecretsPolicy

Step 6: Annotate the ESO service account

Link the Kubernetes service account to the IAM role:

bash
kubectl annotate serviceaccount external-secrets \
  -n external-secrets \
  eks.amazonaws.com/role-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:role/ExternalSecretsRole

Restart the ESO deployment to pick up the new annotation:

bash
kubectl rollout restart deployment/external-secrets -n external-secrets

Step 7: Create a test secret in AWS Secrets Manager

bash
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:

bash
1cat <<EOF | kubectl apply -f -
2apiVersion: external-secrets.io/v1beta1
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
16EOF

Verify it's connected:

bash
kubectl get clustersecretstore aws-secrets-manager

Expected:

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:

bash
1cat <<EOF | kubectl apply -f -
2apiVersion: external-secrets.io/v1beta1
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
24EOF

Step 10: Verify the secret was created

bash
kubectl get secret app-secrets -n default
kubectl get externalsecret app-secrets -n default

Expected 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:

bash
kubectl get secret app-secrets -n default \
  -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

Step 11: Test secret rotation

Update the secret in AWS:

bash
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):

bash
kubectl annotate externalsecret app-secrets \
  -n default \
  force-sync=$(date +%s) --overwrite

Check the updated value:

bash
kubectl get secret app-secrets -n default \
  -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

What 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.

Frequently Asked Questions

Does External Secrets Operator store secrets itself?

No. It reads from a backend you already run — AWS Secrets Manager, Vault, Azure Key Vault — and materialises the value as a native Kubernetes Secret. The backend remains the source of truth. That also means the secret ends up in etcd, so enable encryption at rest and keep RBAC on Secrets tight.

What is the difference between a SecretStore and a ClusterSecretStore?

A SecretStore is namespaced and usable only by ExternalSecrets in that namespace. A ClusterSecretStore is cluster-wide, so one configuration serves every namespace. Use the cluster-scoped version for a shared backend and credential; use namespaced stores when teams must authenticate as different identities.

How quickly does a rotated secret reach my pods?

The operator refreshes on the interval you set, so the Kubernetes Secret updates within that window. Your pods are a separate question: a Secret mounted as a volume updates in place after a delay, but one consumed as an environment variable is fixed for the life of the container. Environment variables need a restart to pick up a rotation.

Why is my ExternalSecret stuck with a SecretSyncedError?

Usually the IRSA annotation missing or the IAM policy not permitting the specific secret ARN. Describe the ExternalSecret and read the status condition, which names the failure. Confirm the operator's ServiceAccount is actually assuming the role by checking the caller identity from inside its pod.

Official References

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.