← Back to DevBytes

External Secrets Operator: Complete Implementation Guide

What is External Secrets Operator?

External Secrets Operator (ESO) is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, Google Secret Manager, and many others directly into Kubernetes. It synchronizes secrets from these external providers into native Kubernetes Secrets, keeping them automatically in sync with their source of truth.

Unlike storing sensitive data directly in Kubernetes Secrets—where they exist as base64-encoded values in etcd—ESO maintains a live connection to your secret backend. It pulls secrets on demand or on a scheduled refresh interval, ensuring that applications always receive fresh credentials without manual intervention or CI/CD pipeline exposure.

ESO acts as a bridge between Kubernetes and external secret stores. The operator watches for custom resources called ExternalSecret and SecretStore, then fetches secrets from the configured provider and materializes them as standard Kubernetes Secret objects that pods can mount as environment variables or volume files.

Why External Secrets Operator Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Managing secrets in Kubernetes presents several challenges that ESO addresses directly:

Core Concepts and Architecture

ESO operates with three primary custom resource definitions (CRDs):

SecretStore

A namespace-scoped resource that defines how to connect to an external secret provider. It holds provider-specific configuration such as AWS credentials, Azure service principal details, or Vault connection parameters. Only ExternalSecret resources in the same namespace can reference a SecretStore.

ClusterSecretStore

A cluster-wide variant of SecretStore. Any ExternalSecret in any namespace can reference a ClusterSecretStore, making it ideal for platform teams that want to configure provider access once and let all namespaces consume secrets from it.

ExternalSecret

The resource that declares which secrets to fetch from a SecretStore or ClusterSecretStore. It specifies the target Kubernetes Secret name, the keys to retrieve from the provider, and optional transformation rules. The ESO controller reconciles this resource and creates or updates the corresponding Kubernetes Secret.

Controller Architecture

The ESO deployment consists of a single controller pod (when installed via Helm) that watches for ExternalSecret, SecretStore, and ClusterSecretStore resources. On reconciliation, the controller authenticates to the configured external provider using the credentials defined in the referenced store, fetches the requested secrets, and writes them into a Kubernetes Secret in the appropriate namespace. The controller also manages the lifecycle—deleting an ExternalSecret removes the corresponding Kubernetes Secret if the deletionPolicy is set accordingly.

Installation and Setup

The recommended installation method is via Helm. Add the ESO chart repository and install the operator:

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

# Update your local Helm chart repository cache
helm repo update

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

Verify the installation by checking that the controller pod is running and the CRDs are registered:

# Check the controller pod status
kubectl get pods -n external-secrets

# Verify the CRDs are installed
kubectl get crd | grep external-secrets

You should see CRDs for secretstores.external-secrets.io, clustersecretstores.external-secrets.io, and externalsecrets.external-secrets.io. The controller pod should show Running with all containers ready.

Configuring Your First SecretStore

Below are complete, working examples for the most common providers. Each example assumes you have already set up the necessary cloud provider authentication (IRSA for AWS, Workload Identity for GCP, or pod identity for Azure).

AWS Secrets Manager

First, ensure your cluster has IAM permissions to access Secrets Manager. When using IRSA (IAM Roles for Service Accounts), create a service account with the appropriate role and reference it in the SecretStore:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: my-app
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: eso-sa  # Service account with IAM role for Secrets Manager access

If you prefer static credentials (not recommended for production), you can use secretRef authentication instead:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager-static
  namespace: my-app
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

Azure Key Vault

For Azure Key Vault, configure the vault URL and authentication. The example below uses workload identity with a service account:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: azure-keyvault
  namespace: my-app
spec:
  provider:
    azurekv:
      vaultUrl: https://my-vault-name.vault.azure.net
      authType: WorkloadIdentity
      serviceAccountRef:
        name: eso-sa  # Service account with Azure Workload Identity federation

Google Cloud Secret Manager

For GCP, specify the project ID and use workload identity authentication:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: gcp-secret-manager
  namespace: my-app
spec:
  provider:
    gcpsm:
      projectID: my-gcp-project-id
      auth:
        workloadIdentity:
          serviceAccountRef:
            name: eso-sa  # K8s service account federated with GCP service account

HashiCorp Vault

Connecting to HashiCorp Vault requires the server address and authentication details. Here is an example using token-based authentication:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store
  namespace: my-app
spec:
  provider:
    vault:
      server: https://vault.example.com
      path: kv-v2/my-secrets
      version: v2
      auth:
        tokenSecretRef:
          name: vault-token
          key: token

