Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to merge the JVM truststore with user-supplied truststore #461

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/v1beta1/solrcloud_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,14 @@ type SolrTLSOptions struct {
// This option is typically used with `spec.updateStrategy.restartSchedule` to restart Solr pods before the mounted TLS cert expires.
// +optional
MountedTLSDir *MountedTLSDirectory `json:"mountedTLSDir,omitempty"`

// Path on the Solr image to your JVM's truststore to merge with an external truststore.
// If supplied, Solr will be configured to use the merged truststore.
// The truststore for the JVM in the default Solr image is: $JAVA_HOME/lib/security/cacerts
MergeJavaTruststore string `json:"mergeJavaTrustStore,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the // +optional tag for both of these new options? Just for consistency sake

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with mountedTLSDir?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically, mountedTLSDir will have a csi driver volume and corresponding mount on the mainContainer, which would get used by the merge initContainer, so the init container would get the truststore file. However, that might cause double creation of the cert for each pod, once for the initContainer and once for the main container, so this would likely put a lot of pressure on the Cert issuer. So probably safer to say this feature is not supported with mountedTLSDir option for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, nvm all that volumesAndMounts doesn't include vols & mounts for the mountedTLSDir option. So this option is not going to work with mountedTLSDir.

So maybe we just punt on this feature for 0.6 and solve it using an init-db script instead? There's already some code in place for mounting a script into the init-db.


// Password for the Java truststore to merge; defaults to "changeit"
MergeJavaTruststorePass string `json:"mergeJavaTrustStorePass,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a secret reference? How bad is it to have this in plain text?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seemed like overkill to me since it's the truststore pass for the JVM which is most likely "changeit" and isn't used by Solr ... that said, we can pull from a secret too ... doubt many would ever use this option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh ok, didn't understand it was unusual to change it. Sounds good

}

// +kubebuilder:validation:Enum=Basic
Expand Down
12 changes: 12 additions & 0 deletions config/crd/bases/solr.apache.org_solrclouds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4926,6 +4926,12 @@ spec:
required:
- key
type: object
mergeJavaTrustStore:
description: 'Path on the Solr image to your JVM''s truststore to merge with an external truststore. If supplied, Solr will be configured to use the merged truststore. The truststore for the JVM in the default Solr image is: $JAVA_HOME/lib/security/cacerts'
type: string
mergeJavaTrustStorePass:
description: Password for the Java truststore to merge; defaults to "changeit"
type: string
mountedTLSDir:
description: Used to specify a path where the keystore, truststore, and password files for the TLS certificate are mounted by an external agent or CSI driver. This option is typically used with `spec.updateStrategy.restartSchedule` to restart Solr pods before the mounted TLS cert expires.
properties:
Expand Down Expand Up @@ -5087,6 +5093,12 @@ spec:
required:
- key
type: object
mergeJavaTrustStore:
description: 'Path on the Solr image to your JVM''s truststore to merge with an external truststore. If supplied, Solr will be configured to use the merged truststore. The truststore for the JVM in the default Solr image is: $JAVA_HOME/lib/security/cacerts'
type: string
mergeJavaTrustStorePass:
description: Password for the Java truststore to merge; defaults to "changeit"
type: string
mountedTLSDir:
description: Used to specify a path where the keystore, truststore, and password files for the TLS certificate are mounted by an external agent or CSI driver. This option is typically used with `spec.updateStrategy.restartSchedule` to restart Solr pods before the mounted TLS cert expires.
properties:
Expand Down
6 changes: 6 additions & 0 deletions config/crd/bases/solr.apache.org_solrprometheusexporters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3688,6 +3688,12 @@ spec:
required:
- key
type: object
mergeJavaTrustStore:
description: 'Path on the Solr image to your JVM''s truststore to merge with an external truststore. If supplied, Solr will be configured to use the merged truststore. The truststore for the JVM in the default Solr image is: $JAVA_HOME/lib/security/cacerts'
type: string
mergeJavaTrustStorePass:
description: Password for the Java truststore to merge; defaults to "changeit"
type: string
mountedTLSDir:
description: Used to specify a path where the keystore, truststore, and password files for the TLS certificate are mounted by an external agent or CSI driver. This option is typically used with `spec.updateStrategy.restartSchedule` to restart Solr pods before the mounted TLS cert expires.
properties:
Expand Down
94 changes: 87 additions & 7 deletions controllers/solrcloud_controller_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,64 @@ var _ = FDescribe("SolrCloud controller - TLS", func() {
})
})

