External Secrets Operator (ESO) holds credentials to your secrets manager and can write Kubernetes Secrets in every namespace, so a weak install turns one cluster foothold into access to the whole vault. ESO is the common way to keep secret values out of Git: manifests carry an ExternalSecret reference and the controller fetches the value from AWS Secrets Manager, Vault, Azure Key Vault, Google Secret Manager or another provider. That convenience concentrates privilege in three places: the controller's Kubernetes permissions, the provider credential behind each store, and the people who can create ESO custom resources. This guide covers the ESO threat model, store design for multi-tenant clusters, RBAC and Helm settings, sync behaviour that affects rotation, detection and a phased rollout. It is written for platform and Kubernetes security engineers. For the wider picture read the Kubernetes Secrets Security Guide and the GitOps Security Guide first.
Key takeaways
- The controller is a cluster-wide secret writer. ESO's own threat model lists cluster-level access to secrets and secret provider access as its top assets, so protect the
external-secretsnamespace like a control plane component. - Store design is your real access boundary. A shared
ClusterSecretStorelets any namespace that can reference it read every key behind it; aSecretStoreper namespace with its own provider role moves enforcement into the secrets manager. - Turn off what you do not use. Cluster stores, cluster external secrets and
PushSecretcan each be disabled by controller flag and Helm value, which removes whole attack paths. - Webhook validation is not a security control. The project says so explicitly; tenant restrictions belong in RBAC, store conditions and an admission policy engine such as Kyverno or OPA Gatekeeper.
- Sync settings decide rotation and blast radius.
refreshPolicy,creationPolicy,deletionPolicyandimmutablecontrol whether a rotated value reaches pods, and whether a deleted resource takes the Secret with it.
How ESO works and where the trust sits
ESO runs three components. The core controller reconciles ExternalSecret, SecretStore, ClusterSecretStore, ClusterExternalSecret and PushSecret resources, calls the provider and writes Kubernetes Secrets. The webhook validates and converts ESO resources. The cert controller manages the caBundle on the webhook configuration and CRDs. The resulting Kubernetes Secret is still only base64 encoded in etcd, so ESO does not remove the need for encryption at rest and tight get/list on Secrets.
ESO's published threat model names the assets and threats directly. It assumes a Helm install with defaults on a public cloud and notes that provider credentials may be broad enough to give full cloud access.
| ESO threat (project ID) | What it means in practice | Primary controls |
|---|---|---|
| T01 Tampering through MITM | Traffic between the API server and webhook can be altered; the project notes mutual authentication is not enforced there | NetworkPolicy, restricted webhook TLS ciphers |
| T02 Webhook denial of service | A flooded webhook blocks changes to ESO resources | Ingress only from the API server on 10250 |
| T03 Unauthorised access to cluster secrets | Anyone who can exec into or impersonate the controller reads every Secret | RBAC on pods/exec, scoped install |
| T04 Access to provider credentials | Static keys in a store secretRef or a broad IAM role unlock the vault | Workload identity, per-store roles, provider policy |
| T05 Exfiltration through malicious resources | A tenant writes an ExternalSecret or PushSecret that moves data where they can read it | Admission policy, disabled CRDs, egress limits |
| T06 Supply chain attacks | A tampered image or chart runs with the controller's privileges | Cosign verification, SLSA provenance, SBOM |
| T07 Malicious workloads in the ESO namespace | Any pod there can borrow the controller's identity and tokens | Dedicated namespace, Pod Security Admission, no other workloads |
The controls the project names are C01 NetworkPolicy, C02 least privilege RBAC, C03 policy enforcement, C04 provider access policy (for example VPC endpoint or Private Link policies) and C05 disabling unneeded CRDs. ESO ships no NetworkPolicies and no admission policies; both are your job.
Choosing a store model for tenants
ESO's multi-tenancy guide describes two patterns. Most real clusters mix them, but the choice of default matters more than any other setting.
| Aspect | Shared ClusterSecretStore | SecretStore per namespace |
|---|---|---|
| Who creates stores | Cluster administrators | Cluster or secret administrators, per namespace |
| Access boundary | Store conditions plus admission policy; ESO cannot restrict keys per namespace itself | Provider role behind each store (IAM policy, Vault policy, Key Vault RBAC) |
| Blast radius of one tenant | Every key the store's credential can read | Only that namespace's keys |
| Operational load | Low | Higher: one provider role per store |
| Choose when | Single team cluster, or a store that serves only non-sensitive shared config | Multi-team or regulated clusters; a separate secret owner manages access |
- Restrict every ClusterSecretStore: use
spec.conditionswithnamespaceSelector,namespacesornamespaceRegexes. Conditions are OR-ed, so a store is usable in a namespace that meets any one of them; one loose regex or a label tenants can set themselves widens access. Pair label-based conditions with a policy that stops tenants editing namespace labels. - No cross-namespace references: the security guide says
SecretStoreandExternalSecretmust not carry cross-namespace references toSecretorSecretStorekinds. Enforce it with admission policy. - One provider role per store: the AWS provider docs recommend a separate role per
SecretStoreand a name prefix (for exampleteam-a/*) in the resource ARN. - Use the controller field for isolation: the
controllerfield on stores selects which ESO instance reconciles them, similar to an ingress class, so a high-trust store can be served by a separate, scoped deployment.
Hardening the install: RBAC, Helm values and network
| Setting | Where | Effect |
|---|---|---|
rbac.serviceAccountTokenCreate: false | Helm | Removes the broad serviceaccounts/token: create grant; add a per-service-account Role with resourceNames for stores that use Kubernetes service account auth |
scopedRBAC: true, scopedNamespace | Helm | Runs ESO against one namespace; implicitly disables cluster stores and cluster external secrets |
--enable-cluster-store-reconciler=false / processClusterStore: false | Flag / Helm | Stops reconciling ClusterSecretStores |
--enable-cluster-external-secret-reconciler=false / processClusterExternalSecret: false | Flag / Helm | Stops ClusterExternalSecrets fanning secrets out across namespaces |
--enable-push-secret-reconciler=false / processPushSecret: false | Flag / Helm | Stops writing back to the provider |
crds.createClusterSecretStore, crds.createPushSecret (and peers) false | Helm | Removes the CRD; disable the matching reconciler too, or the controller logs errors |
webhook.extraArgs.tls-ciphers | Helm | Restricts webhook TLS suites |
- Kubernetes RBAC for people: limit
createandupdateonClusterSecretStoreandClusterExternalSecretto the platform team, keepSecretStorecreation with whoever owns provider roles, denypods/execin the ESO namespace, and review what the aggregatedview,editandadminClusterRoles grant on ESO kinds. See the Kubernetes RBAC Guide. - NetworkPolicy: default deny in the ESO namespace. The core controller needs egress to the API server and the provider (private endpoints preferred) and ingress on 8080 from monitoring. The webhook needs ingress on 10250 from the API server, 8080 from monitoring and 8081 from the kubelet. The cert controller needs ingress on 8080 and 8081.
- Pod security: ESO pods meet the
restrictedPod Security Standards profile by default since v0.8.2; label the namespacepod-security.kubernetes.io/enforce: restrictedso nothing weaker can run beside them. - Supply chain: verify images by digest with
cosign verifyagainst the GitHub Actions OIDC issuer and the project'srelease.ymlworkflow identity, check SLSA provenance and pull the SPDX SBOM attestation. Only the current minor release is supported, so plan upgrades rather than pinning for years.
Where identity fits: the store's provider credential
Every store authenticates to the provider as a non-human identity, and that credential is usually more powerful than anything else in the namespace. Prefer the provider's workload identity (IRSA or EKS Pod Identity on AWS, Workload Identity on GKE and AKS, Kubernetes auth on Vault) over static keys in auth.secretRef, because a static key in a Kubernetes Secret is exactly what an attacker with cluster read access takes first. The Tesla Kubernetes cryptojacking incident started with long-lived AWS credentials readable inside a pod.
- Least privilege on the provider: grant
secretsmanager:GetSecretValue,DescribeSecretandListSecretVersionIdson a name prefix; note that the documented example grantsListSecretsandBatchGetSecretValueon*, so check what that exposes in your account. - Write access only for PushSecret stores:
CreateSecret,PutSecretValueandDeleteSecretbelong on a separate role, withDeleteSecretconditioned on the tagmanaged-by=external-secretsas the AWS provider docs suggest. - Provider-side network policy: restrict the role or vault to the cluster's VPC endpoint or private link so a stolen token is useless off-network.
See the Kubernetes NHI Security Guide and the Secrets Zero Problem Guide for the bootstrap question.
Sync behaviour, rotation and deletion
| Field | Values (default first) | Security consequence |
|---|---|---|
spec.refreshPolicy | Periodic, OnChange, CreatedOnce | Only Periodic picks up a value rotated in the provider; the other two leave the old value in the cluster |
spec.refreshInterval | Duration, for example 1h; 0 syncs once | Sets the worst-case lag between rotating in the vault and pods seeing it |
spec.target.creationPolicy | Owner, Orphan, Merge, None | Owner deletes the Secret with the ExternalSecret; Orphan leaves copies behind that nobody tracks |
spec.target.deletionPolicy | Retain, Delete, Merge | With Retain, deleting a secret in the provider does not remove it from the cluster |
spec.target.immutable | true or unset | The only setting that stops an existing Secret being overwritten when the ExternalSecret is recreated |
PushSecret deletionPolicy | None, Delete | None leaves provider secrets in place after the PushSecret is deleted |
- Rotation needs a reload: a refreshed Secret does not restart pods that read it at start-up as environment variables. Pair rotation with a rollout trigger or mount secrets as volumes and have the application reload.
- Treat
dataFrom.findas broad access: a regexp or tag search pulls every matching key into one Secret. Restrict.spec.data[].remoteRef.keyanddataFromto an approved prefix per namespace with admission policy. - Revocation means deleting in two places: on an incident, rotate in the provider, then confirm the cluster copy changed, or delete it, because
Retainkeeps the old value.
Detection and measurement
- ESO metrics: alert on
externalsecret_sync_calls_erroras a ratio ofexternalsecret_sync_calls_total, onexternalsecret_status_conditionandsecretstore_status_conditionnot Ready, oncontroller_runtime_reconcile_total{result="error"}, and onworkqueue_depthstaying above zero.externalsecret_provider_api_calls_countbyprovider,callandstatusshows denied calls. - Kubernetes audit log: record create and update on all ESO kinds,
pods/execinto the ESO namespace, and any access to Secrets by identities other than the ESO controller and the workloads that own them. See the Kubernetes Audit Log Detection Guide. - Provider logs: CloudTrail, Vault audit or Key Vault diagnostics should show reads only from the expected roles, from the cluster network, at roughly the refresh cadence. A spike in
ListSecretsor reads of keys outside a store's prefix is a strong signal. See Secrets Access Logging and Detection.
Metrics that show the programme is working:
- Percentage of stores using workload identity rather than a static
secretRef(target 100%). - Number of ClusterSecretStores without
spec.conditions(target 0). - Sync error rate over 24 hours, and median time from provider rotation to cluster update.
- Count of orphaned Secrets: labelled as ESO-managed but with no owning ExternalSecret.
- Admission policy violations per week by namespace, trending down after onboarding.
Phased rollout
- Inventory: list every store, its auth method and provider role, every ClusterSecretStore's conditions, and which CRDs and reconcilers are enabled.
- Disable unused features: switch off PushSecret, cluster external secrets and cluster stores if nothing uses them; disable the reconciler and the CRD together. Test in a non-production cluster first.
- Move to workload identity: replace static provider keys store by store, then revoke the keys and delete the Kubernetes Secrets that held them.
- Admission policy in audit mode: deploy Kyverno or Gatekeeper rules for allowed providers, key prefixes per namespace and permitted store references in
Audit, review violations for two weeks, then switch toEnforce. - Network and pod security: apply default deny NetworkPolicies and the restricted Pod Security label, watching sync metrics for breakage.
- Rollback: keep the previous Helm values in Git; policy rules can be returned to
Auditand NetworkPolicies removed without touching synced Secrets, which stay in place under the defaultRetain.
Framework mapping
| Control area | NIST CSF 2.0 | NIST SP 800-53 r5 | MITRE ATT&CK |
|---|---|---|---|
| Least privilege for controller, users and provider roles | PR.AA-05 | AC-6 | T1552.007 Container API |
| Provider credential management and rotation | PR.AA-01 | IA-5, SC-12 | T1552.001 Credentials In Files |
| Disable unused CRDs and reconcilers | PR.PS-01 | CM-7, CM-6 | T1613 Container and Resource Discovery |
| Image signature and provenance checks | PR.PS-01 | SR-4, SR-11 | None specific |
| Secrets at rest and in transit | PR.DS-01, PR.DS-02 | SC-28, SC-8 | None specific |
| Sync and access monitoring | DE.CM-09 | AU-6 | T1609 Container Administration Command |
Common failure patterns
- One ClusterSecretStore with an admin-grade role: any team that can create an ExternalSecret can name any key. Split by namespace or add conditions and a key-prefix policy.
- Static cloud keys bootstrapping the store: the key sits in a Kubernetes Secret, often never rotated, and is readable by anyone with Secret read in that namespace.
- Extra workloads in the ESO namespace: debugging pods or sidecars there can reach the controller's service account and token.
- Assuming rotation is automatic:
CreatedOnceorOnChangestores, or pods that only read environment variables at start-up, keep the old credential long after the vault rotated it. - Secrets still committed beside the ExternalSecret: teams keep the old base64 Secret manifest in the repository. The exposed Kubernetes Secrets case involving SAP shows how long such files stay valid on public GitHub.
Practitioner checklist
- Run ESO in a dedicated namespace with the
restrictedPod Security label and nothing else deployed there. - Set
rbac.serviceAccountTokenCreate: falseand denypods/execin the ESO namespace. - Disable the PushSecret, ClusterExternalSecret and ClusterSecretStore reconcilers and CRDs you do not use.
- Give every ClusterSecretStore
spec.conditionsand stop tenants changing the labels it depends on. - Use workload identity for every store and a separate, prefix-scoped provider role per store.
- Enforce allowed providers, key prefixes and store references with Kyverno or Gatekeeper.
- Apply default deny NetworkPolicies with only the documented ports open.
- Verify image signatures and provenance before each upgrade, and stay on the supported minor release.
- Alert on sync errors, not-Ready stores and unexpected provider reads.
Standards and references
- External Secrets Operator: Security Best Practices
- External Secrets Operator: Threat Model
- External Secrets Operator: Multi Tenancy
- External Secrets Operator: ClusterSecretStore API
- External Secrets Operator: ExternalSecret API
- External Secrets Operator: PushSecret API
- External Secrets Operator: AWS Secrets Manager provider
- External Secrets Operator: Metrics
Related NHI Mgmt Group resources: Kubernetes Secrets Security Guide · GitOps Security Guide · SOPS and Sealed Secrets · Cloud Secrets Managers Guide · Kubernetes Admission Control Guide