Back to Distributed Systems & Kubernetes Series

External Secrets Track Part 1: SecretStore & ExternalSecret

June 6, 2026 Wasil Zafar 35 min read

The External Secrets Operator (ESO) bridges the gap between external secret management systems (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager) and Kubernetes Secrets. Instead of manually syncing secrets or baking them into manifests, ESO watches ExternalSecret CRDs and automatically creates native Kubernetes Secrets from external sources.

Table of Contents

  1. ESO Concepts
  2. Install ESO
  3. SecretStore CRD
  4. ExternalSecret CRD
  5. AWS Secrets Manager Example
  6. Vault Example
  7. Exercises
  8. Key Takeaways & Next Steps

ESO Concepts

The External Secrets Operator solves a fundamental Kubernetes problem: how do you securely inject secrets from an external secret manager without storing them in Git or manually creating Kubernetes Secrets?

  • SecretStore — Defines how to connect to an external provider (credentials, endpoint, auth method)
  • ClusterSecretStore — Cluster-wide version of SecretStore, accessible from any namespace
  • ExternalSecret — Declares what secrets to fetch and where to store them (creates a K8s Secret)
  • Reconciliation Loop — ESO periodically syncs secrets based on refreshInterval, keeping K8s Secrets up to date
  • Supported Providers — Vault, AWS Secrets Manager, AWS SSM Parameter Store, GCP Secret Manager, Azure Key Vault, 1Password, and many more
Flow: ExternalSecret references a SecretStore → ESO reads the ExternalSecret → ESO authenticates to the provider via SecretStore config → ESO fetches the secret value → ESO creates/updates a native Kubernetes Secret → Your pods mount the Secret as usual.

Install ESO

# Add the External Secrets Helm chart repository
helm repo add external-secrets https://charts.external-secrets.io
helm repo update

# Install ESO into its own namespace
helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace \
  --set installCRDs=true

# Verify installation
kubectl get pods -n external-secrets
# NAME                                                READY   STATUS    AGE
# external-secrets-6b7d8c9f4-abc12                    1/1     Running   1m
# external-secrets-cert-controller-5f6d7e8a9-def34    1/1     Running   1m
# external-secrets-webhook-7c8d9e0f1-ghi56            1/1     Running   1m

# Verify CRDs
kubectl get crd | grep external-secrets
# clustersecretstores.external-secrets.io
# externalsecrets.external-secrets.io
# secretstores.external-secrets.io

SecretStore CRD

Cluster vs Namespace Scoped

ESO provides two scopes for defining provider connections:

  • SecretStore — Namespace-scoped. Only ExternalSecrets in the same namespace can reference it. Best for team-owned provider access.
  • ClusterSecretStore — Cluster-wide. Any ExternalSecret in any namespace can reference it. Best for platform teams providing centralized secret access.

Provider Configurations

# AWS Secrets Manager SecretStore (namespace-scoped)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secretsmanager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        secretRef:
          accessKeyIDSecretRef:
            name: aws-credentials
            key: access-key-id
          secretAccessKeySecretRef:
            name: aws-credentials
            key: secret-access-key
# GCP Secret Manager ClusterSecretStore (cluster-wide)
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: gcp-secretmanager
spec:
  provider:
    gcpsm:
      projectID: my-gcp-project-123
      auth:
        secretRef:
          secretAccessKeySecretRef:
            name: gcp-sa-credentials
            key: sa-key.json
            namespace: external-secrets
# HashiCorp Vault SecretStore (Kubernetes auth)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-app"
          serviceAccountRef:
            name: vault-auth-sa

ExternalSecret CRD

The ExternalSecret declares which secrets to fetch from the provider and how to map them into a Kubernetes Secret.

# Basic ExternalSecret — fetch individual keys
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h          # How often to re-sync from provider
  secretStoreRef:
    name: aws-secretsmanager   # Reference to SecretStore
    kind: SecretStore
  target:
    name: app-secrets          # Name of the K8s Secret to create
    creationPolicy: Owner      # ESO owns the Secret (deletes when ExternalSecret is deleted)
  data:
    - secretKey: db-password         # Key in the K8s Secret
      remoteRef:
        key: production/database     # Path in AWS Secrets Manager
        property: password           # JSON property within the secret

    - secretKey: api-key
      remoteRef:
        key: production/api-keys
        property: stripe-key

    - secretKey: redis-url
      remoteRef:
        key: production/redis
        property: connection-string
# Verify the ExternalSecret synced successfully
kubectl get externalsecret app-secrets -n production
# NAME          STORE                REFRESH   STATUS         READY
# app-secrets   aws-secretsmanager   1h        SecretSynced   True

