You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2021/04/14 20:04:13 UTC

[solr-operator] branch main updated: Add mTLS support for the Operator communicating with SolrClouds (#256)

This is an automated email from the ASF dual-hosted git repository.

houston pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 01acbba  Add mTLS support for the Operator communicating with SolrClouds (#256)
01acbba is described below

commit 01acbbad76302da39c8d59b1d9c686c4c3ad31ca
Author: Timothy Potter <th...@gmail.com>
AuthorDate: Wed Apr 14 14:04:02 2021 -0600

    Add mTLS support for the Operator communicating with SolrClouds (#256)
    
    Co-authored-by: Houston Putman <ho...@apache.org>
---
 api/v1beta1/solrcloud_types.go                     |   9 ++
 api/v1beta1/zz_generated.deepcopy.go               |  10 ++
 config/crd/bases/solr.apache.org_solrclouds.yaml   |  30 ++++
 .../solr.apache.org_solrprometheusexporters.yaml   |  30 ++++
 controllers/controller_utils_test.go               |  57 +++++++-
 controllers/solrcloud_controller.go                |  57 +++++---
 controllers/solrcloud_controller_tls_test.go       |  44 ++++++
 controllers/util/prometheus_exporter_util.go       |   4 +-
 controllers/util/solr_api/api.go                   |  19 ++-
 controllers/util/solr_util.go                      | 158 +++++++++++++++------
 docs/running-the-operator.md                       |  28 +++-
 docs/solr-cloud/solr-cloud-crd.md                  |  46 ++++++
 helm/solr-operator/README.md                       |   4 +
 helm/solr-operator/crds/crds.yaml                  |  60 ++++++++
 helm/solr-operator/templates/_helpers.tpl          |  45 ++++++
 helm/solr-operator/templates/deployment.yaml       |  19 +++
 helm/solr-operator/values.yaml                     |   7 +
 main.go                                            |  58 +++++++-
 18 files changed, 605 insertions(+), 80 deletions(-)

diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index f1da3a1..2182e73 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -1052,6 +1052,15 @@ type SolrTLSOptions struct {
 	// Secret containing the key store password; this field is required as most JVMs do not support pkcs12 keystores without a password
 	KeyStorePasswordSecret *corev1.SecretKeySelector `json:"keyStorePasswordSecret"`
 
+	// TLS Secret containing a pkcs12 truststore; if not provided, then the keystore and password are used for the truststore
+	// The specified key is used as the truststore file name when mounted into Solr pods
+	// +optional
+	TrustStoreSecret *corev1.SecretKeySelector `json:"trustStoreSecret,omitempty"`
+
+	// Secret containing the trust store password; if not provided the keyStorePassword will be used
+	// +optional
+	TrustStorePasswordSecret *corev1.SecretKeySelector `json:"trustStorePasswordSecret,omitempty"`
+
 	// Determines the client authentication method, either None, Want, or Need;
 	// this affects K8s ability to call liveness / readiness probes so use cautiously.
 	// +kubebuilder:default=None
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index b93ff93..b3236f9 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -1085,6 +1085,16 @@ func (in *SolrTLSOptions) DeepCopyInto(out *SolrTLSOptions) {
 		*out = new(v1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.TrustStoreSecret != nil {
+		in, out := &in.TrustStoreSecret, &out.TrustStoreSecret
+		*out = new(v1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.TrustStorePasswordSecret != nil {
+		in, out := &in.TrustStorePasswordSecret, &out.TrustStorePasswordSecret
+		*out = new(v1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SolrTLSOptions.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml
index fbf620b..8d8d806 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -4579,6 +4579,36 @@ spec:
                   restartOnTLSSecretUpdate:
                     description: Opt-in flag to restart Solr pods after TLS secret updates, such as if the cert is renewed; default is false.
                     type: boolean
+                  trustStorePasswordSecret:
+                    description: Secret containing the trust store password; if not provided the keyStorePassword will be used
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
+                  trustStoreSecret:
+                    description: TLS Secret containing a pkcs12 truststore; if not provided, then the keystore and password are used for the truststore The specified key is used as the truststore file name when mounted into Solr pods
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
                   verifyClientHostname:
                     description: Verify client's hostname during SSL handshake
                     type: boolean
diff --git a/config/crd/bases/solr.apache.org_solrprometheusexporters.yaml b/config/crd/bases/solr.apache.org_solrprometheusexporters.yaml
index c77a5bb..5b008c9 100644
--- a/config/crd/bases/solr.apache.org_solrprometheusexporters.yaml
+++ b/config/crd/bases/solr.apache.org_solrprometheusexporters.yaml
@@ -3460,6 +3460,36 @@ spec:
                       restartOnTLSSecretUpdate:
                         description: Opt-in flag to restart Solr pods after TLS secret updates, such as if the cert is renewed; default is false.
                         type: boolean
+                      trustStorePasswordSecret:
+                        description: Secret containing the trust store password; if not provided the keyStorePassword will be used
+                        properties:
+                          key:
+                            description: The key of the secret to select from.  Must be a valid secret key.
+                            type: string
+                          name:
+                            description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                            type: string
+                          optional:
+                            description: Specify whether the Secret or its key must be defined
+                            type: boolean
+                        required:
+                        - key
+                        type: object
+                      trustStoreSecret:
+                        description: TLS Secret containing a pkcs12 truststore; if not provided, then the keystore and password are used for the truststore The specified key is used as the truststore file name when mounted into Solr pods
+                        properties:
+                          key:
+                            description: The key of the secret to select from.  Must be a valid secret key.
+                            type: string
+                          name:
+                            description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                            type: string
+                          optional:
+                            description: Specify whether the Secret or its key must be defined
+                            type: boolean
+                        required:
+                        - key
+                        type: object
                       verifyClientHostname:
                         description: Verify client's hostname during SSL handshake
                         type: boolean
diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go
index 9430e7e..334ced9 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -189,7 +189,14 @@ func verifyUserSuppliedTLSConfig(t *testing.T, tls *solr.SolrTLSOptions, expecte
 	assert.Equal(t, expectedKeystorePasswordSecretKey, tls.KeyStorePasswordSecret.Key)
 	assert.Equal(t, expectedTlsSecretName, tls.PKCS12Secret.Name)
 	assert.Equal(t, "keystore.p12", tls.PKCS12Secret.Key)
-	expectTLSEnvVars(t, util.TLSEnvVars(tls, needsPkcs12InitContainer), expectedKeystorePasswordSecretName, expectedKeystorePasswordSecretKey, needsPkcs12InitContainer)
+
+	// is there a separate truststore?
+	expectedTrustStorePath := ""
+	if tls.TrustStoreSecret != nil {
+		expectedTrustStorePath = util.DefaultTrustStorePath + "/" + tls.TrustStoreSecret.Key
+	}
+
+	expectTLSEnvVars(t, util.TLSEnvVars(tls, needsPkcs12InitContainer), expectedKeystorePasswordSecretName, expectedKeystorePasswordSecretKey, needsPkcs12InitContainer, expectedTrustStorePath)
 }
 
 func createTLSOptions(tlsSecretName string, keystorePassKey string, restartOnTLSSecretUpdate bool) *solr.SolrTLSOptions {
@@ -249,7 +256,28 @@ func expectTLSConfigOnPodTemplate(t *testing.T, tls *solr.SolrTLSOptions, podTem
 	mainContainer := podTemplate.Spec.Containers[0]
 	assert.NotNil(t, mainContainer, "Didn't find the main solrcloud-node container in the sts!")
 	assert.NotNil(t, mainContainer.Env, "Didn't find the main solrcloud-node container in the sts!")
-	expectTLSEnvVars(t, mainContainer.Env, tls.KeyStorePasswordSecret.Name, tls.KeyStorePasswordSecret.Key, needsPkcs12InitContainer)
+
+	// is there a separate truststore?
+	expectedTrustStorePath := ""
+	if tls.TrustStoreSecret != nil {
+		expectedTrustStorePath = util.DefaultTrustStorePath + "/" + tls.TrustStoreSecret.Key
+	}
+
+	expectTLSEnvVars(t, mainContainer.Env, tls.KeyStorePasswordSecret.Name, tls.KeyStorePasswordSecret.Key, needsPkcs12InitContainer, expectedTrustStorePath)
+
+	// different trust store?
+	if tls.TrustStoreSecret != nil {
+		var truststoreVol *corev1.Volume = nil
+		for _, vol := range podTemplate.Spec.Volumes {
+			if vol.Name == "truststore" {
+				truststoreVol = &vol
+				break
+			}
+		}
+		assert.NotNil(t, truststoreVol, fmt.Sprintf("truststore volume not found in pod template; volumes: %v", podTemplate.Spec.Volumes))
+		assert.NotNil(t, truststoreVol.VolumeSource.Secret, "Didn't find TLS truststore volume in sts config!")
+		assert.Equal(t, tls.TrustStoreSecret.Name, truststoreVol.VolumeSource.Secret.SecretName)
+	}
 
 	// initContainers
 	if needsPkcs12InitContainer {
@@ -277,11 +305,23 @@ func expectTLSConfigOnPodTemplate(t *testing.T, tls *solr.SolrTLSOptions, podTem
 		assert.Equal(t, expCmd, expInitContainer.Command[2])
 	}
 
+	if tls.ClientAuth == solr.Need {
+		// verify the probes use a command with SSL opts
+		tlsProps := "-Djavax.net.ssl.keyStore=$SOLR_SSL_KEY_STORE -Djavax.net.ssl.keyStorePassword=$SOLR_SSL_KEY_STORE_PASSWORD " +
+			"-Djavax.net.ssl.trustStore=$SOLR_SSL_TRUST_STORE -Djavax.net.ssl.trustStorePassword=$SOLR_SSL_TRUST_STORE_PASSWORD"
+		assert.NotNil(t, mainContainer.LivenessProbe, "main container should have a liveness probe defined")
+		assert.NotNil(t, mainContainer.LivenessProbe.Exec, "liveness probe should have an exec when auth is enabled")
+		assert.True(t, strings.Contains(mainContainer.LivenessProbe.Exec.Command[2], tlsProps), "liveness probe should invoke java with SSL opts")
+		assert.NotNil(t, mainContainer.ReadinessProbe, "main container should have a readiness probe defined")
+		assert.NotNil(t, mainContainer.ReadinessProbe.Exec, "readiness probe should have an exec when auth is enabled")
+		assert.True(t, strings.Contains(mainContainer.ReadinessProbe.Exec.Command[2], tlsProps), "readiness probe should invoke java with SSL opts")
+	}
+
 	return &mainContainer // return as a convenience in case tests want to do more checking on the main container
 }
 
 // ensure the TLS related env vars are set for the Solr pod
-func expectTLSEnvVars(t *testing.T, envVars []corev1.EnvVar, expectedKeystorePasswordSecretName string, expectedKeystorePasswordSecretKey string, needsPkcs12InitContainer bool) {
+func expectTLSEnvVars(t *testing.T, envVars []corev1.EnvVar, expectedKeystorePasswordSecretName string, expectedKeystorePasswordSecretKey string, needsPkcs12InitContainer bool, expectedTruststorePath string) {
 	assert.NotNil(t, envVars)
 	envVars = filterVarsByName(envVars, func(n string) bool {
 		return strings.HasPrefix(n, "SOLR_SSL_")
@@ -292,15 +332,24 @@ func expectTLSEnvVars(t *testing.T, envVars []corev1.EnvVar, expectedKeystorePas
 	if needsPkcs12InitContainer {
 		expectedKeystorePath = util.DefaultWritableKeyStorePath + "/keystore.p12"
 	}
+
+	if expectedTruststorePath == "" {
+		expectedTruststorePath = expectedKeystorePath
+	}
+
 	for _, envVar := range envVars {
 		if envVar.Name == "SOLR_SSL_ENABLED" {
 			assert.Equal(t, "true", envVar.Value)
 		}
 
-		if envVar.Name == "SOLR_SSL_TRUST_STORE" {
+		if envVar.Name == "SOLR_SSL_KEY_STORE" {
 			assert.Equal(t, expectedKeystorePath, envVar.Value)
 		}
 
+		if envVar.Name == "SOLR_SSL_TRUST_STORE" {
+			assert.Equal(t, expectedTruststorePath, envVar.Value)
+		}
+
 		if envVar.Name == "SOLR_SSL_KEY_STORE_PASSWORD" {
 			assert.NotNil(t, envVar.ValueFrom)
 			assert.NotNil(t, envVar.ValueFrom.SecretKeyRef)
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index 30a3d62..9bd11e1 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -367,25 +367,10 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	needsPkcs12InitContainer := false // flag if the StatefulSet needs an additional initCont to create PKCS12 keystore
 	// don't start reconciling TLS until we have ZK connectivity, avoids TLS code having to check for ZK
 	if !blockReconciliationOfStatefulSet && instance.Spec.SolrTLS != nil {
-		ctx := context.TODO()
-		foundTLSSecret := &corev1.Secret{}
-		lookupErr := r.Get(ctx, types.NamespacedName{Name: instance.Spec.SolrTLS.PKCS12Secret.Name, Namespace: instance.Namespace}, foundTLSSecret)
-		if lookupErr != nil {
-			return requeueOrNot, lookupErr
+		foundTLSSecret, err := r.verifyTLSSecretConfig(instance.Spec.SolrTLS.PKCS12Secret.Name, instance.Namespace, instance.Spec.SolrTLS.KeyStorePasswordSecret)
+		if err != nil {
+			return requeueOrNot, err
 		} else {
-			// Make sure the secret containing the keystore password exists as well
-			keyStorePasswordSecret := &corev1.Secret{}
-			err := r.Get(ctx, types.NamespacedName{Name: instance.Spec.SolrTLS.KeyStorePasswordSecret.Name, Namespace: foundTLSSecret.Namespace}, keyStorePasswordSecret)
-			if err != nil {
-				return requeueOrNot, lookupErr
-			}
-
-			// we found the keystore secret, but does it have the key we expect?
-			if _, ok := keyStorePasswordSecret.Data[instance.Spec.SolrTLS.KeyStorePasswordSecret.Key]; !ok {
-				return requeueOrNot, fmt.Errorf("%s key not found in keystore password secret %s",
-					instance.Spec.SolrTLS.KeyStorePasswordSecret.Key, keyStorePasswordSecret.Name)
-			}
-
 			// We have a watch on secrets, so will get notified when the secret changes (such as after cert renewal)
 			// capture the hash of the secret and stash in an annotation so that pods get restarted if the cert changes
 			if instance.Spec.SolrTLS.RestartOnTLSSecretUpdate {
@@ -403,6 +388,18 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 				needsPkcs12InitContainer = true
 			}
 		}
+
+		if instance.Spec.SolrTLS.TrustStoreSecret != nil {
+			// verify the TrustStore secret is configured correctly
+			passwordSecret := instance.Spec.SolrTLS.TrustStorePasswordSecret
+			if passwordSecret == nil {
+				passwordSecret = instance.Spec.SolrTLS.KeyStorePasswordSecret
+			}
+			_, err := r.verifyTLSSecretConfig(instance.Spec.SolrTLS.TrustStoreSecret.Name, instance.Namespace, passwordSecret)
+			if err != nil {
+				return requeueOrNot, err
+			}
+		}
 	}
 
 	pvcLabelSelector := make(map[string]string, 0)
@@ -997,3 +994,27 @@ func (r *SolrCloudReconciler) indexAndWatchForTLSSecret(mgr ctrl.Manager, ctrlBu
 		},
 		builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})), nil
 }
+
+func (r *SolrCloudReconciler) verifyTLSSecretConfig(secretName string, secretNamespace string, passwordSecret *corev1.SecretKeySelector) (*corev1.Secret, error) {
+	ctx := context.TODO()
+
+	foundTLSSecret := &corev1.Secret{}
+	lookupErr := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: secretNamespace}, foundTLSSecret)
+	if lookupErr != nil {
+		return nil, lookupErr
+	} else {
+		// Make sure the secret containing the keystore password exists as well
+		keyStorePasswordSecret := &corev1.Secret{}
+		err := r.Get(ctx, types.NamespacedName{Name: passwordSecret.Name, Namespace: foundTLSSecret.Namespace}, keyStorePasswordSecret)
+		if err != nil {
+			return nil, lookupErr
+		}
+
+		// we found the keystore secret, but does it have the key we expect?
+		if _, ok := keyStorePasswordSecret.Data[passwordSecret.Key]; !ok {
+			return nil, fmt.Errorf("%s key not found in keystore password secret %s", passwordSecret.Key, keyStorePasswordSecret.Name)
+		}
+	}
+
+	return foundTLSSecret, nil
+}
diff --git a/controllers/solrcloud_controller_tls_test.go b/controllers/solrcloud_controller_tls_test.go
index db23661..254cece 100644
--- a/controllers/solrcloud_controller_tls_test.go
+++ b/controllers/solrcloud_controller_tls_test.go
@@ -19,6 +19,7 @@ package controllers
 
 import (
 	"crypto/md5"
+	b64 "encoding/base64"
 	"fmt"
 	solr "github.com/apache/solr-operator/api/v1beta1"
 	"github.com/apache/solr-operator/controllers/util"
@@ -132,6 +133,32 @@ func TestUserSuppliedTLSSecretWithPkcs12Keystore(t *testing.T) {
 	verifyReconcileUserSuppliedTLS(t, instance, false, false)
 }
 
+// User wants a different trust store than the keystore
+func TestUserSuppliedTLSSecretWithSeparateTrustStore(t *testing.T) {
+	tlsSecretName := "tls-cert-secret-from-user"
+	keystorePassKey := "some-password-key-thingy"
+	instance := buildTestSolrCloud()
+	instance.Spec.SolrSecurity = &solr.SolrSecurityOptions{AuthenticationType: solr.Basic} // with basic-auth too
+	instance.Spec.SolrTLS = createTLSOptions(tlsSecretName, keystorePassKey, false)
+
+	trustStoreSecretName := "custom-truststore-secret"
+	trustStoreFile := "truststore.p12"
+	instance.Spec.SolrTLS.TrustStoreSecret = &corev1.SecretKeySelector{
+		LocalObjectReference: corev1.LocalObjectReference{Name: trustStoreSecretName},
+		Key:                  trustStoreFile,
+	}
+
+	instance.Spec.SolrTLS.TrustStorePasswordSecret = &corev1.SecretKeySelector{
+		LocalObjectReference: corev1.LocalObjectReference{Name: trustStoreSecretName},
+		Key:                  "truststore-pass",
+	}
+
+	instance.Spec.SolrTLS.ClientAuth = solr.Need // require client auth too (mTLS between the pods)
+
+	verifyUserSuppliedTLSConfig(t, instance.Spec.SolrTLS, tlsSecretName, keystorePassKey, tlsSecretName, false)
+	verifyReconcileUserSuppliedTLS(t, instance, false, false)
+}
+
 // Test upgrade from non-TLS cluster to TLS enabled cluster
 func TestEnableTLSOnExistingCluster(t *testing.T) {
 
@@ -201,6 +228,7 @@ func TestTLSSecretUpdate(t *testing.T) {
 	instance := buildTestSolrCloud()
 	instance.Spec.SolrSecurity = &solr.SolrSecurityOptions{AuthenticationType: solr.Basic}
 	instance.Spec.SolrTLS = createTLSOptions(tlsSecretName, keystorePassKey, true)
+	instance.Spec.SolrTLS.ClientAuth = solr.Need
 	verifyUserSuppliedTLSConfig(t, instance.Spec.SolrTLS, tlsSecretName, keystorePassKey, tlsSecretName, false)
 	verifyReconcileUserSuppliedTLS(t, instance, false, true)
 }
@@ -232,6 +260,22 @@ func verifyReconcileUserSuppliedTLS(t *testing.T, instance *solr.SolrCloud, need
 
 	cleanupTest(g, instance.Namespace)
 
+	// Custom truststore?
+	if instance.Spec.SolrTLS.TrustStoreSecret != nil {
+		// create the mock truststore secret
+		secretData := map[string][]byte{}
+		secretData[instance.Spec.SolrTLS.TrustStoreSecret.Key] = []byte(b64.StdEncoding.EncodeToString([]byte("mock truststore")))
+		secretData[instance.Spec.SolrTLS.TrustStorePasswordSecret.Key] = []byte(b64.StdEncoding.EncodeToString([]byte("mock truststore password")))
+		trustStoreSecret := corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: instance.Spec.SolrTLS.TrustStoreSecret.Name, Namespace: instance.Namespace},
+			Data:       secretData,
+			Type:       corev1.SecretTypeOpaque,
+		}
+		err := testClient.Create(ctx, &trustStoreSecret)
+		g.Expect(err).NotTo(gomega.HaveOccurred())
+		defer testClient.Delete(ctx, &trustStoreSecret)
+	}
+
 	// create the secret required for reconcile, it has both keys ...
 	tlsKey := "keystore.p12"
 	if needsPkcs12InitContainer {
diff --git a/controllers/util/prometheus_exporter_util.go b/controllers/util/prometheus_exporter_util.go
index f82c899..143c247 100644
--- a/controllers/util/prometheus_exporter_util.go
+++ b/controllers/util/prometheus_exporter_util.go
@@ -171,7 +171,7 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
 
 	if tls != nil {
 		envVars = append(envVars, TLSEnvVars(tls.TLSOptions, tls.NeedsPkcs12InitContainer)...)
-		volumeMounts = append(volumeMounts, tlsVolumeMounts(tls.NeedsPkcs12InitContainer)...)
+		volumeMounts = append(volumeMounts, tlsVolumeMounts(tls.TLSOptions, tls.NeedsPkcs12InitContainer)...)
 		solrVolumes = append(solrVolumes, tlsVolumes(tls.TLSOptions, tls.NeedsPkcs12InitContainer)...)
 		allJavaOpts = append(allJavaOpts, tlsJavaOpts(tls.TLSOptions)...)
 	}
@@ -234,7 +234,7 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP
 
 	// if the supplied TLS secret does not have the pkcs12 keystore, use an initContainer to create its
 	if tls != nil && tls.NeedsPkcs12InitContainer {
-		pkcs12InitContainer := generatePkcs12InitContainer(tls.TLSOptions.KeyStorePasswordSecret,
+		pkcs12InitContainer := generatePkcs12InitContainer(tls.TLSOptions,
 			solrPrometheusExporter.Spec.Image.ToImageName(), solrPrometheusExporter.Spec.Image.PullPolicy)
 		initContainers = append(initContainers, pkcs12InitContainer)
 	}
diff --git a/controllers/util/solr_api/api.go b/controllers/util/solr_api/api.go
index df080e9..8a6dd55 100644
--- a/controllers/util/solr_api/api.go
+++ b/controllers/util/solr_api/api.go
@@ -18,6 +18,7 @@
 package solr_api
 
 import (
+	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	solr "github.com/apache/solr-operator/api/v1beta1"
@@ -31,11 +32,16 @@ import (
 // It's "insecure" but is only used for internal communication, such as getting cluster status
 // so if you're worried about this, don't use a self-signed cert
 var noVerifyTLSHttpClient *http.Client
+var mTLSHttpClient *http.Client
 
 func SetNoVerifyTLSHttpClient(client *http.Client) {
 	noVerifyTLSHttpClient = client
 }
 
+func SetMTLSHttpClient(client *http.Client) {
+	mTLSHttpClient = client
+}
+
 type SolrAsyncResponse struct {
 	ResponseHeader SolrResponseHeader `json:"responseHeader"`
 
@@ -62,9 +68,9 @@ type SolrAsyncStatus struct {
 func CallCollectionsApi(cloud *solr.SolrCloud, urlParams url.Values, httpHeaders map[string]string, response interface{}) (err error) {
 	cloudUrl := solr.InternalURLForCloud(cloud)
 
-	client := http.DefaultClient
-	if cloud.Spec.SolrTLS != nil {
-		client = noVerifyTLSHttpClient
+	client := noVerifyTLSHttpClient
+	if mTLSHttpClient != nil {
+		client = mTLSHttpClient
 	}
 
 	urlParams.Set("wt", "json")
@@ -99,3 +105,10 @@ func CallCollectionsApi(cloud *solr.SolrCloud, urlParams url.Values, httpHeaders
 
 	return err
 }
+
+func init() {
+	// setup an http client that can talk to Solr pods using untrusted, self-signed certs
+	customTransport := http.DefaultTransport.(*http.Transport).Clone()
+	customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+	SetNoVerifyTLSHttpClient(&http.Client{Transport: customTransport})
+}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 224291b..508aabc 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -29,6 +29,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"math/rand"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -84,6 +85,8 @@ const (
 	Pkcs12KeystoreFile          = "keystore.p12"
 	DefaultWritableKeyStorePath = "/var/solr/tls/pkcs12"
 	TLSCertKey                  = "tls.crt"
+	TLSKeyKey                   = "tls.key"
+	DefaultTrustStorePath       = "/var/solr/tls-truststore"
 )
 
 // GenerateStatefulSet returns a new appsv1.StatefulSet pointer generated for the SolrCloud instance
@@ -169,7 +172,7 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
 
 	if solrCloud.Spec.SolrTLS != nil {
 		solrVolumes = append(solrVolumes, tlsVolumes(solrCloud.Spec.SolrTLS, createPkcs12InitContainer)...)
-		volumeMounts = append(volumeMounts, tlsVolumeMounts(createPkcs12InitContainer)...)
+		volumeMounts = append(volumeMounts, tlsVolumeMounts(solrCloud.Spec.SolrTLS, createPkcs12InitContainer)...)
 	}
 
 	var pvcs []corev1.PersistentVolumeClaim
@@ -377,45 +380,14 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
 		}
 	}
 
-	if solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
-
-		// mount the secret in a file so it gets updated; env vars do not see:
-		// https://kubernetes.io/docs/concepts/configuration/secret/#environment-variables-are-not-updated-after-a-secret-update
-		secretName := solrCloud.BasicAuthSecretName()
-		defaultMode := int32(420)
-		vol := &corev1.Volume{
-			Name: strings.ReplaceAll(secretName, ".", "-"),
-			VolumeSource: corev1.VolumeSource{
-				Secret: &corev1.SecretVolumeSource{
-					SecretName:  secretName,
-					DefaultMode: &defaultMode,
-				},
-			},
+	if (solrCloud.Spec.SolrTLS != nil && solrCloud.Spec.SolrTLS.ClientAuth != solr.None) || (solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.ProbesRequireAuth) {
+		probeCommand, vol, volMount := configureSecureProbeCommand(solrCloud, defaultHandler.HTTPGet)
+		if vol != nil {
+			solrVolumes = append(solrVolumes, *vol)
 		}
-		solrVolumes = append(solrVolumes, *vol)
-		mountPath := fmt.Sprintf("/etc/secrets/%s", vol.Name)
-		volumeMounts = append(volumeMounts, corev1.VolumeMount{Name: vol.Name, MountPath: mountPath})
-		usernameFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthUsernameKey)
-		passwordFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthPasswordKey)
-
-		// Is TLS enabled? If so we need some additional SSL related props
-		tlsProps := ""
-		if solrCloud.Spec.SolrTLS != nil {
-			tlsProps = " -Djavax.net.ssl.keyStore=$SOLR_SSL_KEY_STORE -Djavax.net.ssl.keyStorePassword=$SOLR_SSL_KEY_STORE_PASSWORD -Djavax.net.ssl.trustStore=$SOLR_SSL_TRUST_STORE -Djavax.net.ssl.trustStorePassword=$SOLR_SSL_TRUST_STORE_PASSWORD"
+		if volMount != nil {
+			volumeMounts = append(volumeMounts, *volMount)
 		}
-		javaToolOptions := fmt.Sprintf("JAVA_TOOL_OPTIONS=\"-Dbasicauth=$(cat %s):$(cat %s)%s\"", usernameFile, passwordFile, tlsProps)
-
-		// construct the probe command to invoke the SolrCLI "api" action
-		//
-		// and yes, this is ugly, but bin/solr doesn't expose the "api" action (as of 8.8.0) so we have to invoke java directly
-		// taking some liberties on the /opt/solr path based on the official Docker image as there is no ENV var set for that path
-		probeCommand := fmt.Sprintf("%s java -Dsolr.ssl.checkPeerName=false "+
-			"-Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory "+
-			"-Dsolr.install.dir=\"/opt/solr\" -Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+
-			"-classpath \"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\" "+
-			"org.apache.solr.util.SolrCLI api -get %s://localhost:%d%s",
-			javaToolOptions, solrCloud.UrlScheme(), defaultHandler.HTTPGet.Port.IntVal, defaultHandler.HTTPGet.Path)
-
 		// reset the defaultHandler for the probes to invoke the SolrCLI api action instead of HTTP
 		defaultHandler = corev1.Handler{Exec: &corev1.ExecAction{Command: []string{"sh", "-c", probeCommand}}}
 	}
@@ -514,7 +486,7 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCl
 	}
 
 	if createPkcs12InitContainer {
-		pkcs12InitContainer := generatePkcs12InitContainer(solrCloud.Spec.SolrTLS.KeyStorePasswordSecret,
+		pkcs12InitContainer := generatePkcs12InitContainer(solrCloud.Spec.SolrTLS,
 			solrCloud.Spec.SolrImage.ToImageName(), solrCloud.Spec.SolrImage.PullPolicy)
 		initContainers = append(initContainers, pkcs12InitContainer)
 	}
@@ -1066,10 +1038,27 @@ func TLSEnvVars(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 	} else {
 		keystorePath = DefaultKeyStorePath
 	}
-	keystorePath += ("/" + Pkcs12KeystoreFile)
 
+	keystoreFile := keystorePath + "/" + Pkcs12KeystoreFile
 	passwordValueFrom := &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret}
 
+	// If using a truststore that is different from the keystore
+	truststoreFile := keystoreFile
+	truststorePassFrom := passwordValueFrom
+	if opts.TrustStoreSecret != nil {
+		if opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name {
+			// trust store is in a different secret, so will be mounted in a different dir
+			truststoreFile = DefaultTrustStorePath
+		} else {
+			// trust store is a different key in the same secret as the keystore
+			truststoreFile = DefaultKeyStorePath
+		}
+		truststoreFile += "/" + opts.TrustStoreSecret.Key
+		if opts.TrustStorePasswordSecret != nil {
+			truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.TrustStorePasswordSecret}
+		}
+	}
+
 	envVars := []corev1.EnvVar{
 		{
 			Name:  "SOLR_SSL_ENABLED",
@@ -1077,7 +1066,7 @@ func TLSEnvVars(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 		},
 		{
 			Name:  "SOLR_SSL_KEY_STORE",
-			Value: keystorePath,
+			Value: keystoreFile,
 		},
 		{
 			Name:      "SOLR_SSL_KEY_STORE_PASSWORD",
@@ -1085,11 +1074,11 @@ func TLSEnvVars(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 		},
 		{
 			Name:  "SOLR_SSL_TRUST_STORE",
-			Value: keystorePath,
+			Value: truststoreFile,
 		},
 		{
 			Name:      "SOLR_SSL_TRUST_STORE_PASSWORD",
-			ValueFrom: passwordValueFrom,
+			ValueFrom: truststorePassFrom,
 		},
 		{
 			Name:  "SOLR_SSL_WANT_CLIENT_AUTH",
@@ -1112,7 +1101,7 @@ func TLSEnvVars(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 	return envVars
 }
 
-func tlsVolumeMounts(createPkcs12InitContainer bool) []corev1.VolumeMount {
+func tlsVolumeMounts(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []corev1.VolumeMount {
 	mounts := []corev1.VolumeMount{
 		{
 			Name:      "keystore",
@@ -1121,6 +1110,14 @@ func tlsVolumeMounts(createPkcs12InitContainer bool) []corev1.VolumeMount {
 		},
 	}
 
+	if opts.TrustStoreSecret != nil && opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name {
+		mounts = append(mounts, corev1.VolumeMount{
+			Name:      "truststore",
+			ReadOnly:  true,
+			MountPath: DefaultTrustStorePath,
+		})
+	}
+
 	// We need an initContainer to convert a TLS cert into the pkcs12 format Java wants (using openssl)
 	// but openssl cannot write to the /var/solr/tls directory because of the way secret mounts work
 	// so we need to mount an empty directory to write pkcs12 keystore into
@@ -1151,6 +1148,21 @@ func tlsVolumes(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 		},
 	}
 
+	// if they're using a different truststore other than the keystore, but don't mount an additional volume
+	// if it's just pointing at the same secret
+	if opts.TrustStoreSecret != nil && opts.TrustStoreSecret.Name != opts.PKCS12Secret.Name {
+		vols = append(vols, corev1.Volume{
+			Name: "truststore",
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName:  opts.TrustStoreSecret.Name,
+					DefaultMode: &defaultMode,
+					Optional:    &optional,
+				},
+			},
+		})
+	}
+
 	if createPkcs12InitContainer {
 		vols = append(vols, corev1.Volume{
 			Name: "pkcs12",
@@ -1163,9 +1175,9 @@ func tlsVolumes(opts *solr.SolrTLSOptions, createPkcs12InitContainer bool) []cor
 	return vols
 }
 
-func generatePkcs12InitContainer(keyStorePasswordSecret *corev1.SecretKeySelector, imageName string, imagePullPolicy corev1.PullPolicy) corev1.Container {
+func generatePkcs12InitContainer(opts *solr.SolrTLSOptions, imageName string, imagePullPolicy corev1.PullPolicy) corev1.Container {
 	// get the keystore password from the env for generating the keystore using openssl
-	passwordValueFrom := &corev1.EnvVarSource{SecretKeyRef: keyStorePasswordSecret}
+	passwordValueFrom := &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret}
 	envVars := []corev1.EnvVar{
 		{
 			Name:      "SOLR_SSL_KEY_STORE_PASSWORD",
@@ -1183,7 +1195,7 @@ func generatePkcs12InitContainer(keyStorePasswordSecret *corev1.SecretKeySelecto
 		TerminationMessagePath:   "/dev/termination-log",
 		TerminationMessagePolicy: "File",
 		Command:                  []string{"sh", "-c", cmd},
-		VolumeMounts:             tlsVolumeMounts(true),
+		VolumeMounts:             tlsVolumeMounts(opts, true),
 		Env:                      envVars,
 	}
 }
@@ -1461,3 +1473,55 @@ func uniqueProbePaths(paths []string) []string {
 	}
 	return set
 }
+
+// When running with TLS and clientAuth=Need or if the probe endpoints require auth, we need to use a command instead of HTTP Get
+// This function builds the custom probe command and returns any associated volume / mounts needed for the auth secrets
+func configureSecureProbeCommand(solrCloud *solr.SolrCloud, defaultProbeGetAction *corev1.HTTPGetAction) (string, *corev1.Volume, *corev1.VolumeMount) {
+	// mount the secret in a file so it gets updated; env vars do not see:
+	// https://kubernetes.io/docs/concepts/configuration/secret/#environment-variables-are-not-updated-after-a-secret-update
+	basicAuthOption := ""
+	enableBasicAuth := ""
+	var volMount *corev1.VolumeMount
+	var vol *corev1.Volume
+	if solrCloud.Spec.SolrSecurity != nil {
+		secretName := solrCloud.BasicAuthSecretName()
+		defaultMode := int32(420)
+		vol = &corev1.Volume{
+			Name: strings.ReplaceAll(secretName, ".", "-"),
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName:  secretName,
+					DefaultMode: &defaultMode,
+				},
+			},
+		}
+		mountPath := fmt.Sprintf("/etc/secrets/%s", vol.Name)
+		volMount = &corev1.VolumeMount{Name: vol.Name, MountPath: mountPath}
+		usernameFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthUsernameKey)
+		passwordFile := fmt.Sprintf("%s/%s", mountPath, corev1.BasicAuthPasswordKey)
+		basicAuthOption = fmt.Sprintf("-Dbasicauth=$(cat %s):$(cat %s)", usernameFile, passwordFile)
+		enableBasicAuth = " -Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory "
+	}
+
+	// Is TLS enabled? If so we need some additional SSL related props
+	tlsProps := ""
+	if solrCloud.Spec.SolrTLS != nil {
+		tlsProps = "-Djavax.net.ssl.keyStore=$SOLR_SSL_KEY_STORE -Djavax.net.ssl.keyStorePassword=$SOLR_SSL_KEY_STORE_PASSWORD " +
+			"-Djavax.net.ssl.trustStore=$SOLR_SSL_TRUST_STORE -Djavax.net.ssl.trustStorePassword=$SOLR_SSL_TRUST_STORE_PASSWORD"
+	}
+
+	javaToolOptions := strings.TrimSpace(basicAuthOption + " " + tlsProps)
+
+	// construct the probe command to invoke the SolrCLI "api" action
+	//
+	// and yes, this is ugly, but bin/solr doesn't expose the "api" action (as of 8.8.0) so we have to invoke java directly
+	// taking some liberties on the /opt/solr path based on the official Docker image as there is no ENV var set for that path
+	probeCommand := fmt.Sprintf("JAVA_TOOL_OPTIONS=\"%s\" java -Dsolr.ssl.checkPeerName=false %s "+
+		"-Dsolr.install.dir=\"/opt/solr\" -Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+
+		"-classpath \"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\" "+
+		"org.apache.solr.util.SolrCLI api -get %s://localhost:%d%s",
+		javaToolOptions, enableBasicAuth, solrCloud.UrlScheme(), defaultProbeGetAction.Port.IntVal, defaultProbeGetAction.Path)
+	probeCommand = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ")
+
+	return probeCommand, vol, volMount
+}
diff --git a/docs/running-the-operator.md b/docs/running-the-operator.md
index 91905fe..3effe2e 100644
--- a/docs/running-the-operator.md
+++ b/docs/running-the-operator.md
@@ -114,4 +114,30 @@ The final image will only contain the solr-operator binary and necessary License
                           If _true_, then a Zookeeper Operator must be running for the cluster.
                           (_true_ | _false_ , defaults to _false_)
                         
-    
\ No newline at end of file
+## Client Auth for mTLS-enabled Solr clusters
+
+For SolrCloud instances that run with mTLS enabled (see `spec.solrTLS.clientAuth`), the operator needs to supply a trusted certificate when making API calls to the Solr pods it is managing.
+
+This means that the client certificate used by the operator must be added to the truststore on all Solr pods.
+Alternatively, the certificate for the Certificate Authority (CA) that signed the client certificate can be trusted by adding the CA's certificate to the Solr truststore.
+In the latter case, any client certificates issued by the trusted CA will be accepted by Solr, so make sure this is appropriate for your environment.
+
+The client certificate used by the operator should be stored in a [TLS secret](https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets); you must create this secret before deploying the Solr operator.
+
+When deploying the operator, supply the client certificate using the `mTLS.clientCertSecret` Helm chart variable, such as:
+```
+  --set mTLS.clientCertSecret=my-client-cert \
+```
+The specified secret must exist in the same namespace where the operator is deployed.
+
+In addition, if the CA used to sign the server certificate used by Solr is not built into the operator's Docker image, 
+then you'll need to add the CA's certificate to the operator so its HTTP client will trust the server certificates during the TLS handshake.
+
+The CA certificate needs to be stored in Kubernetes secret in PEM format and provided via the following Helm chart variables:
+```
+  --set mTLS.caCertSecret=my-client-ca-cert \
+  --set mTLS.caCertSecretKey=ca-cert-pem
+```
+
+In most cases, you'll also want to configure the operator with `mTLS.insecureSkipVerify=true` (the default) as you'll want the operator to skip hostname verification for Solr pods.
+Setting `mTLS.insecureSkipVerify` to `false` means the operator will enforce hostname verification for the certificate provided by Solr pods.
\ No newline at end of file
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index 500d38f..da7bcc3 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -468,6 +468,30 @@ spec:
       key: keystore.p12
 ```
 
+### Separate TrustStore
+
+A truststore holds public keys for certificates you trust. By default, Solr pods are configured to use the keystore as the truststore.
+However, you may have a separate truststore you want to use for Solr TLS. As with the keystore, you need to provide a PKCS12 truststore in a secret and then configure your SolrCloud TLS settings as shown below:
+```yaml
+spec:
+  ... other SolrCloud CRD settings ...
+
+  solrTLS:
+    keyStorePasswordSecret:
+      name: pkcs12-keystore-manual
+      key: password-key
+    pkcs12Secret:
+      name: pkcs12-keystore-manual
+      key: keystore.p12
+    trustStorePasswordSecret:
+      name: pkcs12-truststore
+      key: password-key
+    trustStoreSecret:
+      name: pkcs12-truststore
+      key: truststore.p12
+``` 
+_Tip: if your truststore is not in PKCS12 format, use `openssl` to convert it._ 
+
 ### Ingress
 
 The Solr operator may create an Ingress for exposing Solr pods externally. When TLS is enabled, the operator adds the following annotation and TLS settings to the Ingress manifest, such as:
@@ -571,6 +595,28 @@ spec:
 The example settings above will result in your Solr pods getting names like: `<ns>-search-solrcloud-0.k8s.solr.cloud` 
 which you can request TLS certificates from LetsEncrypt assuming you own the `k8s.solr.cloud` domain.
 
+#### mTLS
+
+Mutual TLS (mTLS) provides an additional layer of security by ensuring the client applications sending requests to Solr are trusted.
+To enable mTLS, simply set `spec.solrTLS.clientAuth` to either `Want` or `Need`. When mTLS is enabled, the Solr operator needs to
+supply a client certificate that is trusted by Solr; the operator makes API calls to Solr to get cluster status. 
+To configure the client certificate for the operator, see [Running the Operator > mTLS](../running-the-operator.md#Client-Auth-for-mTLS-enabled-Solr-clusters)
+
+When mTLS is enabled, the liveness and readiness probes are configured to execute a local command on each Solr pod instead of the default HTTP Get request.
+Using a command is required so that we can use the correct TLS certificate when making an HTTPs call to the probe endpoints.
+
+To help with debugging the TLS handshake between client and server,
+you can add the `-Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake` Java system property to the `spec.solrOpts` for your SolrCloud instance. 
+
+To verify mTLS is working for your Solr pods, you can supply the client certificate (and CA cert if needed) via curl after opening a port-forward to one of your Solr pods:
+```
+curl "https://localhost:8983/solr/admin/info/system" -v \
+  --key client/private_key.pem \
+  --cert client/client.pem \
+  --cacert root-ca/root-ca.pem
+```
+The `--cacert` option supplies the CA's certificate needed to trust the server certificate provided by the Solr pods during TLS handshake.
+
 ## Authentication and Authorization
 _Since v0.3.0_
 
diff --git a/helm/solr-operator/README.md b/helm/solr-operator/README.md
index 49f4573..68584a5 100644
--- a/helm/solr-operator/README.md
+++ b/helm/solr-operator/README.md
@@ -158,6 +158,10 @@ The command removes all the Kubernetes components associated with the chart and
 | zookeeper-operator.install | boolean | `true` | This option installs the Zookeeper Operator as a helm dependency |
 | zookeeper-operator.use | boolean | `false` | This option enables the use of provided Zookeeper instances for SolrClouds via the Zookeeper Operator, without installing the Zookeeper Operator as a dependency. If `zookeeper-operator.install`=`true`, then this option is ignored. |
 | useZkOperator | string | `"true"` | **DEPRECATED** Replaced by the _boolean_ "zookeeper-operator.use" option. This option will be removed in v0.4.0 |
+| mTLS.clientCertSecret | string | `""` | Name of a Kubernetes TLS secret, in the same namespace, that contains a Client certificate to load into the operator. If provided, this is used when communicating with Solr. |
+| mTLS.caCertSecretKey | string | `""` | Name of a Kubernetes secret, in the same namespace, that contains PEM encoded Root CA Certificate to use when connecting to Solr with Client Auth. |
+| mTLS.caCertSecret | string | `""` | Name of the key in the `caCertSecret` that contains the Root CA Cert as a value. |
+| mTLS.insecureSkipVerify | boolean | `true` | Skip server certificate and hostname verification when connecting to Solr with ClientAuth. |
 
 ### Running the Solr Operator
 
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index c2c96b5..d981efe 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -5720,6 +5720,36 @@ spec:
                   restartOnTLSSecretUpdate:
                     description: Opt-in flag to restart Solr pods after TLS secret updates, such as if the cert is renewed; default is false.
                     type: boolean
+                  trustStorePasswordSecret:
+                    description: Secret containing the trust store password; if not provided the keyStorePassword will be used
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
+                  trustStoreSecret:
+                    description: TLS Secret containing a pkcs12 truststore; if not provided, then the keystore and password are used for the truststore The specified key is used as the truststore file name when mounted into Solr pods
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
                   verifyClientHostname:
                     description: Verify client's hostname during SSL handshake
                     type: boolean
@@ -10038,6 +10068,36 @@ spec:
                       restartOnTLSSecretUpdate:
                         description: Opt-in flag to restart Solr pods after TLS secret updates, such as if the cert is renewed; default is false.
                         type: boolean
+                      trustStorePasswordSecret:
+                        description: Secret containing the trust store password; if not provided the keyStorePassword will be used
+                        properties:
+                          key:
+                            description: The key of the secret to select from.  Must be a valid secret key.
+                            type: string
+                          name:
+                            description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                            type: string
+                          optional:
+                            description: Specify whether the Secret or its key must be defined
+                            type: boolean
+                        required:
+                        - key
+                        type: object
+                      trustStoreSecret:
+                        description: TLS Secret containing a pkcs12 truststore; if not provided, then the keystore and password are used for the truststore The specified key is used as the truststore file name when mounted into Solr pods
+                        properties:
+                          key:
+                            description: The key of the secret to select from.  Must be a valid secret key.
+                            type: string
+                          name:
+                            description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
+                            type: string
+                          optional:
+                            description: Specify whether the Secret or its key must be defined
+                            type: boolean
+                        required:
+                        - key
+                        type: object
                       verifyClientHostname:
                         description: Verify client's hostname during SSL handshake
                         type: boolean
diff --git a/helm/solr-operator/templates/_helpers.tpl b/helm/solr-operator/templates/_helpers.tpl
index 2445fe6..88fdac1 100644
--- a/helm/solr-operator/templates/_helpers.tpl
+++ b/helm/solr-operator/templates/_helpers.tpl
@@ -84,4 +84,49 @@ Determine whether to use ClusterRoles or Roles
 {{- else -}}
     ClusterRole
 {{- end -}}
+{{- end -}}
+
+{{/*
+mTLS vars
+*/}}
+{{- define "solr-operator.mTLS.clientCertDirectory" -}}
+/etc/ssl/solr/client-cert
+{{- end -}}
+
+{{- define "solr-operator.mTLS.caCertDirectory" -}}
+/etc/ssl/solr/ca-cert
+{{- end -}}
+{{- define "solr-operator.mTLS.caCertName" -}}
+rootSolrCert.pem
+{{- end -}}
+
+{{- define "solr-operator.mTLS.volumeMounts" -}}
+{{- if .Values.mTLS.clientCertSecret -}}
+- name: tls-client-cert
+  mountPath: {{ include "solr-operator.mTLS.clientCertDirectory" . }}
+  readOnly: true
+{{- end -}}
+{{ if .Values.mTLS.caCertSecret }}
+- name: tls-ca-cert
+  mountPath: {{ include "solr-operator.mTLS.caCertDirectory" . }}
+  readOnly: true
+{{ end }}
+{{- end -}}
+
+{{- define "solr-operator.mTLS.volumes" -}}
+{{- if .Values.mTLS.clientCertSecret -}}
+- name: tls-client-cert
+  secret:
+    secretName: {{ .Values.mTLS.clientCertSecret }}
+    optional: false
+{{- end -}}
+{{ if .Values.mTLS.caCertSecret }}
+- name: tls-ca-cert
+  secret:
+    secretName: {{ .Values.mTLS.caCertSecret }}
+    items:
+      - key: {{ .Values.mTLS.caCertSecretKey }}
+        path: {{ include "solr-operator.mTLS.caCertName" . }}
+    optional: false
+{{- end -}}
 {{- end -}}
\ No newline at end of file
diff --git a/helm/solr-operator/templates/deployment.yaml b/helm/solr-operator/templates/deployment.yaml
index 955a215..7f800d7 100644
--- a/helm/solr-operator/templates/deployment.yaml
+++ b/helm/solr-operator/templates/deployment.yaml
@@ -53,6 +53,16 @@ spec:
         {{- if .Values.watchNamespaces }}
         - --watch-namespaces={{- include "solr-operator.watchNamespaces" . -}}
         {{- end }}
+        {{- if .Values.mTLS.clientCertSecret }}
+        - --tls-client-cert-path={{- include "solr-operator.mTLS.clientCertDirectory" . -}}/tls.crt
+        - --tls-client-cert-key-path={{- include "solr-operator.mTLS.clientCertDirectory" . -}}/tls.key
+        {{- end }}
+        {{- if .Values.mTLS.caCertSecret }}
+        - --tls-ca-cert-path={{- include "solr-operator.mTLS.caCertDirectory" . -}}/{{- include "solr-operator.mTLS.caCertName" . -}}
+        {{- end }}
+        {{- if .Values.mTLS.insecureSkipVerify }}
+        - --tls-skip-verify-server={{ .Values.mTLS.insecureSkipVerify }}
+        {{- end }}
         env:
           - name: POD_NAMESPACE
             valueFrom:
@@ -67,6 +77,15 @@ spec:
           {{- end }}
         resources:
           {{- toYaml .Values.resources | nindent 10 }}
+        {{- if (include "solr-operator.mTLS.volumeMounts" .) }}
+        volumeMounts:
+          {{- include "solr-operator.mTLS.volumeMounts" .  | nindent 10 }}
+        {{- end }}
+      {{- if (include "solr-operator.mTLS.volumes" .) }}
+      volumes:
+        {{- include "solr-operator.mTLS.volumes" . | nindent 8 }}
+      {{- end }}
+
       {{- if .Values.sidecarContainers }}
       {{ toYaml .Values.sidecarContainers | nindent 6 }}
       {{- end }}
diff --git a/helm/solr-operator/values.yaml b/helm/solr-operator/values.yaml
index 4318b5e..2cc2a8f 100644
--- a/helm/solr-operator/values.yaml
+++ b/helm/solr-operator/values.yaml
@@ -72,3 +72,10 @@ affinity: {}
 tolerations: []
 priorityClassName: ""
 sidecarContainers: []
+
+# Use mTLS when connecting to Solr Clouds from the Solr Operator
+mTLS:
+  clientCertSecret: ""
+  caCertSecret: ""
+  caCertSecretKey: ca-cert.pem
+  insecureSkipVerify: true
\ No newline at end of file
diff --git a/main.go b/main.go
index 094ecb8..a87b695 100644
--- a/main.go
+++ b/main.go
@@ -19,12 +19,14 @@ package main
 
 import (
 	"crypto/tls"
+	"crypto/x509"
 	"flag"
 	"fmt"
 	solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
 	"github.com/apache/solr-operator/controllers"
 	"github.com/apache/solr-operator/controllers/util/solr_api"
 	"github.com/apache/solr-operator/version"
+	"io/ioutil"
 	"net/http"
 	"os"
 	"runtime"
@@ -57,6 +59,12 @@ var (
 
 	// External Operator dependencies
 	useZookeeperCRD bool
+
+	// mTLS information
+	clientSkipVerify  bool
+	clientCertPath    string
+	clientCertKeyPath string
+	caCertPath        string
 )
 
 func init() {
@@ -68,15 +76,17 @@ func init() {
 	// +kubebuilder:scaffold:scheme
 	flag.BoolVar(&useZookeeperCRD, "zk-operator", true, "The operator will not use the zk operator & crd when this flag is set to false.")
 	flag.StringVar(&watchNamespaces, "watch-namespaces", "", "The comma-separated list of namespaces to watch. If an empty string (default) is provided, the operator will watch the entire Kubernetes cluster.")
+
+	flag.BoolVar(&clientSkipVerify, "tls-skip-verify-server", true, "Controls whether a client verifies the server's certificate chain and host name. If true (insecure), TLS accepts any certificate presented by the server and any host name in that certificate.")
+	flag.StringVar(&clientCertPath, "tls-client-cert-path", "", "Path where a TLS client cert can be found")
+	flag.StringVar(&clientCertKeyPath, "tls-client-cert-key-path", "", "Path where a TLS client cert key can be found")
+
+	flag.StringVar(&caCertPath, "tls-ca-cert-path", "", "Path where a Certificate Authority (CA) cert in PEM format can be found")
+
 	flag.Parse()
 }
 
 func main() {
-	// setup an http client that can talk to Solr pods using untrusted, self-signed certs
-	customTransport := http.DefaultTransport.(*http.Transport).Clone()
-	customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
-	solr_api.SetNoVerifyTLSHttpClient(&http.Client{Transport: customTransport})
-
 	namespace = os.Getenv(EnvOperatorPodNamespace)
 	if len(namespace) == 0 {
 		//log.Fatalf("must set env (%s)", constants.EnvOperatorPodNamespace)
@@ -137,6 +147,10 @@ func main() {
 
 	controllers.UseZkCRD(useZookeeperCRD)
 
+	if err = initMTLSConfig(); err != nil {
+		os.Exit(1)
+	}
+
 	if err = (&controllers.SolrCloudReconciler{
 		Client: mgr.GetClient(),
 		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
@@ -166,3 +180,37 @@ func main() {
 		os.Exit(1)
 	}
 }
+
+func initMTLSConfig() error {
+	if clientCertPath != "" {
+		setupLog.Info("mTLS config", "clientSkipVerify", clientSkipVerify, "clientCertPath", clientCertPath,
+			"clientCertKeyPath", clientCertKeyPath, "caCertPath", caCertPath)
+
+		// Load client cert information from files
+		clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientCertKeyPath)
+		if err != nil {
+			setupLog.Error(err, "Error loading clientCert pair for mTLS transport", "certPath", clientCertPath, "keyPath", clientCertKeyPath)
+			return err
+		}
+
+		mTLSTransport := http.DefaultTransport.(*http.Transport).Clone()
+		mTLSTransport.TLSClientConfig = &tls.Config{Certificates: []tls.Certificate{clientCert}, InsecureSkipVerify: clientSkipVerify}
+
+		// Add the rootCA if one is provided
+		if caCertPath != "" {
+			if caCertBytes, err := ioutil.ReadFile(caCertPath); err == nil {
+				caCertPool := x509.NewCertPool()
+				caCertPool.AppendCertsFromPEM(caCertBytes)
+				mTLSTransport.TLSClientConfig.ClientCAs = caCertPool
+				setupLog.Info("Configured the custom CA pem for the mTLS transport", "path", caCertPath)
+			} else {
+				setupLog.Error(err, "Cannot read provided CA pem for mTLS transport", "path", caCertPath)
+				return err
+			}
+		}
+
+		solr_api.SetMTLSHttpClient(&http.Client{Transport: mTLSTransport})
+	}
+
+	return nil
+}