A Guide to Managing Kubernetes Secrets with AWS Secrets Manager and External Secrets Operator


Managing secrets in Kubernetes is notoriously tricky. Hardcoding them? Yikes. Storing them in plaintext? Dangerous. In this post, I’ll show you how to securely integrate AWS Secrets Manager into your K8s workflow using External Secrets Operator (ESO) - so you can automate secret syncing and sleep better at night.
This post walks you through a clean, secure approach: syncing secrets from AWS Secrets Manager (SM) into Kubernetes using the External Secrets Operator (ESO). You'll learn how to set it up with Helm, configure access policies, and sync secrets in different formats - using real examples from the trenches.
Overview
ExternalSecrets is an open-source Kubernetes plugin that serves two main functions: it injects secrets from supported external providers into your application cluster and synchronizes these injected secrets with their corresponding remote counterparts.
In the ExternalSecrets architecture, two key resources play crucial roles:
SecretStore: This resource manages authentication, enabling your Kubernetes cluster to access AWS resources, specifically secrets. It acts as a bridge, ensuring secure and authorized access to the secrets stored in AWS.
ExternalSecret: This resource is responsible for defining and creating secrets. It utilizes the SecretStore to retrieve specific secrets and provides a template for Kubernetes controllers to generate local secrets within the cluster.
Step-By-Step
🛠️ Install External Secrets Operator via Helm
First, install the ESO Helm chart into its own namespace:
kubectl create ns eso
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n eso
🚫 Optional: If you manage CRDs manually, add
--set installCRDs=false
.
🔐 AWS IAM Setup for External Secrets
We need to create an IAM user or role with access to read specific secrets from AWS Secrets Manager.
Example IAM Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:ListSecrets",
"secretsmanager:GetSecretValue",
"secretsmanager:ListSecretVersionIds"
],
"Resource": [
"arn:aws:secretsmanager:ap-southeast-1:071123451249:secret:demo*"
],
"Condition": {
"StringLike": {
"secretsmanager:SecretId": [
"arn:aws:secretsmanager:ap-southeast-1:071123451249:secret:prod/demo"
]
},
"StringEquals": {
"aws:username": ["secret-eso"]
}
}
}
]
}
Why this policy setup?
Fine-grained access: Principle of least privilege.
Conditionals: Limits access to just the needed secrets + specific IAM username.
🔑 Kubernetes Secret for AWS Credentials
We create a K8s secret that holds AWS access keys.
echo -n 'KEYID' > ./access-key
echo -n 'SECRETKEY' > ./secret-access-key
kubectl create secret generic demo-awssm-secret \
--from-file=./access-key \
--from-file=./secret-access-key
rm -f ./access-key ./secret-access-key
⚠️ Pro tip: Store these secrets in a GitOps-friendly secret manager like SealedSecrets, SOPS, or External Secrets from your Git repo—not directly in plain YAML files.
🏗️ Configuring the SecretStore
This tells ESO how to talk to AWS Secrets Manager:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: demo-secretstore
spec:
provider:
aws:
service: SecretsManager
region: ap-southeast-1
auth:
secretRef:
accessKeyIDSecretRef:
name: demo-awssm-secret
key: access-key
secretAccessKeySecretRef:
name: demo-awssm-secret
key: secret-access-key
💾 Syncing Secrets in Kubernetes
You have three common use cases. Here's what each one looks like:
1. ConfigMap-style Secret (plaintext)
Best for multi-line config files.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: demo-secret-as-configmap-template
namespace: demo
spec:
refreshInterval: 5m
secretStoreRef:
name: demo-secretstore
kind: SecretStore
target:
name: demo-config
template:
engineVersion: v2
data:
core-dev.php: "{{ .coredev | toString }}"
custom.ini: "{{ .customini | toString }}"
demo-config.conf: "{{ .conf| toString }}"
service-url.php: "{{ .serviceurl | toString }}"
data:
- secretKey: coredev
remoteRef:
key: prod/demo/core-dev.php
- secretKey: customini
remoteRef:
key: prod/demo/custom.ini
- secretKey: conf
remoteRef:
key: prod/demo/cnf.conf
- secretKey: serviceurl
remoteRef:
key: prod/demo/service-url.php
🧠 Output: A
Secret
with multiple keys mimicking aConfigMap
, storing plaintext files.
2. Raw Secret from JSON (key/value)
Ideal for app credentials or API keys.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: demo-secret
namespace: demo
spec:
refreshInterval: 2m
secretStoreRef:
name: demo-secretstore
kind: SecretStore
target:
name: demo-secret
creationPolicy: Owner
dataFrom:
- extract:
key: prod/demo/secret
🎯 Output: A Kubernetes
Secret
with key/value pairs extracted from a JSON blob in AWS SM.
3. Templated ConfigMap (Redis Config)
When you want ESO to inject secrets into a full config file template.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: demo-secret-in-configmap
namespace: demo
spec:
refreshInterval: 2m
secretStoreRef:
name: demo-secretstore
kind: SecretStore
target:
name: demo-secret-redis-config
template:
data:
redis.conf: |
bind 0.0.0.0
port 6379
requirepass "{{ .redisPassword | toString }}"
protected-mode no
appendonly no
supervised no
save 3600 1 300 10 30 20
dir /opt/redis/data
loglevel notice
logfile "/opt/redis/data/redis.log"
databases 6
data:
- secretKey: redisPassword
remoteRef:
key: prod/demo/redis-credential
⚙️ Output: A fully rendered Redis config with live secrets baked in.
More Info
Subscribe to my newsletter
Read articles from Nhật Trường directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nhật Trường
Nhật Trường
Let explore DevOps, Security, and Tech insights with me. You're about to dive headfirst into my tech brain dump-expect spicy takes on best practice 💻 🚀