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.maxRetriesandretryIntervalfor provider HTTP calls. - Refresh interval: how often the store health check (
Validate()) runs. - Controller field: optional
spec.controllerto 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-managerinto 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:
- Validate SecretStore reference: ESO resolves
spec.secretStoreRef(or per-entrysourceRef) and checks that the store exists and itsspec.controllermatches this controller's class. With the flood gate on (default), ExternalSecrets whose store is unhealthy are skipped. - 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. - Fetch secret data:
GetSecretfor eachdata[]entry,GetSecretMapfordataFrom.extract,GetAllSecretsfordataFrom.find, or the generator forgeneratorRef, then appliesdecodingStrategyandrewriterules. - Apply template engine: if
spec.target.templateis set, ESO renders Go templates with Sprig and ESO helper functions against the fetched data. - Create or update Secret: the controller writes the final Secret according to
creationPolicy, and appliesdeletionPolicyif the provider reports keys as gone (NoSecretError). - Periodic re-sync: under
refreshPolicy: Periodicthe controller requeues afterrefreshInterval, optionally gated bysyncWindows;OnChangeandCreatedOnceonly 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 needindex. - Functions: 200+ Sprig functions (minus
envandexpandenv) 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,templateFroma ConfigMap or Secret, and amergePolicythat decides whether templated keys replace or merge with fetched keys. - Target shape:
template.typesets the Secret type (for examplekubernetes.io/tlsorkubernetes.io/dockerconfigjson), andtemplate.metadatasets 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
refreshIntervalcan throttle other consumers of the same account.externalsecret_provider_api_calls_countshows 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-cachingcaches 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.
OnChangeorCreatedOncefor values that rarely change, and longerrefreshIntervals, 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:
- Data exfiltration risk: a malicious PushSecret can push Secrets to an attacker-controlled provider. Restrict PushSecret and ClusterPushSecret creation with RBAC and admission policies.
- 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.
- Deletion policy:
deletionPolicy: Deleteremoves 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. - Update policy:
updatePolicy: IfNotExistsprevents 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.keyprefixes per namespace for shared stores. - Template validation: make sure ExternalSecret templates do not inject malicious content.
- Creation policy enforcement: prevent
creationPolicy: MergeorNonein 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
caBundleorcaProviderin the store configuration. - For Vault, use mutual TLS (
tls.certSecretRefandtls.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.conditionson ExternalSecret and SecretStore resources reflect the current state (Readywith reasons such asSecretSyncedandSecretSyncedError). - 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¶
- API Overview
- Components
- Advanced Templating v2
- Generators guide
- Threat Model and Security Best Practices
- AWS authentication
- Release process and multi-module versioning
- Provider interface source (
apis/externalsecrets/v1/provider.go) - Health of External Secrets project, issue #5084 and CNCF TOC health issue #1819
- Proposal: out of tree secret stores, PR #3634
- Introducing the external secrets operator for OpenShift (Red Hat Developer, 2025-11-11)