Browse Source

feat(passbolt): add custom CA bundle / CA provider support (#6224)

Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Ali Asghar 1 month ago
parent
commit
e299def3dd

+ 8 - 0
apis/externalsecrets/v1/secretsstore_passbolt_types.go

@@ -33,4 +33,12 @@ type PassboltProvider struct {
 	Auth *PassboltAuth `json:"auth"`
 	// Host defines the Passbolt Server to connect to
 	Host string `json:"host"`
+	// PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+	// if the Host URL is using HTTPS protocol. If not set the system root certificates
+	// are used to validate the TLS connection.
+	// +optional
+	CABundle []byte `json:"caBundle,omitempty"`
+	// The provider for the CA bundle to use to validate Passbolt server certificate.
+	// +optional
+	CAProvider *CAProvider `json:"caProvider,omitempty"`
 }

+ 10 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -3122,6 +3122,16 @@ func (in *PassboltProvider) DeepCopyInto(out *PassboltProvider) {
 		*out = new(PassboltAuth)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CABundle != nil {
+		in, out := &in.CABundle, &out.CABundle
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(CAProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassboltProvider.

+ 44 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -4388,6 +4388,50 @@ spec:
                         - passwordSecretRef
                         - privateKeySecretRef
                         type: object
+                      caBundle:
+                        description: |-
+                          PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+                          if the Host URL is using HTTPS protocol. If not set the system root certificates
+                          are used to validate the TLS connection.
+                        format: byte
+                        type: string
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Passbolt server certificate.
+                        properties:
+                          key:
+                            description: The key where the CA certificate can be found
+                              in the Secret or ConfigMap.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace the Provider type is in.
+                              Can only be defined when used in a ClusterSecretStore.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
                       host:
                         description: Host defines the Passbolt Server to connect to
                         type: string

+ 44 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -4388,6 +4388,50 @@ spec:
                         - passwordSecretRef
                         - privateKeySecretRef
                         type: object
+                      caBundle:
+                        description: |-
+                          PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+                          if the Host URL is using HTTPS protocol. If not set the system root certificates
+                          are used to validate the TLS connection.
+                        format: byte
+                        type: string
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Passbolt server certificate.
+                        properties:
+                          key:
+                            description: The key where the CA certificate can be found
+                              in the Secret or ConfigMap.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace the Provider type is in.
+                              Can only be defined when used in a ClusterSecretStore.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
                       host:
                         description: Host defines the Passbolt Server to connect to
                         type: string

+ 80 - 0
deploy/crds/bundle.yaml

@@ -6339,6 +6339,46 @@ spec:
                             - passwordSecretRef
                             - privateKeySecretRef
                           type: object
+                        caBundle:
+                          description: |-
+                            PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+                            if the Host URL is using HTTPS protocol. If not set the system root certificates
+                            are used to validate the TLS connection.
+                          format: byte
+                          type: string
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Passbolt server certificate.
+                          properties:
+                            key:
+                              description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the object located at the provider type.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace the Provider type is in.
+                                Can only be defined when used in a ClusterSecretStore.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                            type:
+                              description: The type of provider to use such as "Secret", or "ConfigMap".
+                              enum:
+                                - Secret
+                                - ConfigMap
+                              type: string
+                          required:
+                            - name
+                            - type
+                          type: object
                         host:
                           description: Host defines the Passbolt Server to connect to
                           type: string
@@ -18450,6 +18490,46 @@ spec:
                             - passwordSecretRef
                             - privateKeySecretRef
                           type: object
+                        caBundle:
+                          description: |-
+                            PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+                            if the Host URL is using HTTPS protocol. If not set the system root certificates
+                            are used to validate the TLS connection.
+                          format: byte
+                          type: string
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Passbolt server certificate.
+                          properties:
+                            key:
+                              description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the object located at the provider type.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace the Provider type is in.
+                                Can only be defined when used in a ClusterSecretStore.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                            type:
+                              description: The type of provider to use such as "Secret", or "ConfigMap".
+                              enum:
+                                - Secret
+                                - ConfigMap
+                              type: string
+                          required:
+                            - name
+                            - type
+                          type: object
                         host:
                           description: Host defines the Passbolt Server to connect to
                           type: string

+ 29 - 0
docs/api/spec.md

@@ -1775,6 +1775,7 @@ string
 <a href="#external-secrets.io/v1.InfisicalProvider">InfisicalProvider</a>, 
 <a href="#external-secrets.io/v1.KubernetesServer">KubernetesServer</a>, 
 <a href="#external-secrets.io/v1.OvhClientMTLS">OvhClientMTLS</a>, 
+<a href="#external-secrets.io/v1.PassboltProvider">PassboltProvider</a>, 
 <a href="#external-secrets.io/v1.SecretServerProvider">SecretServerProvider</a>, 
 <a href="#external-secrets.io/v1.VaultProvider">VaultProvider</a>)
 </p>
@@ -8575,6 +8576,34 @@ string
 <p>Host defines the Passbolt Server to connect to</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>caBundle</code></br>
+<em>
+[]byte
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>PEM encoded CA bundle used to validate Passbolt server certificate. Only used
+if the Host URL is using HTTPS protocol. If not set the system root certificates
+are used to validate the TLS connection.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1.CAProvider">
+CAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The provider for the CA bundle to use to validate Passbolt server certificate.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.PasswordDepotAuth">PasswordDepotAuth

+ 15 - 0
docs/provider/passbolt.md

@@ -11,6 +11,21 @@ The API requires a password and private key provided in a secret.
 {% include 'passbolt-secret-store.yaml' %}
 ```
 
+#### Custom CA certificate
+
+If your Passbolt instance uses a certificate signed by a private or custom
+Certificate Authority, you can configure the CA bundle that ESO uses to
+validate the Passbolt server certificate. Either supply the PEM-encoded
+bundle inline via `caBundle`, or reference a Secret/ConfigMap via
+`caProvider`.
+
+```yaml
+{% include 'passbolt-secret-store-ca.yaml' %}
+```
+
+If neither `caBundle` nor `caProvider` is set, ESO uses the system root
+certificates to validate the TLS connection.
+
 
 ### Creating an external secret
 

+ 21 - 0
docs/snippets/passbolt-secret-store-ca.yaml

@@ -0,0 +1,21 @@
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: passbolt-with-custom-ca
+spec:
+  provider:
+    passbolt:
+      host: https://passbolt.example.com
+      # Reference a ConfigMap or Secret containing the CA bundle that signed
+      # the Passbolt server certificate.
+      caProvider:
+        type: ConfigMap
+        name: passbolt-ca-bundle
+        key: ca.crt
+      auth:
+        passwordSecretRef:
+          key: password
+          name: passbolt-credentials
+        privateKeySecretRef:
+          key: privateKey
+          name: passbolt-credentials

+ 1 - 1
providers/v1/passbolt/go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/onsi/gomega v1.39.1
 	github.com/passbolt/go-passbolt v0.8.0-beta.1
 	k8s.io/api v0.35.2
+	k8s.io/apimachinery v0.35.2
 	sigs.k8s.io/controller-runtime v0.23.3
 )
 
@@ -86,7 +87,6 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	k8s.io/apiextensions-apiserver v0.35.2 // indirect
-	k8s.io/apimachinery v0.35.2 // indirect
 	k8s.io/client-go v0.35.2 // indirect
 	k8s.io/klog/v2 v2.140.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect

+ 55 - 1
providers/v1/passbolt/passbolt.go

@@ -20,8 +20,11 @@ package passbolt
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
+	"net/http"
 	"net/url"
 	"regexp"
 
@@ -45,6 +48,8 @@ const (
 	errPassboltExternalSecretMissingFindNameRegExp = "missing: find.name.regexp"
 	errPassboltStoreHostSchemeNotHTTPS             = "host Url has to be https scheme"
 	errPassboltSecretPropertyInvalid               = "property must be one of name, username, uri, password or description"
+	errPassboltCAInvalid                           = "failed to parse CA certificate for Passbolt provider"
+	errPassboltUnexpectedTransport                 = "unexpected default http transport type"
 	errNotImplemented                              = "not implemented"
 )
 
@@ -84,7 +89,12 @@ func (provider *ProviderPassbolt) NewClient(ctx context.Context, store esv1.Gene
 		return nil, err
 	}
 
-	client, err := api.NewClient(nil, "", config.Host, privateKey, password)
+	httpClient, err := buildHTTPClient(ctx, config, kube, store.GetKind(), namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := api.NewClient(httpClient, "", config.Host, privateKey, password)
 	if err != nil {
 		return nil, err
 	}
@@ -284,6 +294,50 @@ func assureLoggedIn(ctx context.Context, client *api.Client) error {
 	return client.Login(ctx)
 }
 
+// buildHTTPClient returns an *http.Client configured with the provider's CA bundle
+// or CA provider, if either is set. When neither is set it returns nil so that the
+// underlying SDK uses its default HTTP client (and the system root CAs).
+func buildHTTPClient(ctx context.Context, config *esv1.PassboltProvider, kube kclient.Client, storeKind, namespace string) (*http.Client, error) {
+	if len(config.CABundle) == 0 && config.CAProvider == nil {
+		return nil, nil
+	}
+
+	caCert, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
+		CABundle:   config.CABundle,
+		CAProvider: config.CAProvider,
+		StoreKind:  storeKind,
+		Namespace:  namespace,
+		Client:     kube,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	caCertPool := x509.NewCertPool()
+	if !caCertPool.AppendCertsFromPEM(caCert) {
+		return nil, errors.New(errPassboltCAInvalid)
+	}
+
+	// Clone the default transport so we keep its proxy/dialer/HTTP2/idle
+	// connection settings and only override the TLS configuration.
+	defaultTransport, ok := http.DefaultTransport.(*http.Transport)
+	if !ok {
+		return nil, errors.New(errPassboltUnexpectedTransport)
+	}
+	transport := defaultTransport.Clone()
+	if transport.TLSClientConfig == nil {
+		transport.TLSClientConfig = &tls.Config{}
+	} else {
+		transport.TLSClientConfig = transport.TLSClientConfig.Clone()
+	}
+	transport.TLSClientConfig.RootCAs = caCertPool
+	transport.TLSClientConfig.MinVersion = tls.VersionTLS12
+
+	return &http.Client{
+		Transport: transport,
+	}, nil
+}
+
 // NewProvider creates a new Provider instance.
 func NewProvider() esv1.Provider {
 	return &ProviderPassbolt{}

+ 112 - 0
providers/v1/passbolt/passbolt_test.go

@@ -18,10 +18,24 @@ package passbolt
 
 import (
 	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
 	"errors"
+	"math/big"
+	"net/http"
 	"testing"
+	"time"
 
 	g "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
@@ -147,3 +161,101 @@ func TestGetSecretMap(t *testing.T) {
 	_, err := p.GetSecretMap(context.TODO(), esv1.ExternalSecretDataRemoteRef{})
 	g.Expect(err).To(g.BeEquivalentTo(errors.New(errNotImplemented)))
 }
+
+// generateCABundlePEM creates a self-signed CA certificate in PEM format,
+// for exercising buildHTTPClient without any real PKI dependencies.
+func generateCABundlePEM(t *testing.T) []byte {
+	t.Helper()
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatalf("ecdsa key: %v", err)
+	}
+	tmpl := &x509.Certificate{
+		SerialNumber:          big.NewInt(1),
+		Subject:               pkix.Name{CommonName: "test"},
+		NotBefore:             time.Now().Add(-time.Hour),
+		NotAfter:              time.Now().Add(time.Hour),
+		KeyUsage:              x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+	if err != nil {
+		t.Fatalf("create cert: %v", err)
+	}
+	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
+}
+
+func TestBuildHTTPClient(t *testing.T) {
+	g.RegisterTestingT(t)
+	caPEM := generateCABundlePEM(t)
+
+	scheme := runtime.NewScheme()
+	g.Expect(corev1.AddToScheme(scheme)).To(g.Succeed())
+
+	kubeObjs := []runtime.Object{
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: "passbolt-ca", Namespace: "ns"},
+			Data:       map[string][]byte{"ca.crt": caPEM},
+		},
+	}
+	kube := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(kubeObjs...).Build()
+
+	tests := []struct {
+		name     string
+		provider *esv1.PassboltProvider
+		wantNil  bool
+	}{
+		{
+			name:     "no CA configured returns nil client (SDK default + system roots)",
+			provider: &esv1.PassboltProvider{},
+			wantNil:  true,
+		},
+		{
+			name: "inline caBundle populates the client's RootCAs and leaves default transport settings intact",
+			provider: &esv1.PassboltProvider{
+				CABundle: caPEM,
+			},
+		},
+		{
+			name: "caProvider with a Secret is dereferenced into RootCAs",
+			provider: &esv1.PassboltProvider{
+				CAProvider: &esv1.CAProvider{
+					Type: esv1.CAProviderTypeSecret,
+					Name: "passbolt-ca",
+					Key:  "ca.crt",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client, err := buildHTTPClient(t.Context(), tt.provider, kube, esv1.SecretStoreKind, "ns")
+			g.Expect(err).ToNot(g.HaveOccurred())
+			if tt.wantNil {
+				g.Expect(client).To(g.BeNil())
+				return
+			}
+
+			g.Expect(client).ToNot(g.BeNil())
+			transport, ok := client.Transport.(*http.Transport)
+			g.Expect(ok).To(g.BeTrue())
+			g.Expect(transport.TLSClientConfig).ToNot(g.BeNil())
+			g.Expect(transport.TLSClientConfig.MinVersion).To(g.Equal(uint16(tls.VersionTLS12)))
+
+			// Verify the configured pool actually contains *our* CA, not just any
+			// non-nil pool — guards against silently loading a different bundle.
+			expectedPool := x509.NewCertPool()
+			g.Expect(expectedPool.AppendCertsFromPEM(caPEM)).To(g.BeTrue())
+			g.Expect(transport.TLSClientConfig.RootCAs).ToNot(g.BeNil())
+			g.Expect(transport.TLSClientConfig.RootCAs.Equal(expectedPool)).To(g.BeTrue())
+
+			// Confirm we cloned the default transport and didn't end up with a
+			// bare http.Transport{} (which would drop proxy/dialer defaults).
+			defaultTransport := http.DefaultTransport.(*http.Transport)
+			g.Expect(transport.Proxy).ToNot(g.BeNil(), "cloned transport should keep DefaultTransport.Proxy")
+			g.Expect(transport.MaxIdleConns).To(g.Equal(defaultTransport.MaxIdleConns))
+		})
+	}
+}

+ 6 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -655,6 +655,12 @@ spec:
           key: string
           name: string
           namespace: string
+      caBundle: c3RyaW5n
+      caProvider:
+        key: string
+        name: string
+        namespace: string
+        type: "Secret" # "Secret", "ConfigMap"
       host: string
     passworddepot:
       auth:

+ 6 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -655,6 +655,12 @@ spec:
           key: string
           name: string
           namespace: string
+      caBundle: c3RyaW5n
+      caProvider:
+        key: string
+        name: string
+        namespace: string
+        type: "Secret" # "Secret", "ConfigMap"
       host: string
     passworddepot:
       auth: