CVE-2025-55196
Description
External Secrets Operator is a Kubernetes operator that integrates external secret management systems. From version 0.15.0 to before 0.19.2, a vulnerability was discovered where the List() calls for Kubernetes Secret and SecretStore resources performed by the PushSecret controller did not apply a namespace selector. This flaw allowed an attacker to use label selectors to list and read secrets/secret-stores across the cluster, bypassing intended namespace restrictions. An attacker with the ability to create or update PushSecret resources and control SecretStore configurations could exploit this vulnerability to exfiltrate sensitive data from arbitrary namespaces. This could lead to full disclosure of Kubernetes secrets, including credentials, tokens, and other sensitive information stored in the cluster. This vulnerability has been patched in version 0.19.2. A workaround for this issue includes auditing and restricting RBAC permissions so that only trusted service accounts can create or update PushSecret and SecretStore resources.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/external-secrets/external-secretsGo | >= 0.15.0, < 0.19.2 | 0.19.2 |
Affected products
1- Range: helm-chart-0.15.1, helm-chart-0.16.0, helm-chart-0.17.0, …
Patches
3f6b47d56d1a5fix: bump support table & update roadmap (#5135)
2 files changed · +22 −24
docs/contributing/roadmap.md+0 −3 modified@@ -16,12 +16,9 @@ We have identified the following areas of work. This is subject to change while * ✓ end to end testing with ArgoCD and Flux * ✓ end to end testing for all project maintained providers * API enhancements - * consolidate provider fields * ✓ dataFrom key rewrites - * provider versioning strategy * ✓ pushing secrets to a provider * Documentation Improvements - * Troubleshooting Guides * ✓ FAQ * ✓ review multi tenancy docs * ✓ security model for infosec teams
docs/introduction/stability-support.md+22 −21 modified@@ -16,31 +16,32 @@ During a minor version support time, we cover: We do not do test coverage for any other kubernetes version than the ones running on our test suites. As of version 0.14.x , this is the only kubernetes version that we will guarantee support for. -| ESO Version | Kubernetes Version | Release Date | End of Life | -| ----------- | ------------------ | ------------ | --------------- | -| 0.18.x | 1.33 | July 17, 2025 | Release of 0.19 | -| 0.17.x | 1.33 | May 14, 2025 | July 17, 2025 | -| 0.16.x | 1.32 | Apr 14, 2025 | May 14, 2025 | -| 0.15.x | 1.32 | Mar 19, 2025 | Apr 14, 2025 | -| 0.14.x | 1.32 | Feb 4, 2025 | Mar 19, 2025 | -| 0.13.x | 1.19 → 1.31 | Jan 21, 2025 | Feb 4, 2025 | -| 0.12.x | 1.19 → 1.31 | Dec 24, 2024 | Jan 21, 2025 | -| 0.11.x | 1.19 → 1.31 | Dec 2, 2024 | Dec 24, 2024 | -| 0.10.x | 1.19 → 1.31 | Aug 3, 2024 | Dec 24, 2024 | -| 0.9.x | 1.19 → 1.30 | Jun 22, 2023 | Dec 2, 2024 | -| 0.8.x | 1.19 → 1.28 | Mar 16, 2023 | Aug 3, 2024 | -| 0.7.x | 1.19 → 1.26 | Dec 11, 2022 | Jun 22, 2023 | -| 0.6.x | 1.19 → 1.24 | Oct 9, 2022 | Mar 16, 2023 | -| 0.5.x | 1.19 → 1.24 | Apr 6, 2022 | Dec 11, 2022 | -| 0.4.x | 1.16 → 1.24 | Feb 2, 2022 | Oct 9, 2022 | -| 0.3.x | 1.16 → 1.24 | Jul 25, 2021 | Apr 6, 2022 | +| ESO Version | Kubernetes Version | Release Date | End of Life | +| ----------- | ------------------ | -------------- | --------------- | +| 0.19.x | 1.33 | August 2, 2025 | Release of 0.20 | +| 0.18.x | 1.33 | July 17, 2025 | August 2, 2025 | +| 0.17.x | 1.33 | May 14, 2025 | July 17, 2025 | +| 0.16.x | 1.32 | Apr 14, 2025 | May 14, 2025 | +| 0.15.x | 1.32 | Mar 19, 2025 | Apr 14, 2025 | +| 0.14.x | 1.32 | Feb 4, 2025 | Mar 19, 2025 | +| 0.13.x | 1.19 → 1.31 | Jan 21, 2025 | Feb 4, 2025 | +| 0.12.x | 1.19 → 1.31 | Dec 24, 2024 | Jan 21, 2025 | +| 0.11.x | 1.19 → 1.31 | Dec 2, 2024 | Dec 24, 2024 | +| 0.10.x | 1.19 → 1.31 | Aug 3, 2024 | Dec 24, 2024 | +| 0.9.x | 1.19 → 1.30 | Jun 22, 2023 | Dec 2, 2024 | +| 0.8.x | 1.19 → 1.28 | Mar 16, 2023 | Aug 3, 2024 | +| 0.7.x | 1.19 → 1.26 | Dec 11, 2022 | Jun 22, 2023 | +| 0.6.x | 1.19 → 1.24 | Oct 9, 2022 | Mar 16, 2023 | +| 0.5.x | 1.19 → 1.24 | Apr 6, 2022 | Dec 11, 2022 | +| 0.4.x | 1.16 → 1.24 | Feb 2, 2022 | Oct 9, 2022 | +| 0.3.x | 1.16 → 1.24 | Jul 25, 2021 | Apr 6, 2022 | ## Provider Stability and Support Level The following table describes the stability level of each provider and who's responsible. -| Provider | Stability | Maintainer | -|------------------------------------------------------------------------------------------------------------|-:-:-------|--:--------------------------------------------------------------------------------------------------| +| Provider | Stability | Maintainer | +| -------- | --------: | ---------: |-------|--:--------------------------------------------------------------------------------------------------| | [AWS Secrets Manager](https://external-secrets.io/latest/provider/aws-secrets-manager/) | stable | [external-secrets](https://github.com/external-secrets) | | [AWS Parameter Store](https://external-secrets.io/latest/provider/aws-parameter-store/) | stable | [external-secrets](https://github.com/external-secrets) | | [Hashicorp Vault](https://external-secrets.io/latest/provider/hashicorp-vault/) | stable | [external-secrets](https://github.com/external-secrets) | @@ -78,7 +79,7 @@ The following table describes the stability level of each provider and who's res The following table show the support for features across different providers. | Provider | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret | DeletionPolicy Merge/Delete | -|---------------------------|:------------:|:------------:|:--------------------:|:-----------------------:|:----------------:|:-----------:|:---------------------------:| +| ------------------------- | :----------: | :----------: | :------------------: | :---------------------: | :--------------: | :---------: | :-------------------------: | | AWS Secrets Manager | x | x | x | x | x | x | x | | AWS Parameter Store | x | x | x | x | x | x | x | | Hashicorp Vault | x | x | x | x | x | x | x |
39cdba586353fix: scope secret list call to the namespace the push secret was created (#5133)
3 files changed · +105 −40
pkg/controllers/pushsecret/pushsecret_controller.go+1 −1 modified@@ -429,7 +429,7 @@ func (r *Reconciler) resolveSecrets(ctx context.Context, ps *esapi.PushSecret) ( } var secretList v1.SecretList - err = r.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector}) + err = r.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace}) if err != nil { return nil, err }
pkg/controllers/pushsecret/pushsecret_controller_test.go+81 −36 modified@@ -65,7 +65,6 @@ func init() { } func checkCondition(status v1alpha1.PushSecretStatus, cond v1alpha1.PushSecretStatusCondition) bool { - fmt.Printf("status: %+v\ncond: %+v\n", status.Conditions, cond) for _, condition := range status.Conditions { if condition.Message == cond.Message && condition.Reason == cond.Reason && @@ -232,7 +231,8 @@ var _ = Describe("PushSecret controller", func() { Eventually(func() bool { By("checking if Provider value got updated") secretValue := secret.Data[defaultKey] - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } @@ -248,23 +248,26 @@ var _ = Describe("PushSecret controller", func() { return nil } fakeProvider.SecretExistsFn = func(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) { - _, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()] + setSecretArgs := fakeProvider.GetPushSecretData() + _, ok := setSecretArgs[ref.GetRemoteKey()] return ok, nil } tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists - initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value - tc.secret.Data[defaultKey] = []byte(newVal) tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { - Eventually(func() bool { - By("checking if Provider value did not get updated") + Consistently(func() bool { + By("updating the secret value") + tc.secret.Data[defaultKey] = []byte(newVal) Expect(k8sClient.Update(context.Background(), secret, &client.UpdateOptions{})).Should(Succeed()) - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + + By("checking if Provider value does not get updated") + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } got := providerValue.Value - return bytes.Equal(got, initialValue) + return bytes.Equal(got, []byte(defaultVal)) }, time.Second*10, time.Second).Should(BeTrue()) return true } @@ -275,7 +278,8 @@ var _ = Describe("PushSecret controller", func() { return nil } fakeProvider.SecretExistsFn = func(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) { - _, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()] + setSecretArgs := fakeProvider.GetPushSecretData() + _, ok := setSecretArgs[ref.GetRemoteKey()] return ok, nil } tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists @@ -288,26 +292,25 @@ var _ = Describe("PushSecret controller", func() { }, }) - initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value - tc.secret.Data[defaultKey] = []byte(newVal) // change initial value in secret - tc.secret.Data[otherKey] = []byte(otherVal) - tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { Eventually(func() bool { + tc.secret.Data[defaultKey] = []byte(newVal) // change initial value in secret + tc.secret.Data[otherKey] = []byte(otherVal) + By("checking if only not existing Provider value got updated") Expect(k8sClient.Update(context.Background(), secret, &client.UpdateOptions{})).Should(Succeed()) - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } got := providerValue.Value - otherProviderValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[1].Match.RemoteRef.RemoteKey] + otherProviderValue, ok := setSecretArgs[ps.Spec.Data[1].Match.RemoteRef.RemoteKey] if !ok { return false } gotOther := otherProviderValue.Value - - return bytes.Equal(gotOther, tc.secret.Data[otherKey]) && bytes.Equal(got, initialValue) + return bytes.Equal(gotOther, tc.secret.Data[otherKey]) && bytes.Equal(got, []byte(defaultVal)) }, time.Second*10, time.Second).Should(BeTrue()) return true } @@ -318,7 +321,8 @@ var _ = Describe("PushSecret controller", func() { return nil } fakeProvider.SecretExistsFn = func(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) { - _, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()] + setSecretArgs := fakeProvider.GetPushSecretData() + _, ok := setSecretArgs[ref.GetRemoteKey()] return ok, nil } tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists @@ -371,24 +375,17 @@ var _ = Describe("PushSecret controller", func() { return false, errors.New("don't know") } tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists - initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value - tc.secret.Data[defaultKey] = []byte(newVal) tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { Eventually(func() bool { By("checking if sync failed if secret existence cannot be verified in Provider") - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] - if !ok { - return false - } - got := providerValue.Value expected := v1alpha1.PushSecretStatusCondition{ Type: v1alpha1.PushSecretReady, Status: v1.ConditionFalse, Reason: v1alpha1.ReasonErrored, Message: "set secret failed: could not verify if secret exists in store: don't know", } - return checkCondition(ps.Status, expected) && bytes.Equal(got, initialValue) + return checkCondition(ps.Status, expected) }, time.Second*10, time.Second).Should(BeTrue()) return true } @@ -445,7 +442,8 @@ var _ = Describe("PushSecret controller", func() { tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { Eventually(func() bool { By("checking if Provider value got updated") - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } @@ -507,7 +505,8 @@ var _ = Describe("PushSecret controller", func() { tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { Eventually(func() bool { By("checking if Provider value got updated") - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } @@ -824,7 +823,8 @@ var _ = Describe("PushSecret controller", func() { Eventually(func() bool { By("checking if Provider value got updated") secretValue := secret.Data["some-array_U005b_0_U005d_.entity"] - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } @@ -897,7 +897,8 @@ var _ = Describe("PushSecret controller", func() { } tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { secretValue := secret.Data[defaultKey] - providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value expected := v1alpha1.PushSecretStatusCondition{ Type: v1alpha1.PushSecretReady, Status: v1.ConditionTrue, @@ -929,7 +930,8 @@ var _ = Describe("PushSecret controller", func() { tc.pushsecret.Spec.SecretStoreRefs[0].Kind = "ClusterSecretStore" tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { secretValue := secret.Data[defaultKey] - providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value expected := v1alpha1.PushSecretStatusCondition{ Type: v1alpha1.PushSecretReady, Status: v1.ConditionTrue, @@ -951,7 +953,8 @@ var _ = Describe("PushSecret controller", func() { Name: "test", } tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { - providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value expected := v1alpha1.PushSecretStatusCondition{ Type: v1alpha1.PushSecretReady, Status: v1.ConditionTrue, @@ -1017,7 +1020,8 @@ var _ = Describe("PushSecret controller", func() { } tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { secretValue := secret.Data[defaultKey] - providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value expected := v1alpha1.PushSecretStatusCondition{ Type: v1alpha1.PushSecretReady, Status: v1.ConditionTrue, @@ -1174,6 +1178,44 @@ var _ = Describe("PushSecret controller", func() { } } + // Secrets in different namespace than PushSecret should not be selected. + secretDifferentNamespace := func(tc *testCase) { + fakeProvider.SetSecretFn = func() error { + return nil + } + // Create the Secret in a different namespace + tc.secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecretName, + Namespace: OtherNamespace, + Labels: map[string]string{ + "foo": "bar", + }, + }, + Data: map[string][]byte{ + defaultKey: []byte(defaultVal), + }, + } + // Use label selector to select Secrets + tc.pushsecret.Spec.Selector.Secret = &v1alpha1.PushSecretSecret{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + } + + tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { + Eventually(func() bool { + // We should not be able to reference a secret across namespaces, + // the map should be empty. + Expect(fakeProvider.GetPushSecretData()).To(BeEmpty()) + return true + }, time.Second*10, time.Second).Should(BeTrue()) + return true + } + } + DescribeTable("When reconciling a PushSecret", func(tweaks ...testTweaks) { tc := makeDefaultTestcase() @@ -1228,6 +1270,7 @@ var _ = Describe("PushSecret controller", func() { Entry("should fail if no valid ClusterSecretStore", failNoClusterStore), Entry("should fail if NewClient fails", newClientFail), Entry("should not sync to SecretStore in different namespace", secretStoreDifferentNamespace), + Entry("should not reference secret in different namespace", secretDifferentNamespace), ) }) @@ -1425,7 +1468,8 @@ var _ = Describe("PushSecret Controller Un/Managed Stores", func() { Eventually(func() bool { By("checking if Provider value got updated") secretValue := secret.Data[defaultKey] - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false } @@ -1481,7 +1525,8 @@ var _ = Describe("PushSecret Controller Un/Managed Stores", func() { Eventually(func() bool { By("checking if Provider value got updated") secretValue := secret.Data[defaultKey] - providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] + setSecretArgs := fakeProvider.GetPushSecretData() + providerValue, ok := setSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey] if !ok { return false }
pkg/provider/testing/fake/fake.go+23 −3 modified@@ -16,6 +16,7 @@ package fake import ( "context" + "sync" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,7 +34,8 @@ type SetSecretCallArgs struct { // Client is a fake client for testing. type Client struct { - SetSecretArgs map[string]SetSecretCallArgs + mu *sync.RWMutex + pushSecretData map[string]SetSecretCallArgs NewFn func(context.Context, esv1.GenericStore, client.Client, string) (esv1.SecretsClient, error) GetSecretFn func(context.Context, esv1.ExternalSecretDataRemoteRef) ([]byte, error) GetSecretMapFn func(context.Context, esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) @@ -46,6 +48,7 @@ type Client struct { // New returns a fake provider/client. func New() *Client { v := &Client{ + mu: &sync.RWMutex{}, GetSecretFn: func(context.Context, esv1.ExternalSecretDataRemoteRef) ([]byte, error) { return nil, nil }, @@ -64,7 +67,7 @@ func New() *Client { DeleteSecretFn: func() error { return nil }, - SetSecretArgs: map[string]SetSecretCallArgs{}, + pushSecretData: map[string]SetSecretCallArgs{}, } v.NewFn = func(context.Context, esv1.GenericStore, client.Client, string) (esv1.SecretsClient, error) { @@ -85,13 +88,27 @@ func (v *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) } func (v *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1.PushSecretData) error { - v.SetSecretArgs[data.GetRemoteKey()] = SetSecretCallArgs{ + v.mu.Lock() + defer v.mu.Unlock() + v.pushSecretData[data.GetRemoteKey()] = SetSecretCallArgs{ Value: secret.Data[data.GetSecretKey()], RemoteRef: data, } return v.SetSecretFn() } +// GetPushSecretData safely retrieves the push secret data map for reading. +func (v *Client) GetPushSecretData() map[string]SetSecretCallArgs { + v.mu.RLock() + defer v.mu.RUnlock() + // Create a copy to avoid race conditions + result := make(map[string]SetSecretCallArgs, len(v.pushSecretData)) + for k, v := range v.pushSecretData { + result[k] = v + } + return result +} + func (v *Client) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error { return v.DeleteSecretFn() } @@ -180,4 +197,7 @@ func (v *Client) Reset() { string) (esv1.SecretsClient, error) { return v, nil }) + v.mu.Lock() + defer v.mu.Unlock() + v.pushSecretData = map[string]SetSecretCallArgs{} }
de40e8f4fa95fix: select secretstores in same ns as pushsecret (#5109)
2 files changed · +55 −2
pkg/controllers/pushsecret/pushsecret_controller.go+1 −1 modified@@ -494,7 +494,7 @@ func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) ( } } else { secretStoreList := esv1.SecretStoreList{} - err = r.List(ctx, &secretStoreList, &client.ListOptions{LabelSelector: labelSelector}) + err = r.List(ctx, &secretStoreList, &client.ListOptions{LabelSelector: labelSelector, Namespace: ps.Namespace}) if err != nil { return nil, fmt.Errorf("could not list Secret Stores: %w", err) }
pkg/controllers/pushsecret/pushsecret_controller_test.go+54 −1 modified@@ -86,7 +86,7 @@ var _ = Describe("PushSecret controller", func() { SecretName = "test-secret" ) - var PushSecretNamespace string + var PushSecretNamespace, OtherNamespace string // if we are in debug and need to increase the timeout for testing, we can do so by using an env var if customTimeout := os.Getenv("TEST_CUSTOM_TIMEOUT_SEC"); customTimeout != "" { @@ -99,6 +99,8 @@ var _ = Describe("PushSecret controller", func() { var err error PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient) Expect(err).ToNot(HaveOccurred()) + OtherNamespace, err = ctest.CreateNamespace("test-ns", k8sClient) + Expect(err).ToNot(HaveOccurred()) fakeProvider.Reset() Expect(k8sClient.Create(context.Background(), &genv1alpha1.Fake{ @@ -1121,6 +1123,56 @@ var _ = Describe("PushSecret controller", func() { return checkCondition(ps.Status, expected) } } + // SecretStores in different namespace than PushSecret should not be selected. + secretStoreDifferentNamespace := func(tc *testCase) { + fakeProvider.SetSecretFn = func() error { + return nil + } + // Create the SecretStore in a different namespace + tc.store = &esv1.SecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-ns-store", + Namespace: OtherNamespace, + Labels: map[string]string{ + "foo": "bar", + }, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "SecretStore", + }, + Spec: esv1.SecretStoreSpec{ + Provider: &esv1.SecretStoreProvider{ + Fake: &esv1.FakeProvider{ + Data: []esv1.FakeProviderData{}, + }, + }, + }, + } + // Use label selector to select SecretStores + tc.pushsecret.Spec.SecretStoreRefs = []v1alpha1.PushSecretStoreRef{ + { + Kind: "SecretStore", + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + } + // Should not select the SecretStore in a different namespace + // (if so, it would fail to find it in the same namespace and be reflected in the status) + tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { + // Assert that the status is never updated (no SecretStores found) + Consistently(func() bool { + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ps), ps) + if err != nil { + return false + } + return len(ps.Status.Conditions) == 0 + }, timeout, interval).Should(BeTrue()) + return true + } + } DescribeTable("When reconciling a PushSecret", func(tweaks ...testTweaks) { @@ -1175,6 +1227,7 @@ var _ = Describe("PushSecret controller", func() { Entry("should fail if no valid SecretStore", failNoSecretStore), Entry("should fail if no valid ClusterSecretStore", failNoClusterStore), Entry("should fail if NewClient fails", newClientFail), + Entry("should not sync to SecretStore in different namespace", secretStoreDifferentNamespace), ) })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-fcxq-v2r3-cc8hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55196ghsaADVISORY
- github.com/external-secrets/external-secrets/commit/39cdba5863533007b582dc63dd300839326b2f1dnvdWEB
- github.com/external-secrets/external-secrets/commit/de40e8f4fa9559c1d770bb674589b285da5ef2d1nvdWEB
- github.com/external-secrets/external-secrets/pull/5109nvdWEB
- github.com/external-secrets/external-secrets/pull/5133nvdWEB
- github.com/external-secrets/external-secrets/security/advisories/GHSA-fcxq-v2r3-cc8hnvdWEB
News mentions
0No linked articles in our index yet.