FContext("Secret TLS - Merge Truststore", func() {
tlsSecretName := "tls-cert-secret-from-user"
keystorePassKey := "some-password-key-thingy"
trustStoreSecretName := "custom-truststore-secret"
trustStoreFile := "truststore.p12"
BeforeEach(func() {
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{AuthenticationType: solrv1beta1.Basic}
solrCloud.Spec.SolrTLS = createTLSOptions(tlsSecretName, keystorePassKey, false)
solrCloud.Spec.SolrTLS.TrustStoreSecret = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: trustStoreSecretName},
Key: trustStoreFile,
}
solrCloud.Spec.SolrTLS.TrustStorePasswordSecret = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: trustStoreSecretName},
Key: "truststore-pass",
}
solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Need // require client auth too (mTLS between the pods)
solrCloud.Spec.SolrTLS.MergeJavaTruststore = util.DefaultJvmTruststore
})
FIt("has the correct resources", func() {
verifyReconcileUserSuppliedTLS(ctx, solrCloud, false, false)
})
})

FContext("Secret TLS - Merge Truststores for Server & Client", func() {
serverCertSecret := "tls-server-cert"
clientCertSecret := "tls-client-cert"
keystorePassKey := "some-password-key-thingy"
BeforeEach(func() {
solrCloud.Spec.SolrTLS = createTLSOptions(serverCertSecret, keystorePassKey, false)
solrCloud.Spec.SolrTLS.MergeJavaTruststore = util.DefaultJvmTruststore

solrCloud.Spec.SolrTLS.ClientAuth = solrv1beta1.Need

// Additional client cert
solrCloud.Spec.SolrClientTLS = &solrv1beta1.SolrTLSOptions{
KeyStorePasswordSecret: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: clientCertSecret},
Key: keystorePassKey,
},
PKCS12Secret: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: clientCertSecret},
Key: util.DefaultPkcs12KeystoreFile,
},
TrustStoreSecret: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: clientCertSecret},
Key: util.DefaultPkcs12TruststoreFile,
},
RestartOnTLSSecretUpdate: false,
MergeJavaTruststore: util.DefaultJvmTruststore,
}
})
FIt("has the correct resources", func() {
By("checking that the User supplied TLS Config is correct in the generated StatefulSet")
verifyReconcileUserSuppliedTLS(ctx, solrCloud, false, false)
})
})

