How do I stop External Secrets Operator (ESO) syncing in Upbound Spaces

Last updated: August 27, 2026

How to stop External Secrets syncing in one control plane or across every control plane: which levers are safe, which destroy data, and what each one actually does when you pull it.

Validated on Spaces 1.16.0 · ESO chart 0.17.0-up.1 (app v0.17.0)

The short version

Fleet-wide, safe: Retarget controlPlaneSelector on each SharedSecretStore to a control plane name that does not exist. One edit stops every control plane at once.

Per control plane, safe: Set spec.controller to any non-empty value on the store. Spaces runs ESO with no controller class, so it skips that store entirely.

Never, deletes secrets: Do not set features.alpha.sharedSecrets.enabled: false, and never retarget the selector on a SharedExternalSecret. Both destroy live secrets.

Which lever applies to you

The two safe routes are not interchangeable. The right one depends on where the stores your PushSecrets and ExternalSecrets reference are actually defined. Establish this before touching anything.

Where are the SecretStore / ClusterSecretStore resources your secrets reference defined?

  • At the Space level: as SharedSecretStore resources that Spaces projects into each control plane. → Route A, retarget the selector

  • Inside each control plane: applied by your own Argo CD application or ApplicationSet, or by hand. → Route B, set a controller class


If both exist in your estate, you need both routes: Route A covers what Spaces projects, Route B covers what you apply yourself. A store defined in a control plane is invisible to controlPlaneSelector.

Route A: Retarget the selector

Fleet-wide stop via SharedSecretStore

One edit per shared store, covering every control plane it projects into. Self-healing on resume. This is the recommended route when it applies.

  1. Point the selector at a control plane name that does not exist. Spaces removes the projected ClusterSecretStore from every matched control plane.

spec:
  controlPlaneSelector:
    names:
      - sync-disabled-placeholder
  1. Confirm the projection is gone. status.provisioned on the shared store empties out, and the store disappears from inside each control plane.

kubectl -n <group> get sharedsecretstore <name> \
  -o jsonpath='{.status.provisioned}'
  1. To resume, restore the original selector. Nothing else is required; see the resume timing below before you assume it has failed.


Verified behaviour while stopped

What

Result

Detail

ClusterSecretStore

Removed

Deleted from every control plane the selector previously matched.

Kubernetes Secret

Survives

Owned by the ExternalSecret, which stays in place. Values frozen at last sync.

Remote secret

Survives

Left exactly as last pushed. A source change during the stop did not propagate.

ExternalSecret

SecretSyncedError

Flips within 10s and holds. Loudly visible.

PushSecret

Errored

could not get ClusterSecretStore … not found

Selector trap: Use names with a value that does not exist. Do not try to express "match nothing" as labelSelectors: [{}]; it passes validation and matches every control plane, the exact opposite of the intent. An entirely empty controlPlaneSelector is rejected by validation, which is the one case the API protects you from.

Route B: Set a controller class

Per-control-plane stop via spec.controller

Works on any store, including ones Spaces projects. Deletes nothing at all. Requires a manual nudge to resume; it never self-heals.

Spaces starts its per-control-plane ESO with no --controller-class argument, so it only picks up stores whose controller class is empty. Any non-empty value makes ESO skip that store, and with it every ExternalSecret and PushSecret that references it.

  1. Add the field to each store in the control plane you want stopped.

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: my-store
spec:
  controller: paused   # any non-empty value
  provider: …
  1. If the store is Argo-managed, make the change in git. A live kubectl patch is reverted by selfHeal. Changing it in one templated manifest is also how this becomes a fleet-wide lever with a single commit.

  2. To resume, remove the field and nudge every dependent object. Removing the field alone does nothing, because a skipped object is never re-queued.

kubectl patch clustersecretstore <name> --type=json \
  -p '[{"op":"remove","path":"/spec/controller"}]'

kubectl annotate externalsecret --all -n <ns> resume="$(date +%s)" --overwrite
kubectl annotate pushsecret    --all -n <ns> resume="$(date +%s)" --overwrite


This stop is silent: Skipped objects keep reporting Ready=True / SecretSynced from their last successful run. Nothing flips to an error and no event is emitted, so anyone looking at that environment later sees healthy-looking resources that are not syncing. Leave a note for whoever is on call.

One other option: you can scale the ESO deployment in the control plane's host namespace down to zero, which stops everything ESO does for that control plane without touching any store, ExternalSecret or PushSecret. It is not reverted immediately, but it does not survive a Spaces Helm upgrade, which reapplies the release and brings the replica count back. Like Route B, this stop is silent, because nothing on the ESO resources changes.

What to expect when you turn it back on

The two routes differ most on the way back, and the difference is mechanical. Route A leaves objects erroring, so they stay in the controller's retry queue and recover on their own. Route B filters objects out entirely, so nothing ever re-queues them.

Resume characteristics

Route

Self-heals

Observed latency

Nudge

A: selector

Yes

~90s after a 90s stop; ~5 min after a 4–6 min stop

Optional, makes it immediate

B: controller class

No

Never

Mandatory

Route A latency scales with how long the stop lasted, because failing objects back off exponentially: measured retry gaps doubled through roughly 1, 4, 8, 16, 32 and 64 seconds, bounded by the controller runtime default ceiling of about 16 minutes. After a stop measured in days, expect up to that ceiling. Recovery was stable once it happened, with no flapping over a subsequent 15 minutes. A freshly created object against the restored store syncs immediately, which is a quick way to confirm the store itself is healthy while older objects are still backing off.

Two levers that destroy secrets

Disabling the sharedSecrets feature flag: Setting features.alpha.sharedSecrets.enabled: false is not a "stop syncing" switch. Spaces treats a disabled condition as a Helm uninstall, not a skip. Both the operator release and the in-control-plane CRD release are removed. The ESO chart ships its CRDs as templates and sets no retention policy, so the CRDs go with the release, and every ESO custom resource goes with the CRDs, along with every Kubernetes Secret created by an ExternalSecret on the default creationPolicy: Owner.


Observed on a live Space: 20 ESO CRDs to 0, all stores, ExternalSecrets and PushSecrets gone, and of four Secrets only the one hand-created as a plain Secret survived.

Retargeting a SharedExternalSecret selector: Safe on SharedSecretStore, destructive on SharedExternalSecret. The projection sets creationPolicy: Owner and the generated ExternalSecret is owned by the projected ClusterExternalSecret with blockOwnerDeletion: true. Retargeting the selector deletes the ClusterExternalSecret, which deletes the ExternalSecret, which deletes the Secret. Verified end to end.

Notes

  • Only the fake and kubernetes providers were exercised. Azure Key Vault, Vault and the other backends are untested, though the selector route never reaches the provider at all.

  • A projected store reports status.capabilities: ReadOnly while performing writes normally. The label is wrong, not the behaviour; do not treat it as a signal.