Syncing secrets into Kubernetes with Flux and External Secrets Operator (ESO) is a powerful pattern, but the most surprising thing is how often the "secrets" themselves become the vector for configuration errors, not the operators.

Let’s see it in action. Imagine we have a SecretStore that points to a HashiCorp Vault instance, and we want to sync a specific secret from Vault into our Kubernetes cluster.

First, the SecretStore definition. This tells ESO where to find your external secrets.

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store
  namespace: default
spec:
  provider:
    vault:
      server: "https://vault.example.com:8200"
      path: "kv/data" # For KV v2, this is the mount path
      version: "v2"
      auth:
        appRole:
          path: "approle"
          roleID: "a1b2c3d4-e5f6-7890-1234-abcdef123456"
          secretRef:
            name: vault-approle-secret
            key: secretID

Notice the secretRef. This vault-approle-secret is a standard Kubernetes Secret object that holds the AppRole secretID.

apiVersion: v1
kind: Secret
metadata:
  name: vault-approle-secret
  namespace: default
type: Opaque
data:
  secretID: "c1d2e3f4-g5h6-7890-1234-abcdef123456" # Base64 encoded

Now, the ExternalSecret resource. This is what tells ESO which secret to fetch from Vault and how to map it into Kubernetes.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: my-app-db-credentials
  namespace: default
spec:
  refreshInterval: "1h" # How often to check for updates
  secretStoreRef:
    name: vault-store
    kind: SecretStore
  target:
    name: db-credentials # The name of the Kubernetes Secret to create/update
    creationPolicy: Owner # If the target secret is deleted, ESO will recreate it
  data:
    - secretKey: username # The key within the Kubernetes Secret
      remoteKey: "database/config#username" # Path in Vault and key within the secret data
    - secretKey: password
      remoteKey: "database/config#password"

When you apply these, Flux (or any other Kubernetes controller watching ExternalSecret resources) will see the ExternalSecret object. It then tells ESO to do its job. ESO, using the SecretStore configuration, authenticates to Vault, fetches the secret located at kv/data/database/config (assuming KV v2 and the kv engine is mounted at kv), extracts the username and password fields from the Vault secret data, and creates/updates a Kubernetes Secret named db-credentials in the default namespace with those values.

The problem ESO solves is centralizing secrets management outside of Kubernetes, while still allowing applications to access them as native Kubernetes Secret objects. This means you can rotate secrets in Vault without redeploying your applications, and you don’t have to scatter sensitive credentials across multiple YAML files. The SecretStore acts as a credential for accessing the external secret manager, and the ExternalSecret is the manifest that defines the desired state of the Kubernetes Secret.

The remoteKey format is crucial. For Vault KV v2, it’s typically mount_path/data/secret_path#key_name. So, if your KV v2 engine is mounted at secret and your secret is at myapp/config, and it contains a key api_key, the remoteKey would be secret/data/myapp/config#api_key. If you’re using KV v1, it’s just mount_path/secret_path#key_name. The version: "v2" in the SecretStore is what tells ESO to expect the /data/ segment for KV v2.

One common pitfall is misunderstanding how refreshInterval interacts with Vault’s TTL. If a secret in Vault has a short TTL and is updated, ESO won’t pick up the change until its refreshInterval elapses or the ExternalSecret object is re-applied. It’s not a real-time synchronization mechanism in that sense; it’s a reconciliation loop.

The creationPolicy: Owner is a neat trick. If you delete the db-credentials secret manually, ESO will notice it’s missing and recreate it based on the ExternalSecret definition. This ensures your secret is always present as long as the ExternalSecret object exists and the SecretStore is valid.

If your ExternalSecret is not creating the Secret, the first place to look is the ExternalSecret’s status and events. kubectl describe externalsecret my-app-db-credentials -n default will show you detailed error messages from ESO. Common issues include incorrect SecretStore credentials (check the vault-approle-secret itself, and that the AppRole has read permissions on the Vault path), incorrect remoteKey paths, or network connectivity issues to the Vault server.

The next logical step is to explore how to manage multiple secrets and different secret types, such as certificates, using External Secrets Operator.

Want structured learning?

Take the full Flux course →