FContext("Secret TLS - Pkcs12 Conversion", func() {
tlsSecretName := "tls-cert-secret-from-user-no-pkcs12"
keystorePassKey := "some-password-key-thingy"
Expand Down Expand Up @@ -466,6 +524,11 @@ func expectTLSConfigOnPodTemplateWithGomega(g Gomega, tls *solrv1beta1.SolrTLSOp
expectedTrustStorePath = util.DefaultTrustStorePath + "/" + tls.TrustStoreSecret.Key
}

// did we merge the Java truststore with the user-supplied?
if tls.MergeJavaTruststore != "" {
expectedTrustStorePath = "/var/solr/tls-merged/truststore.p12"
}

if tls.PKCS12Secret != nil {
expectTLSEnvVarsWithGomega(g, mainContainer.Env, tls.KeyStorePasswordSecret.Name, tls.KeyStorePasswordSecret.Key, needsPkcs12InitContainer, expectedTrustStorePath, clientOnly, clientTLS)
} else if tls.TrustStoreSecret != nil {
Expand All @@ -477,7 +540,11 @@ func expectTLSConfigOnPodTemplateWithGomega(g Gomega, tls *solrv1beta1.SolrTLSOp
g.Expect(len(envVars)).To(Equal(3), "Wrong number of SSL env vars for Pod")
for _, envVar := range envVars {
if envVar.Name == "SOLR_SSL_CLIENT_TRUST_STORE" {
g.Expect(envVar.Value).To(Equal(expectedTrustStorePath), "Wrong envVar value for %s", envVar.Name)
expectedPath := expectedTrustStorePath
if clientTLS != nil && clientTLS.MergeJavaTruststore != "" {
expectedPath = "/var/solr/tls-client-merged/truststore.p12"
}
g.Expect(envVar.Value).To(Equal(expectedPath), "Wrong envVar value for %s", envVar.Name)
}
if envVar.Name == "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD" {
g.Expect(envVar.Value).To(BeEmpty(), "EnvVar %s should not use an explicit Value, since it is populated from a secret", envVar.Name)
Expand Down Expand Up @@ -531,6 +598,20 @@ func expectTLSConfigOnPodTemplateWithGomega(g Gomega, tls *solrv1beta1.SolrTLSOp
g.Expect(expInitContainer.Command[2]).To(Equal(expCmd), "Wrong TLS initContainer command")
}

if tls.MergeJavaTruststore != "" {
// verify the merge-truststore initContainer was created as well
g.Expect(podTemplate.Spec.InitContainers).To(Not(BeEmpty()), "An init container should exist to merge truststores")
var expInitContainer *corev1.Container = nil
for _, cnt := range podTemplate.Spec.InitContainers {
if cnt.Name == "merge-truststore" {
expInitContainer = &cnt
break
}
}
g.Expect(expInitContainer).To(Not(BeNil()), "Didn't find the merge-truststore InitContainer in the sts!")
g.Expect(expInitContainer.Command[2]).To(ContainSubstring("keytool"), "Wrong merge initContainer command")
}

if tls.ClientAuth == solrv1beta1.Need {
// verify the probes use a command with SSL opts
tlsProps := ""
Expand Down Expand Up @@ -611,11 +692,6 @@ func expectMountedTLSDirEnvVars(envVars []corev1.EnvVar, solrCloud *solrv1beta1.
}
}

// ensure the TLS related env vars are set for the Solr pod
func expectTLSEnvVars(envVars []corev1.EnvVar, expectedKeystorePasswordSecretName string, expectedKeystorePasswordSecretKey string, needsPkcs12InitContainer bool, expectedTruststorePath string, clientOnly bool, clientTLS *solrv1beta1.SolrTLSOptions) {
expectTLSEnvVarsWithGomega(Default, envVars, expectedKeystorePasswordSecretName, expectedKeystorePasswordSecretKey, needsPkcs12InitContainer, expectedTruststorePath, clientOnly, clientTLS)
}

// ensure the TLS related env vars are set for the Solr pod
func expectTLSEnvVarsWithGomega(g Gomega, envVars []corev1.EnvVar, expectedKeystorePasswordSecretName string, expectedKeystorePasswordSecretKey string, needsPkcs12InitContainer bool, expectedTruststorePath string, clientOnly bool, clientTLS *solrv1beta1.SolrTLSOptions) {
g.Expect(envVars).To(Not(BeNil()), "Env Vars should not be nil")
Expand Down Expand Up @@ -678,7 +754,11 @@ func expectTLSEnvVarsWithGomega(g Gomega, envVars []corev1.EnvVar, expectedKeyst
}

if envVar.Name == "SOLR_SSL_CLIENT_TRUST_STORE" {
g.Expect(envVar.Value).To(Equal("/var/solr/client-tls/truststore.p12"), "Wrong envVar value for %s", envVar.Name)
expectedPath := "/var/solr/client-tls/truststore.p12"
if clientTLS.MergeJavaTruststore != "" {
expectedPath = "/var/solr/tls-client-merged/truststore.p12"
}
g.Expect(envVar.Value).To(Equal(expectedPath), "Wrong envVar value for %s", envVar.Name)
}

if envVar.Name == "SOLR_SSL_CLIENT_KEY_STORE_PASSWORD" || envVar.Name == "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD" {
Expand Down
32 changes: 32 additions & 0 deletions controllers/solrprometheusexporter_controller_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,38 @@ var _ = FDescribe("SolrPrometheusExporter controller - TLS", func() {
})
})

FContext("TLS Secret - Merge TrustStore Only", func() {
tlsSecretName := "tls-cert-secret-from-user"
BeforeEach(func() {
solrPrometheusExporter.Spec = solrv1beta1.SolrPrometheusExporterSpec{
SolrReference: solrv1beta1.SolrReference{
Cloud: &solrv1beta1.SolrCloudReference{
ZookeeperConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
InternalConnectionString: "host:2181",
ChRoot: "/this/path",
},
},
SolrTLS: &solrv1beta1.SolrTLSOptions{
TrustStorePasswordSecret: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: tlsSecretName},
Key: "keystore-passwords-are-important",
},
TrustStoreSecret: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: tlsSecretName},
Key: util.DefaultPkcs12TruststoreFile,
},
RestartOnTLSSecretUpdate: false,
MergeJavaTruststore: util.DefaultJvmTruststore,
},
},
}
})
FIt("has the correct resources", func() {
By("testing the SolrPrometheusExporter Deployment")
testReconcileWithTruststoreOnly(ctx, solrPrometheusExporter, tlsSecretName)
})
})

FContext("TLS Secret - TrustStore Only - Restart on Secret Update", func() {
tlsSecretName := "tls-cert-secret-from-user"
BeforeEach(func() {
Expand Down
104 changes: 103 additions & 1 deletion controllers/util/solr_tls_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
DefaultPkcs12KeystoreFile = "keystore.p12"
DefaultPkcs12TruststoreFile = "truststore.p12"
DefaultKeystorePasswordFile = "keystore-password"
DefaultJvmTruststore = "$JAVA_HOME/lib/security/cacerts"
)

// Helper struct for holding server and/or client cert config
Expand Down Expand Up @@ -72,6 +73,7 @@ type TLSConfig struct {
TruststorePath string
VolumePrefix string
Namespace string
EnvVarPrefix string
}

// Get a TLSCerts struct for reconciling TLS on a SolrCloud
Expand All @@ -83,6 +85,7 @@ func TLSCertsForSolrCloud(instance *solr.SolrCloud) *TLSCerts {
TruststorePath: DefaultTrustStorePath,
CertMd5Annotation: SolrTlsCertMd5Annotation,
Namespace: instance.Namespace,
EnvVarPrefix: "SOLR_SSL",
},
InitContainerImage: instance.Spec.BusyBoxImage,
}
Expand All @@ -94,6 +97,7 @@ func TLSCertsForSolrCloud(instance *solr.SolrCloud) *TLSCerts {
VolumePrefix: "client-",
CertMd5Annotation: SolrClientTlsCertMd5Annotation,
Namespace: instance.Namespace,
EnvVarPrefix: "SOLR_SSL_CLIENT",
}
}
return tls
Expand All @@ -117,6 +121,7 @@ func TLSCertsForExporter(prometheusExporter *solr.SolrPrometheusExporter) *TLSCe
TruststorePath: DefaultTrustStorePath,
CertMd5Annotation: SolrClientTlsCertMd5Annotation,
Namespace: prometheusExporter.Namespace,
EnvVarPrefix: "SOLR_SSL_CLIENT",
},
InitContainerImage: bbImage,
}
Expand All @@ -129,8 +134,9 @@ func TLSCertsForExporter(prometheusExporter *solr.SolrPrometheusExporter) *TLSCe
func (tls *TLSCerts) enableTLSOnSolrCloudStatefulSet(stateful *appsv1.StatefulSet) {
serverCert := tls.ServerConfig

// Add the SOLR_SSL_* vars to the main container's environment
mainContainer := &stateful.Spec.Template.Spec.Containers[0]

// Add the SOLR_SSL_* vars to the main container's environment
mainContainer.Env = append(mainContainer.Env, serverCert.serverEnvVars()...)
// Was a client cert mounted too? If so, add the client env vars to the main container as well
if tls.ClientConfig != nil {
Expand All @@ -151,6 +157,13 @@ func (tls *TLSCerts) enableTLSOnSolrCloudStatefulSet(stateful *appsv1.StatefulSe
// use an initContainer to create the wrapper script in the initdb
stateful.Spec.Template.Spec.InitContainers = append(stateful.Spec.Template.Spec.InitContainers, tls.generateTLSInitdbScriptInitContainer())
}

if serverCert.Options.MergeJavaTruststore != "" {
serverCert.addMergeTruststoreInitContainer(&stateful.Spec.Template)
}
if tls.ClientConfig != nil && tls.ClientConfig.Options.MergeJavaTruststore != "" {
tls.ClientConfig.addMergeTruststoreInitContainer(&stateful.Spec.Template)
}
}

// Enrich the config for a Prometheus Exporter Deployment to allow the exporter to make requests to TLS enabled Solr pods
Expand All @@ -172,6 +185,11 @@ func (tls *TLSCerts) enableTLSOnExporterDeployment(deployment *appsv1.Deployment
// volumes and mounts for TLS when using the mounted dir option
clientCert.mountTLSWrapperScriptAndInitContainer(deployment, tls.InitContainerImage)
}

if clientCert.Options.MergeJavaTruststore != "" {
// add an initContainer that merges the truststores together
clientCert.addMergeTruststoreInitContainer(&deployment.Spec.Template)
}
}

