External Secrets Operator (ESO) is a Kubernetes operator that synchronizes secrets from external secret management systems (like AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault, etc.) into Kubernetes Secrets. This allows you to manage your sensitive data outside of Kubernetes, leveraging the robust security features of dedicated secret stores, while still making them available to your applications within Kubernetes as native Secrets.

Let’s see this in action with a simple example using AWS Secrets Manager.

Imagine you have a secret in AWS Secrets Manager named my-app/database-credentials. It contains a JSON object like this:

{
  "username": "db_user",
  "password": "supersecretpassword"
}

You want to make these credentials available as a Kubernetes Secret named database-credentials in your default namespace, which your application can then mount as a volume or read as environment variables.

First, you need to install External Secrets Operator. You can do this using Helm:

helm repo add external-secrets https://external-secrets.io/charts
helm repo update
helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets-operator \
  --create-namespace

Next, you need to define a SecretStore resource. This tells ESO where to find your external secrets and how to authenticate. For AWS Secrets Manager, it would look something like this:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager-store
  namespace: default
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountToken: "" # This will be populated by the SA of the running pod

In this SecretStore, we specify SecretsManager as the service, the AWS region, and the authentication method. For AWS, ESO can often use the IAM role attached to the Kubernetes service account of the ESO pod itself (if configured appropriately with IRSA or similar). If not, you might need to provide explicit credentials, but using the SA’s IAM role is the recommended and more secure approach.

Now, you create an ExternalSecret resource. This is the resource that links your external secret to a Kubernetes Secret.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials-secret
  namespace: default
spec:
  refreshInterval: "1h" # How often to check for updates in the external secret
  secretStoreRef:
    name: aws-secrets-manager-store
    key: aws-secrets-manager-store # Defaults to the SecretStore name if not specified
  target:
    name: database-credentials # The name of the Kubernetes Secret to create
    creationPolicy: Owner # The owner of the created secret
  data:
    - secretKey: username # The key in the Kubernetes Secret
      remoteRef:
        key: my-app/database-credentials # The name of the secret in AWS Secrets Manager
        property: username # The specific property within the AWS secret (if it's JSON)
    - secretKey: password
      remoteRef:
        key: my-app/database-credentials
        property: password

When you apply this ExternalSecret manifest, ESO will:

  1. Read the ExternalSecret resource.
  2. Consult the secretStoreRef to find the aws-secrets-manager-store.
  3. Use the authentication method defined in the SecretStore to connect to AWS Secrets Manager.
  4. Fetch the secret named my-app/database-credentials from AWS Secrets Manager.
  5. Extract the username and password properties from the fetched secret.
  6. Create or update a Kubernetes Secret named database-credentials in the default namespace, containing username and password keys with their respective values.

Your application pods can then consume this database-credentials Secret like any other Kubernetes secret:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: default
spec:
  containers:
    - name: app
      image: nginx
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: database-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: database-credentials
              key: password
      volumeMounts:
        - name: db-credentials-volume
          mountPath: "/etc/secrets"
  volumes:
    - name: db-credentials-volume
      secret:
        secretName: database-credentials

The most surprising true thing about External Secrets Operator is that it doesn’t store secrets itself; it acts as a highly efficient, event-driven bridge. When a Kubernetes Secret is updated or created via an ExternalSecret resource, ESO reacts, fetches the latest from the external store, and updates the Kubernetes Secret. It’s a reactive system that ensures your in-cluster secrets are always a faithful, up-to-date reflection of your external secrets.

The refreshInterval on the ExternalSecret is crucial. It dictates how often ESO will poll the external secret store for changes. A shorter interval means faster propagation of updates but increased API calls to your secret manager. A longer interval means slower propagation but fewer API calls. If your secrets change very frequently and your applications need those changes immediately, you might consider a very short interval or explore more advanced reconciliation mechanisms if your secret store supports event notifications.

The creationPolicy field on the ExternalSecret’s target is also significant. Owner is the default and recommended setting, meaning the ExternalSecret resource will be the owner of the generated Kubernetes Secret. If the ExternalSecret is deleted, the generated Secret will also be garbage collected. Other options include Merge (merges data into an existing secret) and Orphan (doesn’t set an owner reference).

One thing that often trips people up is how ESO handles updates to secrets that contain multiple key-value pairs. When you define data entries in your ExternalSecret, ESO expects a direct mapping. If your external secret is a JSON blob, you use the property field to extract specific keys. If it’s a plain string, you’d omit property and the entire string content would be placed under the specified secretKey. If you change the structure of your external secret (e.g., from plain text to JSON or vice-versa), you’ll need to adjust your ExternalSecret definition accordingly.

The next concept you’ll likely encounter is how to manage secrets that require more complex transformations or conditional logic, which might lead you to explore custom ExternalSecret templates or more advanced operators that build upon ESO’s foundation.

Want structured learning?

Take the full Argocd course →