# The created Kubernetes Secret
kubectl get secret app-secrets -n production -o yaml
# apiVersion: v1
# kind: Secret
# metadata:
#   name: app-secrets
#   namespace: production
#   ownerReferences:
#     - apiVersion: external-secrets.io/v1beta1
#       kind: ExternalSecret
#       name: app-secrets
# data:
#   db-password: cGFzc3dvcmQxMjM=    (base64 encoded)
#   api-key: c2tfdGVzdF8xMjM=
#   redis-url: cmVkaXM6Ly8uLi4=
refreshInterval: Set this based on how often secrets rotate. For database passwords that rotate daily, use 1h. For static API keys, 24h is sufficient. Set to 0 to disable auto-refresh (manual only).

AWS Secrets Manager Example

Complete end-to-end example: create an IAM policy, set up credentials, create SecretStore, and sync secrets.

# 1. Create IAM policy for ESO (least privilege)
cat <<EOF > eso-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:production/*"
    }
  ]
}
EOF

aws iam create-policy \
  --policy-name ESO-SecretsReadOnly \
  --policy-document file://eso-policy.json

# 2. Create IAM user and attach policy
aws iam create-user --user-name eso-service-account
aws iam attach-user-policy \
  --user-name eso-service-account \
  --policy-arn arn:aws:iam::123456789012:policy/ESO-SecretsReadOnly

# 3. Create access keys and store in Kubernetes
aws iam create-access-key --user-name eso-service-account

kubectl create secret generic aws-credentials \
  --namespace production \
  --from-literal=access-key-id=AKIAIOSFODNN7EXAMPLE \
  --from-literal=secret-access-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# 4. Complete SecretStore + ExternalSecret for AWS
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-sm
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        secretRef:
          accessKeyIDSecretRef:
            name: aws-credentials
            key: access-key-id
          secretAccessKeySecretRef:
            name: aws-credentials
            key: secret-access-key
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: production-db-creds
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-sm
    kind: SecretStore
  target:
    name: db-credentials
  data:
    - secretKey: username
      remoteRef:
        key: production/rds-postgres
        property: username
    - secretKey: password
      remoteRef:
        key: production/rds-postgres
        property: password
    - secretKey: host
      remoteRef:
        key: production/rds-postgres
        property: host

Vault Example

Using Vault with Kubernetes authentication — the most secure pattern since no static credentials are stored in the cluster.

# ServiceAccount for Vault authentication
apiVersion: v1
kind: ServiceAccount
metadata:
  name: vault-auth-sa
  namespace: production
---
# SecretStore using Vault Kubernetes auth
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-kv
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-reader"
          serviceAccountRef:
            name: vault-auth-sa
---
# ExternalSecret fetching from Vault KV v2
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: vault-app-secrets
  namespace: production
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: vault-kv
    kind: SecretStore
  target:
    name: app-config
    creationPolicy: Owner
  data:
    - secretKey: db-url
      remoteRef:
        key: production/database
        property: connection_url
    - secretKey: jwt-signing-key
      remoteRef:
        key: production/auth
        property: jwt_secret
    - secretKey: encryption-key
      remoteRef:
        key: production/encryption
        property: aes_256_key
Vault + K8s Auth: With Kubernetes auth, Vault validates the ServiceAccount token presented by ESO against the Kubernetes API. No static credentials exist — the trust is established between Vault and the cluster's token reviewer API. This is the recommended production pattern.

Exercises

Exercise 1: Install ESO via Helm in a Kind cluster. Create a simple SecretStore using the fake provider (built-in for testing) and an ExternalSecret that syncs a test value. Verify the Kubernetes Secret is created with the correct data.
Exercise 2: Set up a local Vault instance (dev mode). Configure Kubernetes auth in Vault, create a SecretStore with Vault provider, and sync a KV secret into a Kubernetes Secret via ExternalSecret. Change the value in Vault and wait for the refreshInterval to verify the Kubernetes Secret updates automatically.
Exercise 3: Create a ClusterSecretStore that connects to AWS Secrets Manager (or LocalStack for local testing). Then create ExternalSecrets in two different namespaces that both reference the same ClusterSecretStore. Verify that both namespaces get their own copy of the Kubernetes Secret.

Key Takeaways & Next Steps

  • ESO syncs external secrets into native Kubernetes Secrets — no manual kubectl create secret needed
  • SecretStore defines the provider connection; ExternalSecret defines what to fetch
  • ClusterSecretStore provides cluster-wide access; SecretStore is namespace-scoped
  • refreshInterval controls sync frequency — secrets auto-update when the external value changes
  • Vault with Kubernetes auth is the most secure pattern (no static credentials)
  • ESO supports 20+ providers including AWS, GCP, Azure, Vault, 1Password, and Doppler

Next in the Series

In Part 2: PushSecret & Generators, we'll explore pushing Kubernetes Secrets to external providers, generating dynamic secrets (passwords, UUIDs), syncing secrets across multiple namespaces with ClusterExternalSecret, and templating transformations.