// Configures a pod template (either StatefulSet or Deployment) to mount the TLS files from a secret
Expand Down Expand Up @@ -807,3 +825,87 @@ func verifyTLSSecretConfig(client *client.Client, secretName string, secretNames

return foundTLSSecret, nil
}

// Adds an initContainer that merges the JVM's truststore with the user-supplied truststore
func (tls *TLSConfig) addMergeTruststoreInitContainer(template *corev1.PodTemplateSpec) {
mainContainer := &template.Spec.Containers[0]

// supports either client or server truststore env var names
envVar := tls.trustStoreEnvVarName()

// build an initContainer that merges the truststores together
initContainer, mergeVol, mergeMount :=
tls.buildMergeTruststoreInitContainer(mainContainer.Image, mainContainer.ImagePullPolicy, mainContainer.Env)
template.Spec.InitContainers = append(template.Spec.InitContainers, *initContainer)
template.Spec.Volumes = append(template.Spec.Volumes, *mergeVol)
mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *mergeMount)
// point the truststore to the merged
updateEnvVars := []corev1.EnvVar{}
for _, n := range mainContainer.Env {
// copy over all but the one we're swapping out ...
if n.Name != envVar {
updateEnvVars = append(updateEnvVars, n)
}
}
mainContainer.Env = append(updateEnvVars, corev1.EnvVar{Name: envVar, Value: mergeMount.MountPath + "/truststore.p12"})
}

