Răsfoiți Sursa

feat(openbao): support custom CAs via `caBundle` and `caProvider` (#6461)

* add caBundle and caProvider to API

Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io>

* implement caBundle and caProvider in OpenBao provider

Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io>

* Switch OpenBao e2e test to https

Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io>

---------

Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io>
Philipp Stehle 1 săptămână în urmă
părinte
comite
8cb4c1cd00

+ 13 - 0
apis/externalsecrets/v1/secretstore_openbao_types.go

@@ -28,10 +28,23 @@ const (
 )
 
 // OpenBaoProvider configures a store to sync secrets using an OpenBao KV backend.
+// +kubebuilder:validation:AtMostOneOf=caBundle;caProvider
 type OpenBaoProvider struct {
 	// Auth configures how secret-manager authenticates with the OpenBao server.
 	Auth *OpenBaoAuth `json:"auth,omitempty"`
 
+	// PEM encoded CA bundle used to validate the OpenBao server certificate. If
+	// this and `caProvider` are 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 OpenBao server
+	// certificate. If this and `caBundle` are not set the system root
+	// certificates are used to validate the TLS connection.
+	// +optional
+	CAProvider *CAProvider `json:"caProvider,omitempty"`
+
 	// Server is the connection address for the OpenBao server, e.g: `https://openbao.example.com:8200`.
 	Server string `json:"server"`
 

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

@@ -3054,6 +3054,16 @@ func (in *OpenBaoProvider) DeepCopyInto(out *OpenBaoProvider) {
 		*out = new(OpenBaoAuth)
 		(*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)
+	}
 	if in.Path != nil {
 		in, out := &in.Path, &out.Path
 		*out = new(string)

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

@@ -4205,6 +4205,52 @@ spec:
                                 type: string
                             type: object
                         type: object
+                      caBundle:
+                        description: |-
+                          PEM encoded CA bundle used to validate the OpenBao server certificate. If
+                          this and `caProvider` are 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 OpenBao server
+                          certificate. If this and `caBundle` are not set the system root
+                          certificates are used to validate the TLS connection.
+                        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
                       path:
                         description: |-
                           Path is the mount path of the OpenBao KV backend endpoint, e.g:
@@ -4228,6 +4274,11 @@ spec:
                     required:
                     - server
                     type: object
+                    x-kubernetes-validations:
+                    - message: at most one of the fields in [caBundle caProvider]
+                        may be set
+                      rule: '[has(self.caBundle),has(self.caProvider)].filter(x,x==true).size()
+                        <= 1'
                   oracle:
                     description: Oracle configures this store to sync secrets using
                       Oracle Vault provider

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

@@ -4205,6 +4205,52 @@ spec:
                                 type: string
                             type: object
                         type: object
+                      caBundle:
+                        description: |-
+                          PEM encoded CA bundle used to validate the OpenBao server certificate. If
+                          this and `caProvider` are 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 OpenBao server
+                          certificate. If this and `caBundle` are not set the system root
+                          certificates are used to validate the TLS connection.
+                        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
                       path:
                         description: |-
                           Path is the mount path of the OpenBao KV backend endpoint, e.g:
@@ -4228,6 +4274,11 @@ spec:
                     required:
                     - server
                     type: object
+                    x-kubernetes-validations:
+                    - message: at most one of the fields in [caBundle caProvider]
+                        may be set
+                      rule: '[has(self.caBundle),has(self.caProvider)].filter(x,x==true).size()
+                        <= 1'
                   oracle:
                     description: Oracle configures this store to sync secrets using
                       Oracle Vault provider

+ 92 - 0
deploy/crds/bundle.yaml

@@ -6168,6 +6168,49 @@ spec:
                                   type: string
                               type: object
                           type: object
+                        caBundle:
+                          description: |-
+                            PEM encoded CA bundle used to validate the OpenBao server certificate. If
+                            this and `caProvider` are 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 OpenBao server
+                            certificate. If this and `caBundle` are not set the system root
+                            certificates are used to validate the TLS connection.
+                          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
                         path:
                           description: |-
                             Path is the mount path of the OpenBao KV backend endpoint, e.g:
@@ -6190,6 +6233,9 @@ spec:
                       required:
                         - server
                       type: object
+                      x-kubernetes-validations:
+                        - message: at most one of the fields in [caBundle caProvider] may be set
+                          rule: '[has(self.caBundle),has(self.caProvider)].filter(x,x==true).size() <= 1'
                     oracle:
                       description: Oracle configures this store to sync secrets using Oracle Vault provider
                       properties:
@@ -18629,6 +18675,49 @@ spec:
                                   type: string
                               type: object
                           type: object
+                        caBundle:
+                          description: |-
+                            PEM encoded CA bundle used to validate the OpenBao server certificate. If
+                            this and `caProvider` are 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 OpenBao server
+                            certificate. If this and `caBundle` are not set the system root
+                            certificates are used to validate the TLS connection.
+                          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
                         path:
                           description: |-
                             Path is the mount path of the OpenBao KV backend endpoint, e.g:
@@ -18651,6 +18740,9 @@ spec:
                       required:
                         - server
                       type: object
+                      x-kubernetes-validations:
+                        - message: at most one of the fields in [caBundle caProvider] may be set
+                          rule: '[has(self.caBundle),has(self.caProvider)].filter(x,x==true).size() <= 1'
                     oracle:
                       description: Oracle configures this store to sync secrets using Oracle Vault provider
                       properties:

+ 31 - 0
docs/api/spec.md

@@ -2026,6 +2026,7 @@ string
 <a href="#external-secrets.io/v1.GitlabProvider">GitlabProvider</a>, 
 <a href="#external-secrets.io/v1.InfisicalProvider">InfisicalProvider</a>, 
 <a href="#external-secrets.io/v1.KubernetesServer">KubernetesServer</a>, 
+<a href="#external-secrets.io/v1.OpenBaoProvider">OpenBaoProvider</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>, 
@@ -8387,6 +8388,36 @@ OpenBaoAuth
 </tr>
 <tr>
 <td>
+<code>caBundle</code></br>
+<em>
+[]byte
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>PEM encoded CA bundle used to validate the OpenBao server certificate. If
+this and <code>caProvider</code> are 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 OpenBao server
+certificate. If this and <code>caBundle</code> are not set the system root
+certificates are used to validate the TLS connection.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>server</code></br>
 <em>
 string

+ 44 - 5
e2e/framework/addon/openbao.go

@@ -22,7 +22,9 @@ import (
 	"path/filepath"
 
 	. "github.com/onsi/ginkgo/v2"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	"github.com/external-secrets/external-secrets-e2e/framework/util"
 )
@@ -31,9 +33,13 @@ type OpenBao struct {
 	chart     *HelmChart
 	Namespace string
 
-	InClusterURL string
-	LocalURL     string
-	RootToken    string
+	URLs struct {
+		InClusterPlainText string
+		InClusterTLS       string
+		Local              string
+	}
+	RootToken string
+	ServerCA  []byte
 
 	portForwarder *PortForward
 }
@@ -67,10 +73,42 @@ func NewOpenBao() *OpenBao {
 }
 
 func (l *OpenBao) Install() error {
+	serverRootPem, serverPem, serverKeyPem, _, _, _, err := genVaultCertificates(l.Namespace, l.chart.ReleaseName)
+	if err != nil {
+		return err
+	}
+	l.ServerCA = serverRootPem
+
 	if err := l.chart.Install(); err != nil {
 		return err
 	}
 
+	sec := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "openbao-config",
+			Namespace: l.Namespace,
+		},
+		Data: map[string][]byte{},
+	}
+	_, err = controllerutil.CreateOrUpdate(GinkgoT().Context(), l.chart.config.CRClient, sec, func() error {
+		sec.Data = map[string][]byte{
+			"server-cert.pem":     serverPem,
+			"server-cert-key.pem": serverKeyPem,
+			"config.hcl": []byte(`
+				ui = true
+				listener "tcp" {
+					address = "[::]:8300"
+					tls_cert_file = "/etc/bao/server-cert.pem"
+					tls_key_file = "/etc/bao/server-cert-key.pem"
+				}
+			`),
+		}
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
 	if err := l.initBao(); err != nil {
 		return err
 	}
@@ -96,8 +134,9 @@ func (l *OpenBao) initBao() error {
 		return err
 	}
 
-	l.InClusterURL = fmt.Sprintf("http://%s.%s.svc.cluster.local:8200", l.chart.ReleaseName, l.Namespace)
-	l.LocalURL = fmt.Sprintf("http://localhost:%d", l.portForwarder.localPort)
+	l.URLs.InClusterTLS = fmt.Sprintf("https://%s.%s.svc.cluster.local:8300", l.chart.ReleaseName, l.Namespace)
+	l.URLs.InClusterPlainText = fmt.Sprintf("http://%s.%s.svc.cluster.local:8200", l.chart.ReleaseName, l.Namespace)
+	l.URLs.Local = fmt.Sprintf("http://localhost:%d", l.portForwarder.localPort)
 
 	return nil
 }

+ 17 - 0
e2e/k8s/openbao.values.yaml

@@ -4,3 +4,20 @@ injector:
 server:
   dev:
     enabled: true
+
+  extraArgs: "-dev -config /etc/bao/config.hcl"
+
+  service:
+    extraPorts:
+      - name: https
+        port: 8300
+        targetPort: 8300
+
+  volumeMounts:
+    - name: config
+      mountPath: /etc/bao
+      readOnly: true
+  volumes:
+    - name: config
+      secret:
+        secretName: openbao-config

+ 5 - 4
e2e/suites/provider/cases/openbao/provider.go

@@ -57,7 +57,7 @@ func (s *openBaoProvider) DeleteSecret(key string) {
 }
 
 func (s *openBaoProvider) updateSecret(method string, path string, body io.Reader, expectedStatus int) {
-	req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", s.addon.LocalURL, path), body)
+	req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", s.addon.URLs.Local, path), body)
 	Expect(err).ToNot(HaveOccurred())
 	req.Header.Add("X-Vault-Token", s.addon.RootToken)
 
@@ -83,9 +83,10 @@ func makeStore(name, ns string, v *addon.OpenBao) *esv1.SecretStore {
 		Spec: esv1.SecretStoreSpec{
 			Provider: &esv1.SecretStoreProvider{
 				OpenBao: &esv1.OpenBaoProvider{
-					Version: esv1.OpenBaoKVStoreV2,
-					Path:    new(secretStorePath),
-					Server:  v.InClusterURL,
+					Version:  esv1.OpenBaoKVStoreV2,
+					Path:     new(secretStorePath),
+					Server:   v.URLs.InClusterTLS,
+					CABundle: v.ServerCA,
 				},
 			},
 		},

+ 58 - 4
providers/v1/openbao/client.go

@@ -18,9 +18,12 @@ package openbao
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net/http"
 	"strconv"
 	"time"
 
@@ -41,15 +44,61 @@ var (
 const (
 	errInvalidRevVersion      = "invalid Ref.Version: %w"
 	errSecretKeyNotFound      = "cannot find secret data for key: %q"
+	errFetchMount             = "error while validating %q: %w"
 	errInvalidMountType       = `expected mount type "kv" found %q`
 	errInvalidMountVersion    = "expected kv engine version %s found version %s"
 	errKVv1VersionUnsupported = "OpenBao KVv1 secrets do not support versioning (use KVv2)"
+	errCustomCA               = "cannot set OpenBao CA certificate: %w"
 )
 
 type client struct {
-	client    *api.Client
-	store     *esv1.OpenBaoProvider
-	storeKind string
+	client     *api.Client
+	httpClient *http.Client
+	store      *esv1.OpenBaoProvider
+	storeKind  string
+}
+
+func (c *client) setup(ctx context.Context, kube k8sClient.Client, namespace string, httpClient httpClientFactory) error {
+	c.httpClient = httpClient()
+
+	config := api.DefaultConfig()
+	config.HttpClient = c.httpClient
+	config.Address = c.store.Server
+
+	if len(c.store.CABundle) != 0 || c.store.CAProvider != nil {
+		caCertPool := x509.NewCertPool()
+		ca, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
+			CABundle:   c.store.CABundle,
+			CAProvider: c.store.CAProvider,
+			StoreKind:  c.storeKind,
+			Namespace:  namespace,
+			Client:     kube,
+		})
+		if err != nil {
+			return fmt.Errorf(errCustomCA, err)
+		}
+		ok := caCertPool.AppendCertsFromPEM(ca)
+		if !ok {
+			return fmt.Errorf(errCustomCA, errors.New("failed add certificate to CertPool"))
+		}
+
+		if transport, ok := config.HttpClient.Transport.(*http.Transport); ok {
+			transport = transport.Clone()
+			if transport.TLSClientConfig == nil {
+				transport.TLSClientConfig = &tls.Config{}
+			}
+			transport.TLSClientConfig.RootCAs = caCertPool
+			config.HttpClient.Transport = transport
+		}
+	}
+
+	client, err := api.NewClient(config)
+	if err != nil {
+		return err
+	}
+	c.client = client
+
+	return c.setupAuth(ctx, kube, namespace)
 }
 
 func (c *client) setupAuth(ctx context.Context, kube k8sClient.Client, namespace string) error {
@@ -70,7 +119,12 @@ func (c *client) setupAuth(ctx context.Context, kube k8sClient.Client, namespace
 }
 
 func (c *client) Close(_ context.Context) error {
+	if c.httpClient != nil {
+		c.httpClient.CloseIdleConnections()
+		c.httpClient = nil
+	}
 	c.client = nil
+	c.store = nil
 	return nil
 }
 
@@ -231,7 +285,7 @@ func (c *client) Validate() (esv1.ValidationResult, error) {
 
 	mount, err := c.client.Sys().MountInfoWithContext(ctx, c.path())
 	if err != nil {
-		return esv1.ValidationResultError, err
+		return esv1.ValidationResultError, fmt.Errorf(errFetchMount, c.store.Server, err)
 	}
 
 	if mount.Type != "kv" {

+ 16 - 17
providers/v1/openbao/provider.go

@@ -23,7 +23,6 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/openbao/openbao/api/v2"
 	k8sClient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
@@ -35,9 +34,11 @@ var (
 
 // Provider implements the ESO Provider interface for OpenBao.
 type Provider struct {
-	HTTPClient *http.Client
+	HTTPClientFactory httpClientFactory
 }
 
+type httpClientFactory func() *http.Client
+
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
 func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
 	return esv1.SecretStoreReadOnly
@@ -47,23 +48,13 @@ func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
 func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube k8sClient.Client, namespace string) (esv1.SecretsClient, error) {
 	spec := store.GetSpec().Provider.OpenBao // if this is somehow nil, there is a bug in the framework
 
-	baoConfig := api.DefaultConfig()
-	baoConfig.HttpClient = p.HTTPClient
-	baoConfig.Address = spec.Server
-
-	baoClient, err := api.NewClient(baoConfig)
-	if err != nil {
-		return nil, err
-	}
-
 	client := &client{
-		client:    baoClient,
 		storeKind: store.GetKind(),
 		store:     spec,
 	}
 
 	if client.storeKind != esv1.ClusterSecretStoreKind || namespace != "" || !isReferentSpec(spec) {
-		err = client.setupAuth(ctx, kube, namespace)
+		err := client.setup(ctx, kube, namespace, p.HTTPClientFactory)
 		if err != nil {
 			return nil, err
 		}
@@ -73,11 +64,15 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 }
 
 func isReferentSpec(prov *esv1.OpenBaoProvider) bool {
-	if prov.Auth == nil {
-		return false
+	if prov.Auth != nil {
+		auth := prov.Auth
+
+		if auth.TokenSecretRef != nil && auth.TokenSecretRef.Namespace == nil {
+			return true
+		}
 	}
 
-	if prov.Auth.TokenSecretRef != nil && prov.Auth.TokenSecretRef.Namespace == nil {
+	if prov.CAProvider != nil && prov.CAProvider.Namespace == nil {
 		return true
 	}
 
@@ -87,7 +82,11 @@ func isReferentSpec(prov *esv1.OpenBaoProvider) bool {
 // NewProvider creates a new Provider instance.
 func NewProvider() esv1.Provider {
 	return &Provider{
-		HTTPClient: http.DefaultClient,
+		HTTPClientFactory: func() *http.Client {
+			return &http.Client{
+				Transport: http.DefaultTransport.(*http.Transport).Clone(),
+			}
+		},
 	}
 }
 

+ 115 - 1
providers/v1/openbao/provider_test.go

@@ -17,13 +17,16 @@ limitations under the License.
 package openbao_test
 
 import (
+	"crypto/x509"
 	"encoding/json"
 	"fmt"
+	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"regexp"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -386,12 +389,123 @@ func TestProvider_Validate(t *testing.T) {
 	Expect(client.Validate()).To(Equal(esv1.ValidationResultUnknown))
 }
 
+var dummyCA = []byte(`-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----`)
+
+func TestProvider_CustomCA(t *testing.T) {
+	cases := []struct {
+		name          string
+		spec          esv1.OpenBaoProvider
+		k8sObjects    []client.Object
+		expectedError string
+	}{
+		{
+			name: "CABundle",
+			spec: esv1.OpenBaoProvider{
+				CABundle: dummyCA,
+			},
+		},
+		{
+			name: "CABundle_invalid",
+			spec: esv1.OpenBaoProvider{
+				CABundle: []byte("invalid"),
+			},
+			expectedError: "cannot set OpenBao CA certificate: failed to decode ca bundle: failed to parse the new certificate, not valid pem data",
+		},
+		{
+			name: "CAProvider",
+			spec: esv1.OpenBaoProvider{
+				CAProvider: &esv1.CAProvider{
+					Type: esv1.CAProviderTypeSecret,
+					Name: "dummy-ca",
+					Key:  "ca.pem",
+				},
+			},
+			k8sObjects: []client.Object{&corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "dummy-ca",
+					Namespace: "default",
+				},
+				Data: map[string][]byte{
+					"ca.pem": dummyCA,
+				},
+			}},
+		},
+		{
+			name: "CAProvider_not_found",
+			spec: esv1.OpenBaoProvider{
+				CAProvider: &esv1.CAProvider{
+					Type: esv1.CAProviderTypeSecret,
+					Name: "dummy-ca",
+					Key:  "ca.pem",
+				},
+			},
+			expectedError: `cannot set OpenBao CA certificate: failed to get cert from secret: failed to resolve secret key ref: cannot get Kubernetes secret "dummy-ca" from namespace "default": secrets "dummy-ca" not found`,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			RegisterTestingT(t)
+
+			kube := clientfake.NewClientBuilder().WithObjects(tc.k8sObjects...).Build()
+			provider := openbao.NewProvider().(*openbao.Provider)
+
+			originalFactory := provider.HTTPClientFactory
+			var httpClient atomic.Pointer[http.Client]
+			var factoryCallCount atomic.Int64
+			provider.HTTPClientFactory = func() *http.Client {
+				c := originalFactory()
+				httpClient.Store(c)
+				factoryCallCount.Add(1)
+				return c
+			}
+
+			store := makeValidSecretStoreWithVersion(esv1.OpenBaoKVStoreV2)
+			store.Spec.Provider.OpenBao = &tc.spec
+
+			client, err := provider.NewClient(t.Context(), store, kube, "default")
+
+			if tc.expectedError != "" {
+				Expect(err).To(MatchError(tc.expectedError))
+				Expect(client).To(BeNil())
+			} else {
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client).NotTo(BeNil())
+
+				expectedPool := x509.NewCertPool()
+				Expect(expectedPool.AppendCertsFromPEM(dummyCA)).To(BeTrue())
+
+				Expect(factoryCallCount.Load()).To(BeEquivalentTo(1))
+				transport := httpClient.Load().Transport
+				Expect(transport).To(BeAssignableToTypeOf(&http.Transport{}))
+				tls := transport.(*http.Transport).TLSClientConfig
+				Expect(tls.RootCAs.Equal(expectedPool)).To(BeTrueBecause("root CAs should equal expected cert pool"))
+
+				client.Close(t.Context())
+			}
+		})
+	}
+}
+
 func setupClient(t *testing.T, v esv1.OpenBaoKVStoreVersion) esv1.SecretsClient {
 	kube, provider := setupProvider(t)
 
 	client, err := provider.NewClient(t.Context(), makeValidSecretStoreWithVersion(v), kube, "default")
 	Expect(err).NotTo(HaveOccurred())
 	Expect(client).NotTo(BeNil())
+	t.Cleanup(func() {
+		client.Close(t.Context())
+	})
 	return client
 }
 
@@ -409,6 +523,6 @@ func setupProvider(t *testing.T) (client.WithWatch, *openbao.Provider) {
 	}).Build()
 
 	provider := openbao.NewProvider().(*openbao.Provider)
-	provider.HTTPClient = r.GetDefaultClient()
+	provider.HTTPClientFactory = r.GetDefaultClient
 	return kube, provider
 }

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

@@ -623,6 +623,12 @@ spec:
           key: string
           name: string
           namespace: string
+      caBundle: c3RyaW5n
+      caProvider:
+        key: string
+        name: string
+        namespace: string
+        type: "Secret" # "Secret", "ConfigMap"
       path: string
       server: string
       version: "v2"

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

@@ -623,6 +623,12 @@ spec:
           key: string
           name: string
           namespace: string
+      caBundle: c3RyaW5n
+      caProvider:
+        key: string
+        name: string
+        namespace: string
+        type: "Secret" # "Secret", "ConfigMap"
       path: string
       server: string
       version: "v2"