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.

ExternalSecrets architecture - Ali Nadir

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:

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

  2. 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 a ConfigMap, 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

0
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 💻 🚀