func (tls *TLSConfig) buildMergeTruststoreInitContainer(solrImageName string, imagePullPolicy corev1.PullPolicy, serverEnvVars []corev1.EnvVar) (*corev1.Container, *corev1.Volume, *corev1.VolumeMount) {
// StatefulSet might have a client and server SSL config, so need to vary the initContainer and vol mount names
envVar := tls.trustStoreEnvVarName()
passEnvVar := envVar + "_PASSWORD"
volName := fmt.Sprintf("merge-%struststore", tls.VolumePrefix)
mergedEnvVar := tls.mergedTruststoreEnvVarName()
mountPath := tls.mergedTruststoreMountPath()
envVars := []corev1.EnvVar{
{
Name: mergedEnvVar,
Value: mountPath + "/truststore.p12",
},
}
envVars = append(envVars, serverEnvVars...)

// the default truststore pass for most JVM is "changeit"
javaTruststorePass := tls.Options.MergeJavaTruststorePass
if javaTruststorePass == "" {
javaTruststorePass = "changeit"
}

// Use Java's keytool to merge the JVM's truststore with the user-supplied truststore
cmd := fmt.Sprintf(`keytool -importkeystore -srckeystore %s -srcstorepass %s -destkeystore $%s -deststoretype pkcs12 -deststorepass $%s;
keytool -importkeystore -srckeystore $%s -srcstorepass $%s -destkeystore $%s -deststoretype pkcs12 -deststorepass $%s`,
tls.Options.MergeJavaTruststore, javaTruststorePass, mergedEnvVar, passEnvVar, envVar, passEnvVar, mergedEnvVar, passEnvVar)

// Need volume mounts from mainContainer (to get access to the user-supplied truststore)
_, mounts := tls.volumesAndMounts()
// Mount a shared dir between the initContainer and mainContainer to store the merged truststore
mergeVol := &corev1.Volume{Name: volName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}
mergeMount := &corev1.VolumeMount{Name: volName, ReadOnly: false, MountPath: mountPath}
mounts = append(mounts, *mergeMount)

return &corev1.Container{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we possibly use the default initContainer resources?

You can see how it was done in this PR: https://github.com/apache/solr-operator/pull/451/files#diff-653faf42eabedf3285e433f247c993282f035ee70781d151f8c8d68fee2621a3R618-R649

Name: volName,
Image: solrImageName, // we use the Solr image for the initContainer since it has the truststore and keytool
ImagePullPolicy: imagePullPolicy,
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
Command: []string{"sh", "-c", cmd},
VolumeMounts: mounts,
Env: envVars,
}, mergeVol, mergeMount
}

func (tls *TLSConfig) trustStoreEnvVarName() string {
return tls.EnvVarPrefix + "_TRUST_STORE"
}

func (tls *TLSConfig) mergedTruststoreEnvVarName() string {
if tls.VolumePrefix == "client-" {
return "MERGED_CLIENT_TRUST_STORE"
}
return "MERGED_TRUST_STORE"
}

func (tls *TLSConfig) mergedTruststoreMountPath() string {
return fmt.Sprintf("/var/solr/tls-%smerged", tls.VolumePrefix)
}
Loading