Skip to content

Banner image Banner image

Cloud‑native patterns: Why you should use External Secrets Operator with ArgoCD

There's a moment every GitOps team hits where someone asks the obvious question: "where do the secrets go?"

The rest of the configuration is in Git. It's version-controlled, reviewable, auditable. But you can't put secrets there. So they end up... somewhere else. Sometimes it's a manual kubectl create secret that nobody documents. Sometimes it's base64-encoded values tucked into Helm values files with a comment that says "TODO: fix this properly". Sometimes it's a mix of both, spread across three clusters, maintained by different people who've all developed their own workarounds.

External Secrets Operator (ESO) is the "fix this properly" solution. It connects ArgoCD's GitOps workflow to your secret manager of choice — AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Azure Key Vault — so secrets are pulled automatically at deploy time, never stored in Git, and rotated without touching your GitOps configuration.


How ESO works with ArgoCD

The core idea: instead of storing secret values in Git, you store references to secrets. ESO reads those references, fetches the actual values from your secret manager, and creates Kubernetes Secrets in the cluster. ArgoCD syncs the ExternalSecret manifest from Git. ESO handles the rest.

ESO system context ESO system context

The flow:

  1. You store the secret value in your secret manager (AWS Secrets Manager, Vault, etc.)
  2. You commit an ExternalSecret manifest to Git — it describes which secret to fetch and where to put it
  3. ArgoCD syncs the ExternalSecret manifest to the cluster
  4. ESO sees the ExternalSecret, authenticates to your secret manager, fetches the value, and creates a standard Kubernetes Secret
  5. Your application reads from the Kubernetes Secret as normal — it doesn't know or care about ESO

From ArgoCD's perspective, it's syncing YAML that lives in Git. From the application's perspective, there's a Kubernetes Secret with the right values. The only thing that's changed is where the values come from.

ESO flow ESO flow


The two key resources

SecretStore defines how to connect to your secret manager — a cluster-level or namespace-level resource that holds the authentication configuration:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: eu-west-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

ExternalSecret defines what to fetch and where to put it:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets
    kind: SecretStore
  target:
    name: database-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: /production/database/credentials
        property: password
    - secretKey: DB_USERNAME
      remoteRef:
        key: /production/database/credentials
        property: username

The ExternalSecret is what lives in Git. It has no sensitive values. Anyone with access to the repository can see which secrets are being used and where they come from, without being able to see the values. That's your audit story right there.


Why this matters more than it sounds

The security argument is obvious — plaintext secrets in Git is bad. But the operational argument is just as compelling.

Secret rotation without redeployment. Update the value in your secret manager. ESO picks it up on the next refresh interval (configurable — default is one hour, can be much shorter). No need to update any GitOps configuration, trigger a pipeline, or restart anything manually. The secret rotates and the cluster picks it up automatically.

Consistent secret management across clusters. The same ExternalSecret manifests work across your dev, staging, and production clusters, as long as each cluster has a SecretStore pointing to the right secret manager path for that environment. One manifest, consistent behaviour everywhere.

Clean ArgoCD diffs. When ArgoCD compares the cluster state to Git, it compares the ExternalSecret manifest — not the generated Kubernetes Secret. You don't get spurious diffs because a secret value changed. The GitOps workflow stays clean.

Auditability. You can see in Git which applications use which secrets, when the ExternalSecret configuration changed, and who approved it. The secret manager gives you a separate audit trail for when values changed and who accessed them. Together, that's a solid compliance story.


Getting started

If you're running ArgoCD and haven't added ESO yet, the path is straightforward:

  1. Install ESO via Helm or the operator — it runs as a controller in your cluster
  2. Create a ClusterSecretStore or namespace-scoped SecretStore with credentials for your secret manager
  3. Replace any Secret manifests that contain actual values with ExternalSecret manifests that reference them
  4. Let ArgoCD sync — ESO creates the Kubernetes Secrets automatically

The migration from raw Kubernetes Secrets to ExternalSecrets is incremental. You don't have to do it all at once. Pick the most sensitive secrets first (database credentials, API keys, service account tokens) and work through the rest as you go.

Once it's in place, the question of "where do the secrets go?" has a clean answer: in your secret manager, where they belong.


The working code

The companion repo has complete YAML for both AWS Secrets Manager and HashiCorp Vault backends — ClusterSecretStore, ExternalSecret, and a multi-environment pattern with environment-specific refresh intervals.

→ eso-argocd example

# Check that ESO has successfully created a secret
kubectl get externalsecret payments-db-creds -n payments
kubectl describe secret payments-db-creds -n payments