Learn how to sync Kubernetes secrets from AWS Secrets Manager using the External Secrets Operator—so you can keep credentials out of Git and manage them centrally across dev, stage, and production.
In most enterprise systems, release cycles use separate environments (dev, stage, live), each with its own configuration. An application might need three different sets of database credentials—one per environment—so developers work against a dedicated database without touching production. Managing these credentials securely is where Kubernetes External Secrets and AWS Secrets Manager fit in.
Kubernetes Secrets store sensitive data in your cluster. They are native resources in the cluster data store (etcd) and can be mounted into containers at runtime. Managing them by hand is error-prone and doesn’t scale. This guide shows how to use External Secrets to pull secrets from AWS Secrets Manager into Kubernetes automatically.
The External Secrets Operator lets you use external secret backends—such as AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault—to populate Kubernetes Secrets. You declare what to fetch; the controller keeps the cluster in sync.
An ExternalSecret declares how to fetch the secret data; the controller turns those into standard Kubernetes Secrets. Pods use Secrets as usual—no code changes. By default, Secrets are not encrypted at rest in etcd and can be exposed via the API or etcd backups. To harden this, use an external KMS plugin to encrypt Secrets in etcd.
How External Secrets Work (High-Level)
ExternalSecretsare added in the cluster (e.g.kubectl apply -f external-secret-example.yml)- Controller fetches
ExternalSecretsusing the Kubernetes API - Controller uses
ExternalSecretsto fetch secret data from external providers (e.g, AWS Secrets Manager) - Controller upserts
Secrets - Pods consume the synced Secrets as normal Kubernetes Secrets.
Related reading:
- How to use External Secrets with GCP Secrets manager
- How to create and manage Secrets in GCP Secret Manager using Terraform
1. Configure AWS Access for Secrets Manager
Create an IAM policy that allows reading from AWS Secrets Manager. Start by creating a policy named secrets-reader:
POLICY_ARN=$(aws iam create-policy --policy-name secrets-reader --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:ListSecrets",
"secretsmanager:GetSecretValue"
],
"Resource": [
"*"
]
}
]
}' | jq -r .Policy.Arn)
We now create a group that will use this policy:
aws iam create-group --group-name secret-readers
aws iam attach-group-policy --policy-arn $POLICY_ARN --group-name secret-readers
Next, create an IAM user and add it to the group:
aws iam create-user --user-name external-secrets
aws iam add-user-to-group --group-name secret-readers --user-name external-secrets
Create access keys for that user and store them in a Kubernetes secret (the key names must match the SecretStore refs below):
aws iam create-access-key --user-name external-secrets > creds.json
ACCESS_KEY=$(cat creds.json | jq -r .AccessKey.AccessKeyId)
SECRET_KEY=$(cat creds.json | jq -r .AccessKey.SecretAccessKey)
kubectl create secret generic aws-secret --from-literal=access-key=$ACCESS_KEY --from-literal=secret-access-key=$SECRET_KEY
Next, add a secret in AWS Secrets Manager that we will sync into Kubernetes:
aws secretsmanager --region=eu-west-1 create-secret --name live/citizix-db-secret \
--description "DB Secret for live citizix" \
--secret-string '{
"password": "SuperS3cure",
"host": "citizix-db.cvytwhgsiext.eu-west-1.rds.amazonaws.com",
"port": "5432",
"db": "citizix",
"user": "citizix"
}'
2. Install the External Secrets Operator
The External Secrets Operator is available as a Helm chart. Add the repo and install it in your cluster:
helm repo add external-secrets https://external-secrets.github.io/kubernetes-external-secrets/
helm install external-secrets external-secrets/kubernetes-external-secrets
To install without Helm, template the manifests and apply them with kubectl:
helm template --include-crds --output-dir ./output_dir external-secrets/kubernetes-external-secrets
Apply the manifests in ./output_dir to deploy the operator.
3. Create a SecretStore (AWS Secrets Manager Backend)
A SecretStore is a namespaced resource that defines how the External Secrets controller authenticates to your secret backend (here, AWS Secrets Manager). It holds the backend configuration and references a Kubernetes Secret that contains the AWS credentials.
External Secrets also supports ClusterSecretStore, a cluster-wide store that any namespace can use. For a single namespace we use a namespaced SecretStore.
Create a SecretStore named aws-secret-manager that uses the aws-secret we created earlier:
cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secret-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-secret
key: access-key
secretAccessKeySecretRef:
name: aws-secret
key: secret-access-key
EOF
Note: For a ClusterSecretStore, you must set namespace in both accessKeyIDSecretRef and secretAccessKeySecretRef to the namespace where the credential Secret lives.
cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: aws-secret-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-secret
key: access-key
namespace: external-secrets
secretAccessKeySecretRef:
name: aws-secret
key: secret-access-key
namespace: external-secrets
EOF
Verify the SecretStore is valid:
$ kubectl get secretstore
NAME AGE STATUS
aws-secret-manager 9s Valid
4. Create an ExternalSecret (Sync One Secret as a Single Key)
An ExternalSecret declares which secret to fetch from AWS Secrets Manager and which Kubernetes Secret to create. It uses the SecretStore for authentication.
Create an ExternalSecret that pulls the AWS secret live/citizix-db-secret (created above) and writes it into a Kubernetes Secret citizix-db-secret under the key db-url:
cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: es-citizix-db-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secret-manager
kind: SecretStore
target:
name: citizix-db-secret
creationPolicy: Owner
data:
- secretKey: db-url
remoteRef:
key: live/citizix-db-secret
EOF
Check that the ExternalSecret synced successfully:
$ kubectl get externalsecret
NAME STORE REFRESH INTERVAL STATUS
es-citizix-db-secret aws-secret-manager 1h SecretSynced
Confirm the Kubernetes Secret was created:
$ kubectl get secret
NAME TYPE DATA AGE
citizix-db-secret Opaque 1 62s
Inspect the secret (values are base64-encoded):
$ kubectl describe secret citizix-db-secret
Name: citizix-db-secret
Namespace: default
Labels: <none>
Annotations: reconcile.external-secrets.io/data-hash: 9ca2636815e1610df9f9458d03e3814d
Type: Opaque
Data
====
db-url: 54 bytes
5. Extract JSON Keys into Separate Secret Keys
When the value in AWS Secrets Manager is JSON, you can map each key to a separate key in the Kubernetes Secret using dataFrom and extract. Here we use the same live/citizix-db-secret (it’s JSON with password, host, port, db, user) and create a Secret whose keys are those JSON fields:
cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: es-json-citizix-db-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secret-manager
kind: SecretStore
target:
name: json-citizix-db-secret
creationPolicy: Owner
dataFrom:
- extract:
key: live/citizix-db-secret
EOF
Verify the ExternalSecret status:
$ kubectl get es
NAME STORE REFRESH INTERVAL STATUS
es-json-citizix-db-secret aws-secret-manager 1h SecretSynced
Confirm the Kubernetes Secret has one key per JSON field:
$ kubectl get secret
NAME TYPE DATA AGE
json-citizix-db-secret Opaque 5 14s
Describe the secret to see the keys (password, host, port, db, user):
$ kubectl describe secret json-citizix-db-secret
Name: json-citizix-db-secret
Namespace: default
Labels: <none>
Annotations: reconcile.external-secrets.io/data-hash: 9c7f95c6c59808128f2ab2630e980dee
Type: Opaque
Data
====
db: 7 bytes
host: 54 bytes
password: 10 bytes
port: 4 bytes
user: 7 bytes
Summary and Next Steps
You now have Kubernetes External Secrets syncing from AWS Secrets Manager: IAM and a SecretStore for access, ExternalSecrets for full-value and JSON key extraction, and Kubernetes Secrets updated on the configured refresh interval.
Next steps:
- Use IRSA (IAM Roles for Service Accounts) on EKS instead of long-lived access keys for better security.
- Restrict the IAM policy to specific secret ARNs or prefixes instead of
"*". - Enable encryption at rest for Kubernetes Secrets (e.g. with a KMS provider and etcd encryption).
- Reuse the same pattern for other backends (e.g. External Secrets with GCP Secret Manager).