
How to Use AWS Secrets Manager with Kubernetes (EKS)
The default pattern for secrets in Kubernetes — environment variables from ConfigMaps or opaque Secrets baked into manifests — puts sensitive values in etcd and in any CI system that touches the cluster. For EKS workloads, the better path is AWS Secrets Manager backed by IRSA (IAM Roles for Service Accounts): secrets live in a managed store, access is scoped per service account, and rotation can be handled independently of the application.
This covers the full integration: creating the secret, wiring up OIDC, scoping the IAM trust relationship, deploying the Secrets Store CSI Driver, and consuming the secret both as a mounted file and as an environment variable.
Prerequisites
- AWS CLI configured with an admin IAM user (programmatic access)
eksctlandkubectlinstalled- Basic familiarity with Kubernetes concepts
Goals
- Secret stored in AWS Secrets Manager and accessible from inside a pod
- IAM authentication wired up via OIDC and a service-account-scoped trust policy
- Secrets Store CSI Driver and AWS provider running in the cluster
- Deployment that mounts the secret as a file and exposes it as an environment variable
Step 1 — Create a Secret in AWS Secrets Manager
Head to the AWS Secrets Manager console and create a new secret:
- Choose Other type of secret and add a key-value pair (e.g.,
my_api_token→<your-value>). - Give the secret a descriptive name such as
prod/service-token. You'll reference this name later in Kubernetes. - Skip Resource Permissions and the rotation section for now — access will be granted directly via an IAM role.
Once created, open the secret and copy its full ARN (including the random suffix AWS appends). You'll need this when writing the IAM policy.
This approach works with both AWS Secrets Manager and AWS Parameter Store.
Step 2 — Create an EKS Cluster
Use eksctl to spin up a cluster. You can pass flags directly or use a YAML config file:
eksctl create cluster \
--name my-cluster \
--region us-east-1 \
--version 1.29 \
--zones us-east-1a,us-east-1b \
--managedeksctl uses CloudFormation under the hood and creates at least two stacks: one for the cluster and one for the managed node group, backed by an EC2 Auto Scaling Group.
Once the cluster is ready, verify connectivity:
kubectl get nodesStep 3 — Create an IAM OpenID Connect (OIDC) Provider
To allow Kubernetes service accounts to assume IAM roles, you need an OIDC identity provider:
- Go to the EKS console, open your cluster, and copy the OpenID Connect provider URL.
- In the IAM console, go to Identity providers and click Add provider.
- Select OpenID Connect, paste the URL, and click Get thumbprint.
- Set the audience to
sts.amazonaws.com.
Step 4 — Create an IAM Policy and Role
Policy
Create a policy that allows reading the specific secret:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "<your-secret-arn>"
}
]
}Name it something like APITokenReadAccess.
Role
Create an IAM role:
- Select Web identity as the trusted entity and choose the OIDC provider you created.
- Attach the
APITokenReadAccesspolicy. - Name the role
api-token-access.
Tighten the Trust Relationship
Edit the role's trust policy so only a specific Kubernetes service account can assume it:
{
"Condition": {
"StringEquals": {
"<oidc-provider-url>:sub": "system:serviceaccount:production:nginx"
}
}
}Replace the aud key with sub and set the value to :<service-account-name. This ensures no other service account in the cluster can assume this role.
Step 5 — Create the Kubernetes Namespace and Service Account
Create a folder (e.g., nginx/) to hold your Kubernetes manifests.
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: productionserviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/api-token-accessThe annotation on the service account is what links it to the IAM role. Without it, the pod won't be able to assume the role and the secret retrieval will fail.
Apply both:
kubectl apply -f nginx/Step 6 — Install the Secrets Store CSI Driver
The Secrets Store CSI Driver is what mounts secrets from external stores into pods as volumes. There are two ways to install it.
Option A — Plain YAML
Apply the two required Custom Resource Definitions:
kubectl apply -f secret-provider-class-crd.yaml
kubectl apply -f secret-provider-class-pod-status-crd.yamlThen deploy the driver components: ServiceAccount, ClusterRole, ClusterRoleBinding, DaemonSet, and CSIDriver. The ClusterRole must include secrets access if you want to expose secrets as environment variables.
kubectl apply -f csi-driver/
kubectl logs -l app=secrets-store-csi-driverOption B — Helm
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driverNote: The Helm chart does not enable Kubernetes secret syncing by default. You'll need to add RBAC permissions manually if you want environment variable support.
Step 7 — Install the AWS Secrets and Configuration Provider
This provider bridges the CSI driver with AWS Secrets Manager and Parameter Store. Create and apply the following manifests: ServiceAccount, ClusterRole, ClusterRoleBinding, and a DaemonSet.
kubectl apply -f aws-provider/
kubectl logs -l app=csi-secrets-store-provider-awsStep 8 — Create a SecretProviderClass
A SecretProviderClass is the custom resource that maps your AWS secret to a Kubernetes-consumable object. Here's what the manifest looks like:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets
namespace: production
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/service-token"
objectType: "secretsmanager"
objectAlias: "api-token"
secretObjects:
- secretName: api-token
type: Opaque
data:
- objectName: api-token
key: SECRET_TOKENThe parameters.objects block tells the provider which secret to fetch from AWS. The secretObjects block syncs it into a native Kubernetes Secret, which is what makes environment variable injection possible.
kubectl apply -f nginx/secret-provider-class.yamlStep 9 — Deploy Nginx and Consume the Secret
Here's a deployment that mounts the secret as a file and exposes it as an environment variable:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: production
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
serviceAccountName: nginx # must match the annotated service account
containers:
- name: nginx
image: nginx
volumeMounts:
- name: secrets-store
mountPath: /mnt/api-token
readOnly: true
env:
- name: API_TOKEN
valueFrom:
secretKeyRef:
name: api-token
key: SECRET_TOKEN
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: aws-secretskubectl apply -f nginx/deployment.yamlWatch the CSI driver logs after applying. You'll see immediately whether the secret was retrieved successfully or whether there's a permissions error to troubleshoot.
Step 10 — Verify
Check the mounted file:
kubectl exec -it <pod-name> -n production -- cat /mnt/api-token/api-tokenCheck the environment variable:
kubectl exec -it <pod-name> -n production -- env | grep API_TOKENBoth should display your secret value from AWS Secrets Manager.
Troubleshooting Common Failures
Pod stuck in ContainerCreating with a CSI volume error. The most common cause is the AWS provider DaemonSet not running on the node where the pod is scheduled. Check with kubectl get pods -n kube-system -l app=csi-secrets-store-provider-aws and confirm there is a running pod on every node.
AccessDeniedException in the provider logs. The IRSA trust policy is the first thing to verify. The StringEquals condition must match :<service-account-name exactly. A mismatch in namespace or service account name silently fails at the STS AssumeRoleWithWebIdentity call.
Secret syncs to the volume but the Kubernetes Secret is not created. The CSI driver must have the syncSecret.enabled=true Helm value set, and the ClusterRole must grant secrets RBAC access. Without this, secretObjects in the SecretProviderClass is silently ignored, so environment variable injection will fail while the mounted file works fine.
Secret value is stale after rotation in Secrets Manager. The CSI driver polls for updates on a rotation interval (default: 2 minutes). Pods that consume the secret via environment variables do not automatically pick up the new value because environment variables are set at container start. Only the mounted file reflects the updated value in-place. For rotation to reach environment variables, the pod must be restarted.
The annotation
eks.amazonaws.com/role-arnon the service account is what triggers the token projection for IRSA. If the annotation is missing or points to the wrong ARN, the pod's service account token will not be exchanged for AWS credentials, and every Secrets Manager API call will fail with a 401.
Summary
| Component | Purpose |
|---|---|
| AWS Secrets Manager | Stores the secret |
| OIDC Provider | Bridges Kubernetes identity to IAM |
| IAM Role + Policy | Grants read access to the secret |
| Secrets Store CSI Driver | Mounts secrets as volumes |
| AWS Provider | Fetches from Secrets Manager / Parameter Store |
| SecretProviderClass | Maps the AWS secret to Kubernetes |
| Kubernetes Secret (synced) | Enables environment variable usage |
Conclusion
With this setup, pods consume secrets directly from AWS Secrets Manager without any credentials in manifests, images, or etcd. The IRSA trust relationship scopes access to a single service account, the CSI driver handles volume mounting and Secret sync, and Secrets Manager handles rotation independently of the cluster. Adding new secrets requires updating the SecretProviderClass and redeploying — the IRSA and driver infrastructure does not need to change.
Comments