How to Use External Secrets with AWS Secrets Manager

Sync Kubernetes secrets from AWS Secrets Manager using External Secrets Operator. Step-by-step: IAM policy, SecretStore, ExternalSecret, and JSON key extraction for dev/stage/prod.

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)

  1. ExternalSecrets are added in the cluster (e.g. kubectl apply -f external-secret-example.yml)
  2. Controller fetches ExternalSecrets using the Kubernetes API
  3. Controller uses ExternalSecrets to fetch secret data from external providers (e.g, AWS Secrets Manager)
  4. Controller upserts Secrets
  5. Pods consume the synced Secrets as normal Kubernetes Secrets.

Related reading:

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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:

1
2
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:

1
2
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):

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
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:

1
2
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:

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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:

1
2
3
4
$ 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
$ kubectl get externalsecret

NAME                   STORE                   REFRESH INTERVAL   STATUS
es-citizix-db-secret   aws-secret-manager      1h                 SecretSynced

Confirm the Kubernetes Secret was created:

1
2
3
4
$ kubectl get secret

NAME                  TYPE                                  DATA   AGE
citizix-db-secret     Opaque                                1      62s

Inspect the secret (values are base64-encoded):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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:

1
2
3
4
$ 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:

1
2
3
4
$ 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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ 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).
comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy