Skip to content

Explanation

What this page covers

How External Secrets Operator (ESO) works and why it is designed this way: the resource model, the three runtime components, the reconciliation loop, providers and generators, the template engine, the project's history and governance, scaling behavior, and the threat model. Exact field values, flags and version tables are in Reference; step-by-step recipes are in How-to Guides.

Overview

External Secrets Operator (ESO) is a Kubernetes operator that synchronizes secrets from external secret management systems into native Kubernetes Secret objects. It extends Kubernetes with Custom Resource Definitions (CRDs) that declare where secrets live and how to synchronize them. The controller fetches secrets from an external API and creates or updates Kubernetes Secrets accordingly. If the value changes in the external system, the next reconciliation updates the Secret.

ESO is a sync tool, not a secret store. It holds no secrets of its own and performs no encryption: the source of truth stays in the provider (Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, and about 40 others), and the result is an ordinary Kubernetes Secret that workloads consume via env, envFrom, or volume mounts. That design choice explains both its main strength (applications need no SDK or sidecar) and its main weakness (secret values land in etcd).

Project History and Governance

ESO grew out of GoDaddy's kubernetes-external-secrets (a Node.js project) and several similar operators, which merged into one Go project under the external-secrets GitHub organization. Key milestones:

Date Event
2022-07-26 Accepted into the CNCF Sandbox
2025-04-14 v0.16.0 promotes ExternalSecret, SecretStore and their cluster variants to external-secrets.io/v1; removes v1alpha1 and the v1 template engine
2025-05-14 v0.17.0 stops serving v1beta1
2025-07-30 Maintainers announce a release pause until more long-term maintainers join (issue #5084)
2025-08-13 CNCF TOC opens a project-health issue (cncf/toc#1819)
2025-09-22 Releases resume with v0.20.0 after a maintainer vote
2025-11-07 v1.0.0 GA; repository split into Go modules (/apis, /runtime, /providers/v1/*, /generators/v1/*)
2026-02-06 v2.0.0 removes unmaintained Alibaba and Device42 providers
2026-09-18 v2.11.0, current minor

The 2025 sustainability crisis

In mid-2025 ESO had effectively one quasi-full-time maintainer. On 2025-07-30 the maintainers paused all official releases, including security patches and image publishing, and set conditions for resuming: six consecutive community meetings with at least five members, reviewers or maintainers, contributors joining the contributor ladder, and newly elected permanent reviewers and maintainers. Community and corporate volunteers stepped in, maintainers voted to resume, and v0.20.0 shipped on 2025-09-22. Since v2.0 (February 2026) a new minor has shipped about every three weeks.

Governance is defined in GOVERNANCE.md: maintainers are elected by a supermajority, and votes from contributors at the same employer count as one. As of 2026-09 the maintainer list has five people with different affiliations (independent, External Secrets Inc, Form3, IBM, Kubermatic). External Secrets Inc is a commercial company founded around the project that sells enterprise secrets tooling; the operator itself stays Apache-2.0. Red Hat ships a supported "external secrets operator for Red Hat OpenShift", generally available since November 2025. The project has discussed applying for CNCF incubation, but as of 2026-09 it is still listed as Sandbox.

Component Architecture

A Helm install creates three Deployments from one binary (ghcr.io/external-secrets/external-secrets): the core controller, the admission webhook, and the cert controller. The diagram shows the reconcilers inside the core controller, the CRDs they watch, and the provider and generator calls.

flowchart LR
    subgraph NS["Namespace external-secrets"]
        subgraph CORE["Deployment external-secrets (core controller)"]
            ESR["ExternalSecret reconciler"]
            CESR["ClusterExternalSecret reconciler"]
            SSR["SecretStore / ClusterSecretStore reconciler"]
            PSR["PushSecret / ClusterPushSecret reconciler"]
            GEN["Generator runtime"]
        end
        WH["Deployment external-secrets-webhook<br/>(validating webhook, port 10250)"]
        CC["Deployment external-secrets-cert-controller"]
        TLS["Secret external-secrets-webhook<br/>(webhook TLS)"]
    end

    subgraph API["kube-apiserver"]
        ES["ExternalSecret"]
        CES["ClusterExternalSecret"]
        SS["SecretStore / ClusterSecretStore"]
        PS["PushSecret / ClusterPushSecret"]
        GR["Generators (generators.external-secrets.io)"]
        KS["Kubernetes Secret"]
        VWC["ValidatingWebhookConfiguration"]
    end

    subgraph PROV["Providers (in-process Go modules)"]
        AWS["AWS Secrets Manager / Parameter Store"]
        VLT["HashiCorp Vault / OpenBao"]
        GCP["GCP Secret Manager"]
        AZ["Azure Key Vault"]
        OTH["about 35 more (1Password, Doppler, Infisical, Webhook, ...)"]
    end

    CES -->|"stamps per namespace"| ES
    ESR -->|"watch"| ES
    ESR -->|"read"| SS
    ESR -->|"create / update"| KS
    ESR -->|"generatorRef"| GEN
    GEN -->|"read spec"| GR
    SSR -->|"Validate() health check"| SS
    PSR -->|"read source"| KS
    ESR -->|"GetSecret / GetSecretMap / GetAllSecrets"| PROV
    PSR -->|"PushSecret / DeleteSecret"| PROV
    CC -->|"writes cert"| TLS
    CC -->|"injects caBundle"| VWC
    TLS --> WH
    VWC -->|"admission review"| WH
Component Role Can be disabled
Core controller Reconciles all ESO CRDs, calls providers and generators, writes Secrets No (createOperator: false only for split installs)
Webhook Validates (Cluster)SecretStore and (Cluster)ExternalSecret beyond the OpenAPI schema; historically served CRD conversion webhook.create=false
Cert controller Generates the webhook's TLS certificate and injects caBundle into the ValidatingWebhookConfiguration (and CRDs when conversion is on) certController.create=false (bring your own certs, for example cert-manager)

The webhook is a correctness aid, not a security boundary: upstream's threat model states explicitly that its validation is not a security control.

Deployment Model

ESO is normally installed with the Helm chart from https://charts.external-secrets.io. The chart does not force a namespace; the upstream docs install into external-secrets with --create-namespace.

Namespace: external-secrets (chosen at install time)
  Deployment: external-secrets                 (core controller, replicaCount: 1)
  Deployment: external-secrets-webhook         (validating webhook)
  Deployment: external-secrets-cert-controller (webhook TLS bootstrap)
  ServiceAccounts: one per component, bound to ClusterRoles
  CRDs: ExternalSecret, ClusterExternalSecret, SecretStore, ClusterSecretStore,
        PushSecret, ClusterPushSecret, generator kinds, ClusterGenerator, GeneratorState

For high availability, increase replicaCount and set leaderElect: true: leader election is off by default, so several replicas without it would reconcile the same objects concurrently. With leader election, only one replica reconciles at a time; the others are hot standbys. Throughput therefore scales with concurrent (parallel reconciles in the leader), not with replica count.

Core CRDs

SecretStore

A namespaced resource that defines how to access an external secret provider. It separates the concern of authentication from the secret data itself. A SecretStore contains:

  • Provider configuration: exactly one provider block (aws, vault, gcpsm, azurekv, and more) with its connection parameters.
  • Authentication: references to Kubernetes Secrets or ServiceAccounts used to obtain provider credentials (static keys, tokens, workload identity).
  • Retry settings: optional retrySettings.maxRetries and retryInterval for provider HTTP calls.
  • Refresh interval: how often the store health check (Validate()) runs.
  • Controller field: optional spec.controller to target a specific ESO controller instance when running several.

Namespaced stores may only reference Secrets in their own namespace, which gives natural tenant isolation.

ClusterSecretStore

A cluster-scoped SecretStore that serves as a centralized gateway to a secret provider. Any ExternalSecret in any namespace can reference it. Cluster operators typically manage ClusterSecretStores to provide shared access to organizational secret backends.

The conditions field on ClusterSecretStore restricts which namespaces may use it (by explicit list, label selector, or regex). Without conditions, every namespace can read everything the store's credentials can read, which is the most common multi-tenancy mistake with ESO.

ExternalSecret

A namespaced resource that declares what data to fetch and how to materialize it as a Kubernetes Secret. It points at a store (spec.secretStoreRef or per-entry sourceRef), maps remote keys to Secret keys (data[]) or pulls whole secrets and searches (dataFrom[] with extract, find, or a generator), and optionally renders a template. refreshPolicy and refreshInterval (default 1h) decide when to re-fetch; creationPolicy and deletionPolicy decide who owns the resulting Secret and what happens when provider data disappears. See the field and policy tables.

PushSecret

Reverses the normal flow: pushes data from a Kubernetes Secret (or a generator) out to one or more providers. Use cases include:

  • Syncing certificates generated by cert-manager into Vault or AWS Secrets Manager.
  • Distributing auto-generated credentials to external systems.
  • Propagating Secrets across clusters through a shared provider.

PushSecret supports updatePolicy (Replace or IfNotExists) and deletionPolicy (None or Delete) to control how provider-side secrets are managed. PushSecret is still an v1alpha1 API, and only some providers implement the write path (see the provider table).

ClusterExternalSecret

Distributes ExternalSecret resources into selected namespaces across the cluster. The controller stamps a copy of the ExternalSecret template into each namespace matching namespaceSelectors. This enables bulk distribution of secrets (for example, a registry pull secret) with a single resource.

ClusterPushSecret

The push-direction counterpart of ClusterExternalSecret: a cluster-scoped resource that creates PushSecret objects in namespaces selected by namespaceSelectors. It errors out if a PushSecret with the same name already exists in a target namespace.

Generators

Generators produce values instead of fetching them: registry tokens (ECR, ACR, GCR, Quay, Cloudsmith), cloud session tokens (AWS STS), Vault dynamic secrets, GitHub App and GitLab deploy tokens, Grafana service-account tokens, passwords, SSH keys, UUIDs, TOTP codes, and arbitrary webhooks. An ExternalSecret references one through dataFrom[].sourceRef.generatorRef; every refresh produces a new value, which suits short-lived registry tokens but means a generated password rotates on every refreshInterval unless the policy is CreatedOnce. ClusterGenerator makes a generator spec discoverable cluster-wide, but the output still lands only in the referencing ExternalSecret's namespace. GeneratorState records generated values that need cleanup (for example, revoking a Grafana token) once they are superseded.

Secret Sync Flow

This sequence shows a single ExternalSecret reconcile against a Vault-backed SecretStore using Kubernetes auth.

sequenceDiagram
    participant Dev as Developer or GitOps tool
    participant K8s as kube-apiserver
    participant WH as ESO webhook
    participant Ctrl as ESO core controller
    participant Vault as HashiCorp Vault

    Dev->>K8s: Apply ExternalSecret (external-secrets.io/v1)
    K8s->>WH: ValidatingAdmissionReview
    WH-->>K8s: Allowed
    K8s-->>Ctrl: Watch event triggers reconcile
    Ctrl->>K8s: Get referenced SecretStore
    Ctrl->>Ctrl: Check spec.controller class and store health (flood gate)
    Ctrl->>K8s: TokenRequest for the store ServiceAccount
    Ctrl->>Vault: POST auth/kubernetes/login with JWT
    Vault-->>Ctrl: Vault token
    Ctrl->>Vault: GET secret/data/myapp (KV v2)
    Vault-->>Ctrl: Secret payload
    Ctrl->>Ctrl: Decode, rewrite keys, render template (engine v2)
    Ctrl->>K8s: Create or update Secret per creationPolicy
    Ctrl->>K8s: Patch status condition Ready=True reason SecretSynced
    Note over Ctrl: Requeue after refreshInterval (Periodic policy)

Reconciliation Loop

The controller follows a structured reconciliation process:

  1. Validate SecretStore reference: ESO resolves spec.secretStoreRef (or per-entry sourceRef) and checks that the store exists and its spec.controller matches this controller's class. With the flood gate on (default), ExternalSecrets whose store is unhealthy are skipped.
  2. Instantiate provider client: using the store's credentials, ESO calls the provider's NewClient(); for namespaced auth it reads referenced Secrets or requests ServiceAccount tokens.
  3. Fetch secret data: GetSecret for each data[] entry, GetSecretMap for dataFrom.extract, GetAllSecrets for dataFrom.find, or the generator for generatorRef, then applies decodingStrategy and rewrite rules.
  4. Apply template engine: if spec.target.template is set, ESO renders Go templates with Sprig and ESO helper functions against the fetched data.
  5. Create or update Secret: the controller writes the final Secret according to creationPolicy, and applies deletionPolicy if the provider reports keys as gone (NoSecretError).
  6. Periodic re-sync: under refreshPolicy: Periodic the controller requeues after refreshInterval, optionally gated by syncWindows; OnChange and CreatedOnce only react to spec changes.

Sync Lifecycle

The state diagram shows the Ready condition reasons an ExternalSecret moves through.

stateDiagram-v2
    [*] --> Pending: ExternalSecret created
    Pending --> SecretSynced: First successful sync
    Pending --> SecretSyncedError: Store not ready, auth or fetch failure
    SecretSynced --> SecretSynced: refreshInterval tick, values unchanged or updated
    SecretSynced --> SecretSyncedError: Provider unreachable or auth failure
    SecretSyncedError --> SecretSynced: Retry succeeds
    SecretSyncedError --> SecretSyncedError: Requeue with backoff
    SecretSynced --> SecretMissing: Target Secret deleted out of band
    SecretMissing --> SecretSynced: Recreated on next reconcile
    SecretSynced --> SecretDeleted: Provider keys gone and deletionPolicy Delete
    SecretSynced --> [*]: ExternalSecret deleted, creationPolicy decides Secret cleanup

When the provider is unavailable, the existing Kubernetes Secret is left in place and workloads keep running on the last synced value; only the status condition and the externalsecret_sync_calls_error metric change.

Ownership Policies

The target.creationPolicy controls who owns the underlying Kubernetes Secret. Owner (default) sets an ownerReference, so deleting the ExternalSecret garbage-collects the Secret. Orphan writes the Secret without an owner, so it survives the ExternalSecret. Merge and CreateOrMerge add ESO-managed keys to a Secret that something else owns, which is how ESO co-exists with Helm-created Secrets. None writes nothing. Two ExternalSecrets with Owner on the same target Secret conflict and surface SecretOwnedByOther. The full table is in Reference.

Provider Plugin Architecture

Providers are Go packages compiled into the ESO binary and registered at start-up; since v1.0 each lives in its own Go module under providers/v1/<name>, all versioned with the same tag. There is no out-of-process plugin mechanism in the released code. A proposal for out-of-tree providers over gRPC (PR #3634) was closed unmerged as stale on 2024-12-27, and the later tracking issue #5218 was closed as not planned (checked 2026-09-28).

Every provider implements two interfaces from apis/externalsecrets/v1:

Provider:
  NewClient(ctx, store, kubeClient, namespace) -> SecretsClient
  ValidateStore(store) -> (warnings, error)
  Capabilities() -> ReadOnly | WriteOnly | ReadWrite

SecretsClient:
  GetSecret(ctx, ExternalSecretDataRemoteRef) -> []byte
  GetSecretMap(ctx, ExternalSecretDataRemoteRef) -> map[string][]byte
  GetAllSecrets(ctx, ExternalSecretFind) -> map[string][]byte
  PushSecret(ctx, *corev1.Secret, PushSecretData) -> error
  DeleteSecret(ctx, PushSecretRemoteRef) -> error
  SecretExists(ctx, PushSecretRemoteRef) -> bool
  Validate() -> ValidationResult (Ready | Unknown | Error)
  Close(ctx) -> error

The controller picks the implementation from the single provider block in the store spec, so new providers need no change to the reconcile loop. A NoSecretError from GetSecret is what drives deletionPolicy.

Provider quality varies widely. Only about ten providers are stable (AWS Secrets Manager and Parameter Store, Azure Key Vault, GCP Secret Manager, HashiCorp Vault, Akeyless, CyberArk, IBM, Oracle, Previder); the rest are alpha and maintained by individual contributors or vendors. Stores whose provider has no explicit maintainer produce admission warnings and controller warning events. Unmaintained providers get removed: v2.0.0 dropped Alibaba Cloud and Device42.

SecretStore Authentication Chain

Each SecretStore defines a provider and an auth method. The controller resolves credentials before calling the provider:

Provider Auth method Mechanism
AWS Secrets Manager / Parameter Store Controller pod identity ESO's own ServiceAccount (IRSA or EKS Pod Identity) supplies credentials through the AWS SDK default chain; optional role to assume
AWS Secrets Manager / Parameter Store auth.jwt.serviceAccountRef (IRSA) ESO requests a token for the referenced ServiceAccount and calls sts:AssumeRoleWithWebIdentity; cannot be combined with EKS Pod Identity
AWS Secrets Manager / Parameter Store auth.secretRef Static access key ID and secret access key from a Kubernetes Secret
HashiCorp Vault / OpenBao Kubernetes ServiceAccount JWT sent to auth/kubernetes/login
HashiCorp Vault AppRole, JWT/OIDC, cert, userpass, LDAP, IAM, token Role ID plus Secret ID, projected JWT, TLS client cert, or static token
GCP Secret Manager Workload Identity / Workload Identity Federation Kubernetes ServiceAccount token exchanged for GCP credentials
Azure Key Vault WorkloadIdentity, ManagedIdentity, ServicePrincipal Federated ServiceAccount token, managed identity (including legacy AAD Pod Identity), or client ID and secret

For ClusterSecretStore, references to credential Secrets or ServiceAccounts normally carry an explicit namespace. Providers that support referent authentication let you omit it, in which case ESO resolves the credential in the namespace of the ExternalSecret that uses the store; this lets one store definition serve many tenants with per-namespace credentials.

Template Engine

ESO can transform secret data before writing the Kubernetes Secret, using Go templates in spec.target.template (and the same engine in PushSecret.spec.template). The current engine is v2, the only one since v0.16.0:

  • Variable substitution: {{ .secretKey }} references fetched values; keys with dashes need index.
  • Functions: 200+ Sprig functions (minus env and expandenv) plus ESO helpers for PKCS#12, PEM filtering, certificate SANs, JWK conversion, RSA decryption, and YAML.
  • Control structures: if, range, with, and pipelines.
  • Template sources: inline template.data, templateFrom a ConfigMap or Secret, and a mergePolicy that decides whether templated keys replace or merge with fetched keys.
  • Target shape: template.type sets the Secret type (for example kubernetes.io/tls or kubernetes.io/dockerconfigjson), and template.metadata sets labels, annotations and finalizers.

Templates run inside the controller with the controller's privileges, which makes the function set a security boundary: getSecretKey (cross-namespace reads, CVE-2026-22822) and getHostByName (DNS exfiltration, CVE-2026-34984) were both removed after advisories. A worked TLS example is in How-to Guides.

PushSecret Flow (Reverse Sync)

PushSecret reads a Kubernetes Secret (or generator output), optionally templates it, and writes it to every matching store.

flowchart LR
    CM["cert-manager Certificate"] -->|"issues"| K8sS["Secret app-tls"]
    K8sS -->|"selector.secret"| PS["PushSecret (v1alpha1)"]
    GEN["Password generator"] -.->|"selector.generatorRef"| PS
    PS --> CTRL["ESO core controller"]
    CTRL -->|"PushSecret()<br/>updatePolicy Replace or IfNotExists"| V["Vault KV v2"]
    CTRL -->|"PushSecret()"| AWS["AWS Secrets Manager"]
    CTRL -->|"DeleteSecret() when deletionPolicy Delete"| V

Use cases include writing TLS certificates generated by cert-manager to Vault, pushing generated credentials to an external store, and synchronizing secrets across clusters via a shared provider.

Multi-Controller Deployment

ESO supports running multiple controller instances within a single cluster. Each controller runs with a --controller-class and processes only SecretStore/ClusterSecretStore resources whose spec.controller matches (and the ExternalSecrets that reference them). This enables:

  • Separation of tenant workloads.
  • Different provider credentials and network egress per controller.
  • Gradual rollout of controller upgrades.

Multi-controller maturity

Upstream still labels running multiple controllers as "not widely tested". Test thoroughly before relying on this pattern for critical workloads. A namespace-scoped install (scopedNamespace plus scopedRBAC) is the better-trodden path to isolation.

Roles and Responsibilities

Role Responsibility
Cluster Operator Installs ESO, manages ClusterSecretStores, defines access policies
Application Developer Defines ExternalSecrets and application configuration

Each role maps roughly to a Kubernetes RBAC role. ESO itself runs with elevated privileges to create and manage Secrets across all namespaces. Upstream notes that ESO does not manage the lifecycle of secrets in the provider (rotation, expiry); that stays with the provider or with generators.

Performance and Scaling

ESO is poll-based. The load it generates is roughly number of ExternalSecrets x data entries / refreshInterval provider calls, plus one Validate() per store every --store-requeue-interval (5 minutes by default). What matters in practice:

  • Provider quotas, not ESO CPU, are usually the limit. AWS Secrets Manager, Azure Key Vault and GCP Secret Manager all enforce per-account or per-vault request quotas. Many ExternalSecrets on a short refreshInterval can throttle other consumers of the same account. externalsecret_provider_api_calls_count shows the call rate per provider.
  • Concurrency is per leader. --concurrent (default 1) is the number of parallel reconciles; more replicas only add standbys under leader election.
  • Memory follows the informer caches. By default only ESO-managed Secrets are cached; --enable-secrets-caching caches every Secret in the cluster and can raise memory sharply.
  • The flood gate protects providers. When a store is unhealthy, dependent ExternalSecrets are skipped instead of hammering a failing endpoint.
  • Refresh policy is a scaling lever. OnChange or CreatedOnce for values that rarely change, and longer refreshIntervals, cut provider traffic sharply.

Benchmarks

Upstream publishes no controlled benchmarks. The rough, unsourced estimates previously kept here are now in Reference, clearly labelled.

Threat Model

ESO operates with elevated privileges inside the Kubernetes cluster. The core controller can create, read, update and delete Secrets in all namespaces, and holds or can obtain provider credentials. Upstream's threat model rates the data as critical and the worst-case impact as "organisation takeover". It names four assets (cluster-level Secret access, CRD and webhook write access, provider access, the ability to modify resources) and seven threats.

Threat vector Impact Mitigation
Compromised ESO pod or workload in the ESO namespace Read access to all synced Secrets and provider credentials NetworkPolicies, Pod Security Standards, dedicated namespace, no other workloads there
Leaked provider credentials Access to the external secret store Short-lived tokens, IRSA, Pod Identity, Workload Identity
Malicious ExternalSecret Exfiltrate provider secrets into an attacker-controlled namespace RBAC on ExternalSecret creation, ClusterSecretStore conditions, OPA/Kyverno policies
Malicious SecretStore Route secret fetching through an attacker-controlled endpoint, or reuse powerful credentials Restrict SecretStore creation, validate url, role, caBundle and similar fields by policy
PushSecret abuse Push cluster Secrets to an attacker-controlled external store Restrict PushSecret RBAC, audit PushSecret creation
Privilege escalation via ClusterSecretStore Cross-namespace secret access Namespace conditions on every ClusterSecretStore
Template abuse Cross-namespace reads or DNS exfiltration via template functions Stay on the current minor (both known template CVEs are fixed), restrict who can write templates
Webhook MITM or DoS Tampered admission decisions, blocked resource creation mTLS between API server and webhook, webhook NetworkPolicy
Supply-chain attack Malicious image or chart Verify Cosign signatures, SBOM and provenance

Past advisories show where this surface actually broke: an over-privileged cert-controller ClusterRole (CVE-2024-45041), a PushSecret controller listing Secrets without a namespace filter (CVE-2025-55196), and two template-function escapes (CVE-2026-22822, CVE-2026-34984). Details are in Reference.

RBAC Model

Cluster Operator Permissions

Cluster operators manage the ESO deployment and ClusterSecretStores. They require broad permissions:

  • external-secrets.io/clustersecretstores: CRUD at cluster scope.
  • external-secrets.io/externalsecrets: read and list across namespaces for troubleshooting.
  • Core Kubernetes secrets: read and write across namespaces, delegated to the ESO ServiceAccount.

Application Developer Permissions

Application developers define ExternalSecrets within their namespaces and reference pre-approved stores; they should not create SecretStores or PushSecrets. If developers can create SecretStore resources, they can define arbitrary provider connections and reuse any credentials in their namespace. A ready-to-apply Role is in How-to Guides.

ESO Controller ServiceAccount

The Helm chart creates, per component, a ClusterRole and ClusterRoleBinding: the core controller may manage Secrets in all namespaces and all ESO CRDs, and by default may create ServiceAccount tokens for any ServiceAccount (needed for workload-identity auth). The cert controller may patch ValidatingWebhookConfigurations and CustomResourceDefinitions to inject caBundle.

Narrow this by setting rbac.serviceAccountTokenCreate: false (and granting token creation per ServiceAccount with resourceNames), by a namespace-scoped install (scopedRBAC: true), or by running several controllers with different ServiceAccounts.

Provider Credential Security

The recurring theme across providers is to replace long-lived static credentials with identity federation, so that nothing reusable is stored in a Kubernetes Secret. Configuration recipes are in How-to Guides.

AWS (IRSA / Pod Identity)

With IAM Roles for Service Accounts, a ServiceAccount annotated with an IAM role ARN is exchanged for temporary STS credentials. With EKS Pod Identity, the EKS Pod Identity Agent supplies credentials to the ESO pod itself; serviceAccountRef impersonation cannot be combined with Pod Identity. Either way, no static accessKeyID or secretAccessKey is stored in Kubernetes.

GCP (Workload Identity)

For GCP Secret Manager, Workload Identity (or Workload Identity Federation outside GKE) binds a Kubernetes ServiceAccount to a GCP identity, so ESO exchanges a projected token for short-lived GCP credentials.

HashiCorp Vault (Kubernetes Auth)

With Vault's Kubernetes auth method, ESO sends a ServiceAccount JWT to auth/kubernetes/login and gets a Vault token bound to a narrowly scoped Vault policy. --enable-vault-token-cache reuses tokens instead of logging in on every request.

Static token risks

Do not use tokenSecretRef with static Vault tokens in production. These tokens never expire unless manually revoked. Prefer the Kubernetes auth method or AppRole with periodic secret IDs.

Namespace Isolation

SecretStore (Namespaced)

A SecretStore is namespaced and can only be referenced by ExternalSecret resources in the same namespace. Credentials referenced by the SecretStore (via secretRef) must also exist in the same namespace. The upstream rules are explicit: ExternalSecrets must not reference SecretStores or Secrets across namespaces, and SecretStores must not reference Secrets in other namespaces.

ClusterSecretStore (Cluster-Scoped)

A ClusterSecretStore can be referenced by ExternalSecret resources in any namespace. Restrict it with spec.conditions (namespaces, namespaceSelector, or namespaceRegexes); an example is in How-to Guides. If cluster-wide kinds are not needed at all, disable them in the chart.

Cross-Namespace References

Some ClusterSecretStore fields reference Kubernetes Secrets in other namespaces (for example caProvider.namespace or auth secretRef.namespace). In multi-tenant environments, audit these references carefully to prevent privilege escalation.

PushSecret Security Considerations

PushSecret reverses the normal data flow by pushing Kubernetes Secrets to external providers. This introduces additional security concerns:

  1. Data exfiltration risk: a malicious PushSecret can push Secrets to an attacker-controlled provider. Restrict PushSecret and ClusterPushSecret creation with RBAC and admission policies.
  2. Provider write permissions: the store referenced by a PushSecret needs write access to the provider. Grant the minimum required, and use separate read-only stores for ExternalSecrets.
  3. Deletion policy: deletionPolicy: Delete removes secrets from the provider when the PushSecret is deleted. Make sure this does not cause unintended data loss. Since v0.20, stores referenced by such PushSecrets get finalizers so the delete can complete.
  4. Update policy: updatePolicy: IfNotExists prevents overwriting secrets that already exist in the provider.

Admission Control Integration

ESO's own webhook only validates structure, so use a policy engine (OPA Gatekeeper, Kyverno) to enforce rules on ESO resources:

  • Allowed providers: restrict which provider types and endpoints can appear in SecretStores.
  • Allowed namespaces: restrict which namespaces can create ExternalSecrets or PushSecrets.
  • Remote key scoping: restrict remoteRef.key prefixes per namespace for shared stores.
  • Template validation: make sure ExternalSecret templates do not inject malicious content.
  • Creation policy enforcement: prevent creationPolicy: Merge or None in sensitive namespaces.

Network Security

  • Deploy ESO in a dedicated namespace with strict NetworkPolicies.
  • Restrict egress from the core controller to the kube-apiserver and the required provider endpoints; the webhook and cert controller only need the kube-apiserver.
  • Use TLS for all provider connections. Verify certificates with caBundle or caProvider in the store configuration.
  • For Vault, use mutual TLS (tls.certSecretRef and tls.keySecretRef) when the Vault server requires client certificates.
  • Prefer private endpoints (VPC endpoints, Private Service Connect, Private Link) to providers.

Audit and Observability

  • ESO emits Kubernetes events for reconciliation successes and failures. Monitor these events for anomalous patterns.
  • The status.conditions on ExternalSecret and SecretStore resources reflect the current state (Ready with reasons such as SecretSynced and SecretSyncedError).
  • Prometheus metrics (externalsecret_*, secretstore_*, controller-runtime) and an optional bundled Grafana dashboard cover sync errors, reconcile latency and provider call rates.
  • At --loglevel=debug, ESO logs Secret deletions and key-level diffs (key names only, never values), which is useful for alerting on destructive changes.
  • Log all changes to ESO CRDs via Kubernetes audit logging, and rely on the provider's audit log (Vault audit devices, AWS CloudTrail) for who read what.

Sources