Browse Source

feat(kubernetes): fall back to system CA roots when no CA is configured (#5961)

Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Raj Singh 1 month ago
parent
commit
1dd00cf57c

+ 25 - 1
docs/provider/kubernetes.md

@@ -82,7 +82,7 @@ spec:
 
 ### Target API-Server Configuration
 
-The servers `url` can be omitted and defaults to `kubernetes.default`. You **have to** provide a CA certificate in order to connect to the API Server securely.
+The servers `url` can be omitted and defaults to `kubernetes.default`. If no `caBundle` or `caProvider` is specified, the operator uses the system certificate roots from the container image. Both the default (`distroless/static`) and UBI images include standard CA certificates, so connections to servers using well-known CAs (e.g., Let's Encrypt) work without explicit CA configuration.
 For your convenience, each namespace has a ConfigMap `kube-root-ca.crt` that contains the CA certificate of the internal API Server (see `RootCAConfigMap` [feature gate](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/)).
 Use that if you want to connect to the same API server.
 If you want to connect to a remote API Server you need to fetch it and store it inside the cluster as ConfigMap or Secret.
@@ -106,6 +106,30 @@ spec:
           key: ca.crt
 ```
 
+!!! note
+    System CA roots only cover certificates signed by well-known CAs. Internal Kubernetes API servers typically use self-signed or cluster-internal CAs — you still need to provide explicit `caBundle` or `caProvider` for those.
+
+If the remote server uses a certificate from a well-known CA, you can omit CA configuration entirely:
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: k8s-store-system-ca
+spec:
+  provider:
+    kubernetes:
+      remoteNamespace: default
+      server:
+        url: "https://my-proxy.example.com"
+        # No caBundle or caProvider — uses system CA roots
+      auth:
+        token:
+          bearerToken:
+            name: my-token
+            key: token
+```
+
 ### Authentication
 
 It's possible to authenticate against the Kubernetes API using client certificates, a bearer token or service account. The operator enforces that exactly one authentication method is used. You can not use the service account that is mounted inside the operator, this is by design to avoid reading secrets across namespaces.

+ 37 - 0
providers/v1/kubernetes/auth_test.go

@@ -203,6 +203,43 @@ func TestSetAuth(t *testing.T) {
 			wantErr: false,
 		},
 		{
+			name: "should use system ca roots when no ca configured",
+			fields: fields{
+				namespace: "default",
+				kube: fclient.NewClientBuilder().WithObjects(&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foobar",
+						Namespace: "default",
+					},
+					Data: map[string][]byte{
+						"token": []byte("mytoken"),
+					},
+				}).Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL: serverURL,
+					},
+					Auth: &esv1.KubernetesAuth{
+						Token: &esv1.TokenAuth{
+							BearerToken: v1.SecretKeySelector{
+								Name: "foobar",
+								Key:  "token",
+							},
+						},
+					},
+				},
+			},
+			want: &want{
+				Host:        serverURL,
+				BearerToken: "mytoken",
+				TLSClientConfig: rest.TLSClientConfig{
+					Insecure: false,
+					CAData:   nil,
+				},
+			},
+			wantErr: false,
+		},
+		{
 			name: "should set token from secret",
 			fields: fields{
 				namespace: "default",

+ 16 - 11
providers/v1/kubernetes/validate.go

@@ -32,51 +32,56 @@ import (
 	"github.com/external-secrets/external-secrets/runtime/metrics"
 )
 
+const (
+	warnNoCAConfigured = "No caBundle or caProvider specified; TLS connections will use system certificate roots."
+)
+
 // ValidateStore validates the Kubernetes SecretStore configuration.
 func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
 	storeSpec := store.GetSpec()
 	k8sSpec := storeSpec.Provider.Kubernetes
+	var warnings admission.Warnings
 	if k8sSpec.AuthRef == nil && k8sSpec.Server.CABundle == nil && k8sSpec.Server.CAProvider == nil {
-		return nil, errors.New("a CABundle or CAProvider is required")
+		warnings = append(warnings, warnNoCAConfigured)
 	}
 	if store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind &&
 		k8sSpec.Server.CAProvider != nil &&
 		k8sSpec.Server.CAProvider.Namespace == nil {
-		return nil, errors.New("CAProvider.namespace must not be empty with ClusterSecretStore")
+		return warnings, errors.New("CAProvider.namespace must not be empty with ClusterSecretStore")
 	}
 	if store.GetObjectKind().GroupVersionKind().Kind == esv1.SecretStoreKind &&
 		k8sSpec.Server.CAProvider != nil &&
 		k8sSpec.Server.CAProvider.Namespace != nil {
-		return nil, errors.New("CAProvider.namespace must be empty with SecretStore")
+		return warnings, errors.New("CAProvider.namespace must be empty with SecretStore")
 	}
 	if k8sSpec.Auth != nil && k8sSpec.Auth.Cert != nil {
 		if k8sSpec.Auth.Cert.ClientCert.Name == "" {
-			return nil, errors.New("ClientCert.Name cannot be empty")
+			return warnings, errors.New("ClientCert.Name cannot be empty")
 		}
 		if k8sSpec.Auth.Cert.ClientCert.Key == "" {
-			return nil, errors.New("ClientCert.Key cannot be empty")
+			return warnings, errors.New("ClientCert.Key cannot be empty")
 		}
 		if err := esutils.ValidateSecretSelector(store, k8sSpec.Auth.Cert.ClientCert); err != nil {
-			return nil, err
+			return warnings, err
 		}
 	}
 	if k8sSpec.Auth != nil && k8sSpec.Auth.Token != nil {
 		if k8sSpec.Auth.Token.BearerToken.Name == "" {
-			return nil, errors.New("BearerToken.Name cannot be empty")
+			return warnings, errors.New("BearerToken.Name cannot be empty")
 		}
 		if k8sSpec.Auth.Token.BearerToken.Key == "" {
-			return nil, errors.New("BearerToken.Key cannot be empty")
+			return warnings, errors.New("BearerToken.Key cannot be empty")
 		}
 		if err := esutils.ValidateSecretSelector(store, k8sSpec.Auth.Token.BearerToken); err != nil {
-			return nil, err
+			return warnings, err
 		}
 	}
 	if k8sSpec.Auth != nil && k8sSpec.Auth.ServiceAccount != nil {
 		if err := esutils.ValidateReferentServiceAccountSelector(store, *k8sSpec.Auth.ServiceAccount); err != nil {
-			return nil, err
+			return warnings, err
 		}
 	}
-	return nil, nil
+	return warnings, nil
 }
 
 // Validate checks if the client has the necessary permissions to access secrets in the target namespace.

+ 78 - 7
providers/v1/kubernetes/validate_test.go

@@ -60,13 +60,14 @@ func TestValidateStore(t *testing.T) {
 	}
 
 	tests := []struct {
-		name    string
-		fields  fields
-		store   esv1.GenericStore
-		wantErr bool
+		name        string
+		fields      fields
+		store       esv1.GenericStore
+		wantErr     bool
+		wantWarning bool
 	}{
 		{
-			name: "empty ca",
+			name: "empty ca returns warning for system roots",
 			store: &esv1.SecretStore{
 				Spec: esv1.SecretStoreSpec{
 					Provider: &esv1.SecretStoreProvider{
@@ -74,7 +75,66 @@ func TestValidateStore(t *testing.T) {
 					},
 				},
 			},
-			wantErr: true,
+			wantErr:     false,
+			wantWarning: true,
+		},
+		{
+			name: "authRef suppresses no-ca warning",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						Kubernetes: &esv1.KubernetesProvider{
+							AuthRef: &v1.SecretKeySelector{
+								Name: "my-kubeconfig",
+								Key:  "config",
+							},
+						},
+					},
+				},
+			},
+			wantErr:     false,
+			wantWarning: false,
+		},
+		{
+			name: "token auth without ca returns warning only",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						Kubernetes: &esv1.KubernetesProvider{
+							Auth: &esv1.KubernetesAuth{
+								Token: &esv1.TokenAuth{
+									BearerToken: v1.SecretKeySelector{
+										Name: "my-token",
+										Key:  "token",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr:     false,
+			wantWarning: true,
+		},
+		{
+			name: "no ca with other validation error still returns warning",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						Kubernetes: &esv1.KubernetesProvider{
+							Auth: &esv1.KubernetesAuth{
+								Cert: &esv1.CertAuth{
+									ClientCert: v1.SecretKeySelector{
+										Name: "",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr:     true,
+			wantWarning: true,
 		},
 		{
 			name: "invalid client cert name",
@@ -300,9 +360,20 @@ func TestValidateStore(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			k := &Provider{}
-			if _, err := k.ValidateStore(tt.store); (err != nil) != tt.wantErr {
+			warnings, err := k.ValidateStore(tt.store)
+			if (err != nil) != tt.wantErr {
 				t.Errorf("ProviderKubernetes.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
 			}
+			if tt.wantWarning {
+				if len(warnings) != 1 {
+					t.Fatalf("ProviderKubernetes.ValidateStore() expected exactly 1 warning, got %d: %v", len(warnings), warnings)
+				}
+				if warnings[0] != warnNoCAConfigured {
+					t.Errorf("ProviderKubernetes.ValidateStore() warning = %q, want %q", warnings[0], warnNoCAConfigured)
+				}
+			} else if len(warnings) > 0 {
+				t.Errorf("ProviderKubernetes.ValidateStore() unexpected warnings: %v", warnings)
+			}
 		})
 	}
 }