For Kubernetes-based authentication to Vault (where Vault trusts the Kubernetes cluster's JWT tokens), use:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store-k8s-auth
  namespace: my-app
spec:
  provider:
    vault:
      server: https://vault.example.com
      path: kv-v2/my-secrets
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: my-vault-role
          serviceAccountRef:
            name: eso-sa

Creating ExternalSecret Resources

Once your SecretStore (or ClusterSecretStore) is in place, create an ExternalSecret to fetch secrets. This resource declares which keys to pull and how to map them into the target Kubernetes Secret.

Basic Example: Single Secret Key

The simplest use case fetches a single secret value from the provider and places it into a Kubernetes Secret:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: my-external-secret
  namespace: my-app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: my-k8s-secret
    creationPolicy: Owner
  data:
    - secretKey: db-password
      remoteRef:
        key: prod/database/password

This creates a Kubernetes Secret named my-k8s-secret in the my-app namespace with a single key db-password whose value comes from the AWS Secrets Manager secret named prod/database/password. The refreshInterval of 1h tells ESO to reconcile every hour.

Fetching Multiple Keys from a JSON Secret

Many providers store secrets as JSON blobs. ESO can parse JSON and extract individual properties into separate Kubernetes Secret keys:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: my-app
spec:
  refreshInterval: 5m
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-creds
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/credentials
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/credentials
        property: password
    - secretKey: host
      remoteRef:
        key: prod/database/credentials
        property: host
    - secretKey: port
      remoteRef:
        key: prod/database/credentials
        property: port

Here, the AWS secret prod/database/credentials contains a JSON object like {"username":"admin","password":"s3cret!","host":"db.example.com","port":"5432"}. ESO extracts each property into its own key in the resulting Kubernetes Secret.

Using Metadata and Version Information

You can also pull metadata alongside secret values. For AWS Secrets Manager, for example, you can retrieve the secret version ID:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: secret-with-metadata
  namespace: my-app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: my-secret-with-version
    creationPolicy: Owner
  data:
    - secretKey: api-key
      remoteRef:
        key: prod/api/key
    - secretKey: version-id
      remoteRef:
        key: prod/api/key
        version: latest
        metadata: versionId

Working with ClusterSecretStore

When multiple teams across different namespaces need access to the same external secret provider, a ClusterSecretStore avoids duplicating provider configuration. Platform teams create it once, and developers reference it from any namespace.

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: global-aws-secrets
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: eso-sa
            namespace: external-secrets  # Service account in the ESO namespace

Now any ExternalSecret in any namespace can use this store:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: team-a-secret
  namespace: team-a
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: global-aws-secrets
    kind: ClusterSecretStore
  target:
    name: app-secret
    creationPolicy: Owner
  data:
    - secretKey: token
      remoteRef:
        key: team-a/service-token

This pattern centralizes credential management while keeping secrets namespace-scoped in their target locations.

Advanced Features

Secret Templating and Transformation

ESO supports Go templates for transforming secret values before they land in the Kubernetes Secret. This is useful when you need to combine multiple values or format them in a specific way:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: templated-secret
  namespace: my-app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: connection-string
    creationPolicy: Owner
    template:
      engineVersion: v2
      data:
        DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/mydb"
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/credentials
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/credentials
        property: password
    - secretKey: host
      remoteRef:
        key: prod/database/credentials
        property: host
    - secretKey: port
      remoteRef:
        key: prod/database/credentials
        property: port

The resulting Kubernetes Secret will contain a single key DATABASE_URL with the fully formed connection string. The template variables {{ .username }}, {{ .password }}, etc., are populated from the data blocks defined in the ExternalSecret.

Controlling Deletion Behavior

The creationPolicy field controls what happens when the ExternalSecret is deleted:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: orphan-secret
  namespace: my-app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: persistent-secret
    creationPolicy: Orphan  # Secret survives ExternalSecret deletion
  data:
    - secretKey: api-key
      remoteRef:
        key: prod/api/key

Automatic Secret Rotation Detection

ESO can detect secret rotation events from providers that support event-driven notifications (such as AWS Secrets Manager rotation). Combine refresh intervals with rotation detection for near-real-time updates:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: auto-rotated-secret
  namespace: my-app
spec:
  refreshInterval: 1h  # Periodic reconciliation as a fallback
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: rotated-creds
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: prod/database/password-rotating

For event-driven refresh, configure the ESO controller to listen for provider events (e.g., AWS EventBridge notifications via SQS). The controller can then reconcile specific ExternalSecret resources immediately when the backing secret changes, rather than waiting for the next refreshInterval window.

Specifying Secret Versions

For providers that support versioning (AWS Secrets Manager, GCP Secret Manager), you can pin to a specific version or always fetch the latest:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: pinned-version-secret
  namespace: my-app
spec:
  refreshInterval: 24h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: stable-creds
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: prod/database/password
        version: "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"  # Pin to a specific version

Best Practices

Scope SecretStores Appropriately

Use namespace-scoped SecretStore resources for team-specific providers and ClusterSecretStore for shared platform-wide providers. Avoid giving every namespace access to every provider—limit blast radius by scoping store references to the namespaces that genuinely need them.

Apply Least Privilege to Service Accounts

The service account referenced in a SecretStore should have permissions only for the specific secrets that namespace requires. For AWS, use resource-level permissions in IAM policies; for Vault, use path-scoped ACL policies; for Azure, limit the Key Vault access policy to specific secrets.

Set Appropriate Refresh Intervals

Choose refresh intervals based on your secret rotation cadence and the cost of provider API calls. Short intervals (1–5 minutes) are appropriate for rapidly rotating secrets in high-security environments. Longer intervals (1–24 hours) work well for stable credentials. Balance freshness against provider rate limits and costs.

Monitor ExternalSecret Status Conditions

Each ExternalSecret exposes status conditions that indicate whether reconciliation succeeded. Monitor these conditions with your observability stack:

# Check the status of an ExternalSecret
kubectl get externalsecret my-external-secret -n my-app -o jsonpath='{.status.conditions}' | jq .

Common conditions include Ready, FetchSucceeded, and Synced. Alert on any condition that stays False for an extended period.

Use Template Functions for Complex Transformations

When secrets require formatting beyond simple key-value mapping (connection strings, configuration files, combined credentials), leverage ESO's template engine. Keep templates readable and version them alongside your infrastructure-as-code to track changes.

Implement Deletion Policies Intentionally

Choose creationPolicy values deliberately. Owner is safest for ephemeral secrets managed entirely by ESO. Orphan prevents accidental data loss during ESO maintenance but requires manual cleanup. Merge is useful when ESO supplements an existing Secret with additional keys from an external provider.

Secure the SecretStore Credentials

The credentials used to authenticate to external providers (access keys, tokens, service principal secrets) should themselves be stored securely. Use Kubernetes Secrets referenced via secretRef or, better yet, avoid static credentials entirely by using workload identity federation (IRSA, Azure Workload Identity, GCP Workload Identity Federation). Never embed raw credentials in SecretStore YAML.

Plan for Provider Failures

ESO caches secrets in Kubernetes Secret resources, which means applications continue to function even if the external provider is temporarily unavailable. However, if a pod restarts and the cached Secret has expired keys, failures can occur. Design applications to handle credential failures gracefully, and set refreshInterval conservatively enough that the cache remains valid during typical provider outages.

Audit and Version Your ExternalSecret Definitions

Treat ExternalSecret and SecretStore YAML files as part of your infrastructure-as-code. Store them in version control, review changes through pull requests, and apply them via GitOps workflows (Flux, ArgoCD) rather than ad-hoc kubectl apply commands.

Enable Controller Metrics

ESO exposes Prometheus metrics for reconciliation counts, latency, and error rates. Scrape these metrics to build dashboards and alerts. Key metrics include external_secrets_sync_calls_total, external_secrets_sync_duration_seconds, and external_secrets_store_ready.

# Example Prometheus ServiceMonitor for ESO
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: external-secrets-monitor
  namespace: external-secrets
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: external-secrets
  endpoints:
    - port: metrics
      interval: 30s

Conclusion

External Secrets Operator fundamentally improves how Kubernetes workloads consume sensitive data. By bridging external secret stores with native Kubernetes Secret resources, ESO eliminates the security gaps inherent in storing secrets directly in cluster etcd or injecting them via CI/CD pipelines. It centralizes secret management, enforces a single source of truth, enables automatic rotation, and provides comprehensive audit trails through the underlying provider's logging capabilities.

The implementation pattern is straightforward: define a SecretStore or ClusterSecretStore to establish provider connectivity, then create ExternalSecret resources that declare which secrets to fetch and how to map them into Kubernetes Secret keys. Advanced features like templating, version pinning, deletion policies, and event-driven refresh give teams precise control over secret lifecycle management. By following best practices—least-privilege service accounts, appropriate refresh intervals, monitoring of status conditions, and infrastructure-as-code management—you can build a robust, auditable, and scalable secret management layer that serves every namespace in your cluster without compromising security or developer experience.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles