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
SharedSecretStoreresources that Spaces projects into each control plane. → Route A, retarget the selectorInside 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.
Point the selector at a control plane name that does not exist. Spaces removes the projected
ClusterSecretStorefrom every matched control plane.
spec:
controlPlaneSelector:
names:
- sync-disabled-placeholderConfirm the projection is gone.
status.provisionedon the shared store empties out, and the store disappears from inside each control plane.
kubectl -n <group> get sharedsecretstore <name> \
-o jsonpath='{.status.provisioned}'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 |
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 |
|
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.
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: …
If the store is Argo-managed, make the change in git. A live
kubectl patchis reverted byselfHeal. Changing it in one templated manifest is also how this becomes a fleet-wide lever with a single commit.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
fakeandkubernetesproviders 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: ReadOnlywhile performing writes normally. The label is wrong, not the behaviour; do not treat it as a signal.