controllers/util/solr_tls_util.go (612 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package util import ( "context" "crypto/md5" "fmt" "strconv" "strings" solr "github.com/apache/solr-operator/api/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) const ( SolrTlsCertMd5Annotation = "solr.apache.org/tlsCertMd5" SolrClientTlsCertMd5Annotation = "solr.apache.org/tlsClientCertMd5" DefaultKeyStorePath = "/var/solr/tls" DefaultClientKeyStorePath = "/var/solr/client-tls" DefaultWritableKeyStorePath = "/var/solr/tls/pkcs12" TLSCertKey = "tls.crt" DefaultTrustStorePath = "/var/solr/tls-truststore" DefaultClientTrustStorePath = "/var/solr/client-tls-truststore" InitdbInitContainer = "generate-init-db" InitdbPath = "/docker-entrypoint-initdb.d" DefaultPkcs12KeystoreFile = "keystore.p12" DefaultPkcs12TruststoreFile = "truststore.p12" DefaultKeystorePasswordFile = "keystore-password" ) // Helper struct for holding server and/or client cert config // This struct is intended for internal use only and is only exposed outside the package so that the controllers can access type TLSCerts struct { // Server cert config ServerConfig *TLSConfig // Client cert config ClientConfig *TLSConfig // Image used for initContainers that help configure the TLS settings InitContainerImage *solr.ContainerImage } // Holds TLS options from the user config as well as other config properties determined during reconciliation // This struct is intended for internal use only and is only exposed outside the package so that the controllers can access type TLSConfig struct { // TLS options provided by the user in the CRD definition Options *solr.SolrTLSOptions // Flag to indicate if we need to convert the provided keystore into the p12 format needed by Java NeedsPkcs12InitContainer bool // The MD5 hash of the cert, used for restarting Solr pods after the cert updates if so desired CertMd5 string // The annotation varies based on the cert type (client or server) CertMd5Annotation string // The paths vary based on whether this config is for a client or server cert KeystorePath string TruststorePath string VolumePrefix string Namespace string } // Get a TLSCerts struct for reconciling TLS on a SolrCloud func TLSCertsForSolrCloud(instance *solr.SolrCloud) *TLSCerts { tls := &TLSCerts{ ServerConfig: &TLSConfig{ Options: instance.Spec.SolrTLS.DeepCopy(), KeystorePath: DefaultKeyStorePath, TruststorePath: DefaultTrustStorePath, CertMd5Annotation: SolrTlsCertMd5Annotation, Namespace: instance.Namespace, }, InitContainerImage: instance.Spec.BusyBoxImage, } if instance.Spec.SolrClientTLS != nil { tls.ClientConfig = &TLSConfig{ Options: instance.Spec.SolrClientTLS.DeepCopy(), KeystorePath: DefaultClientKeyStorePath, TruststorePath: DefaultClientTrustStorePath, VolumePrefix: "client-", CertMd5Annotation: SolrClientTlsCertMd5Annotation, Namespace: instance.Namespace, } } return tls } // Get a TLSCerts struct for reconciling TLS on an Exporter func TLSCertsForExporter(prometheusExporter *solr.SolrPrometheusExporter) *TLSCerts { // when using mounted dir option, we need a busy box image for our initContainers bbImage := prometheusExporter.Spec.BusyBoxImage if bbImage == nil { bbImage = &solr.ContainerImage{ Repository: solr.DefaultBusyBoxImageRepo, Tag: solr.DefaultBusyBoxImageVersion, PullPolicy: solr.DefaultPullPolicy, } } return &TLSCerts{ ClientConfig: &TLSConfig{ Options: prometheusExporter.Spec.SolrReference.SolrTLS.DeepCopy(), KeystorePath: DefaultKeyStorePath, TruststorePath: DefaultTrustStorePath, CertMd5Annotation: SolrClientTlsCertMd5Annotation, Namespace: prometheusExporter.Namespace, }, InitContainerImage: bbImage, } } // Enrich the config for a SolrCloud StatefulSet to enable TLS, either loaded from a secret or // a directory on the main pod containing per-pod specific TLS files. In the latter case, the "mounted dir" // typically comes from an external agent (such as a cert-manager extension) or CSI driver that injects the // pod-specific TLS files using mutating web hooks 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] 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 { mainContainer.Env = append(mainContainer.Env, tls.ClientConfig.clientEnvVars()...) } if serverCert.Options.PKCS12Secret != nil { // Cert comes from a secret, so setup the pod template to mount the secret serverCert.mountTLSSecretOnPodTemplate(&stateful.Spec.Template) // mount the client certificate from a different secret (at different mount points) if tls.ClientConfig != nil && tls.ClientConfig.Options.PKCS12Secret != nil { tls.ClientConfig.mountTLSSecretOnPodTemplate(&stateful.Spec.Template) } } else if tls.hasPasswordsInFiles() { // the TLS files come from some auto-mounted directory on the main container mountInitDbIfNeeded(stateful) // use an initContainer to create the wrapper script in the initdb stateful.Spec.Template.Spec.InitContainers = append(stateful.Spec.Template.Spec.InitContainers, tls.generateTLSInitdbScriptInitContainer()) } } // Enrich the config for a Prometheus Exporter Deployment to allow the exporter to make requests to TLS enabled Solr pods func (tls *TLSCerts) enableTLSOnExporterDeployment(deployment *appsv1.Deployment) { clientCert := tls.ClientConfig // Add the SOLR_SSL_* vars to the main container's environment mainContainer := &deployment.Spec.Template.Spec.Containers[0] mainContainer.Env = append(mainContainer.Env, clientCert.clientEnvVars()...) mainContainer.Env = append(mainContainer.Env, corev1.EnvVar{Name: "SOLR_SSL_CHECK_PEER_NAME", Value: strconv.FormatBool(clientCert.Options.CheckPeerName)}) // the exporter process doesn't read the SOLR_SSL_* env vars, so we need to pass them via JAVA_OPTS appendJavaOptsToEnv(mainContainer, clientCert.clientJavaOpts()) if clientCert.Options.PKCS12Secret != nil || clientCert.Options.TrustStoreSecret != nil { // Cert comes from a secret, so setup the pod template to mount the secret clientCert.mountTLSSecretOnPodTemplate(&deployment.Spec.Template) } else if clientCert.Options.MountedTLSDir != nil && clientCert.hasPasswordsInFiles() { // volumes and mounts for TLS when using the mounted dir option clientCert.mountTLSWrapperScriptAndInitContainer(deployment, tls.InitContainerImage) } } // Configures a pod template (either StatefulSet or Deployment) to mount the TLS files from a secret func (tls *TLSConfig) mountTLSSecretOnPodTemplate(template *corev1.PodTemplateSpec) *corev1.Container { mainContainer := &template.Spec.Containers[0] // the TLS files are mounted from a secret, setup the volumes and mounts vols, mounts := tls.volumesAndMounts() // 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 if tls.NeedsPkcs12InitContainer { vols = append(vols, corev1.Volume{Name: "pkcs12", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}) mounts = append(mounts, corev1.VolumeMount{Name: "pkcs12", ReadOnly: false, MountPath: DefaultWritableKeyStorePath}) pkcs12InitContainer := tls.generatePkcs12InitContainer(mainContainer.Image, mainContainer.ImagePullPolicy, mounts) template.Spec.InitContainers = append(template.Spec.InitContainers, pkcs12InitContainer) } template.Spec.Volumes = append(template.Spec.Volumes, vols...) mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, mounts...) // track the MD5 of the TLS cert (from secret) to trigger restarts if the cert changes if tls.Options.RestartOnTLSSecretUpdate && tls.CertMd5 != "" { if template.Annotations == nil { template.Annotations = make(map[string]string, 1) } template.Annotations[tls.CertMd5Annotation] = tls.CertMd5 } return mainContainer } // Make sure the secret containing the keystore and corresponding password secret exist and have the expected keys // Also, set up to watch for updates if desired // Also, verifies the configured truststore if provided func (tls *TLSConfig) VerifyKeystoreAndTruststoreSecretConfig(client *client.Client) (*corev1.Secret, error) { opts := tls.Options foundTLSSecret, err := verifyTLSSecretConfig(client, opts.PKCS12Secret.Name, tls.Namespace, opts.KeyStorePasswordSecret) if err != nil { return nil, err } else { if opts.RestartOnTLSSecretUpdate { err = tls.saveCertMd5(foundTLSSecret) if err != nil { return nil, err } } if _, ok := foundTLSSecret.Data[opts.PKCS12Secret.Key]; !ok { // the keystore.p12 key is not in the TLS secret, indicating we need to create it using an initContainer tls.NeedsPkcs12InitContainer = true } } // verify the truststore config is valid too if opts.TrustStoreSecret != nil { // verify the TrustStore secret is configured correctly passwordSecret := opts.TrustStorePasswordSecret if passwordSecret == nil { passwordSecret = opts.KeyStorePasswordSecret } _, err := verifyTLSSecretConfig(client, opts.TrustStoreSecret.Name, tls.Namespace, passwordSecret) if err != nil { return nil, err } } else { // does the supplied keystore secret also contain the truststore? if _, ok := foundTLSSecret.Data[DefaultPkcs12TruststoreFile]; ok { // there's a truststore in the supplied TLS secret opts.TrustStoreSecret = &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: foundTLSSecret.Name}, Key: DefaultPkcs12TruststoreFile} if opts.TrustStorePasswordSecret == nil { opts.TrustStorePasswordSecret = opts.KeyStorePasswordSecret } } } return foundTLSSecret, nil } // Special case where the user only configured a truststore for the exporter (no keystore) func (tls *TLSConfig) VerifyTruststoreOnly(client *client.Client) error { secret := tls.Options.TrustStoreSecret truststoreSecret, err := verifyTLSSecretConfig(client, secret.Name, tls.Namespace, tls.Options.TrustStorePasswordSecret) if err != nil { return err } // make sure truststore.p12 is actually in the supplied secret if _, ok := truststoreSecret.Data[secret.Key]; !ok { return fmt.Errorf("%s key not found in truststore password secret %s", secret.Key, secret.Name) } // If we have a watch on secrets, then get notified when the secret changes (such as after cert renewal) // capture the hash of the truststore and stash in an annotation so that pods get restarted if the cert changes // If watch = false, then we may be watching the keystore instead if tls.Options.RestartOnTLSSecretUpdate { tls.CertMd5 = fmt.Sprintf("%x", md5.Sum(truststoreSecret.Data[secret.Key])) } return nil } func (tls *TLSConfig) saveCertMd5(tlsSecret *corev1.Secret) error { // 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 tlsCertBytes, ok := tlsSecret.Data[TLSCertKey]; ok { tls.CertMd5 = fmt.Sprintf("%x", md5.Sum(tlsCertBytes)) return nil } return fmt.Errorf("%s key not found in TLS secret %s, cannot watch for updates to the cert without this data but 'restartOnTLSSecretUpdate' is enabled", TLSCertKey, tlsSecret.Name) } func (tls *TLSConfig) volumeName(baseName string) string { return tls.VolumePrefix + baseName } // Get a list of volumes for the keystore and optionally a truststore loaded from a TLS secret func (tls *TLSConfig) volumesAndMounts() ([]corev1.Volume, []corev1.VolumeMount) { optional := false vols := []corev1.Volume{} mounts := []corev1.VolumeMount{} keystoreSecretName := "" opts := tls.Options if opts.PKCS12Secret != nil { keystoreSecretName = opts.PKCS12Secret.Name volName := tls.volumeName("keystore") vols = append(vols, corev1.Volume{ Name: volName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: opts.PKCS12Secret.Name, DefaultMode: &SecretReadOnlyPermissions, Optional: &optional, }, }, }) mounts = append(mounts, corev1.VolumeMount{Name: volName, ReadOnly: true, MountPath: tls.KeystorePath}) } // 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 != keystoreSecretName { volName := tls.volumeName("truststore") vols = append(vols, corev1.Volume{ Name: volName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: opts.TrustStoreSecret.Name, DefaultMode: &SecretReadOnlyPermissions, Optional: &optional, }, }, }) mounts = append(mounts, corev1.VolumeMount{Name: volName, ReadOnly: true, MountPath: tls.TruststorePath}) } return vols, mounts } // Determine whether any passwords for Keystores/Truststores are stored in files func (tls *TLSCerts) hasPasswordsInFiles() (hasPasswordsInFiles bool) { return tls != nil && (tls.ServerConfig.hasPasswordsInFiles() || tls.ClientConfig.hasPasswordsInFiles()) } // Determine whether any passwords for Keystores/Truststores are stored in files func (tls *TLSConfig) hasPasswordsInFiles() (hasPasswordsInFiles bool) { if tls != nil && tls.Options.MountedTLSDir != nil { serverDir := tls.Options.MountedTLSDir hasPasswordsInFiles = serverDir.KeystorePasswordFile != "" || serverDir.KeystorePassword == "" if serverDir.TruststorePasswordFile != "" { hasPasswordsInFiles = true } } return } // Get the SOLR_SSL_* env vars for enabling TLS on Solr pods func (tls *TLSConfig) serverEnvVars() []corev1.EnvVar { opts := tls.Options // Determine the correct values for the SOLR_SSL_WANT_CLIENT_AUTH and SOLR_SSL_NEED_CLIENT_AUTH vars wantClientAuth := "false" needClientAuth := "false" if opts.ClientAuth == solr.Need { needClientAuth = "true" } else if opts.ClientAuth == solr.Want { wantClientAuth = "true" } envVars := []corev1.EnvVar{ { Name: "SOLR_SSL_ENABLED", Value: "true", }, { Name: "SOLR_SSL_WANT_CLIENT_AUTH", Value: wantClientAuth, }, { Name: "SOLR_SSL_NEED_CLIENT_AUTH", Value: needClientAuth, }, { Name: "SOLR_SSL_CHECK_PEER_NAME", Value: strconv.FormatBool(opts.CheckPeerName), }, } // tricky ... bin/solr checks for null SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION via -z to set -Dsolr.jetty.ssl.verifyClientHostName=HTTPS // so only add the SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION env var if false if !opts.VerifyClientHostname { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION", Value: "false"}) } // keystore / truststore come from either a mountedTLSDir or sourced from a secret mounted on the pod if opts.MountedTLSDir != nil { // TLS files are mounted by some external agent envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_KEY_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)}) keyStorePassword := "" if opts.MountedTLSDir.KeystorePassword != "" && opts.MountedTLSDir.KeystorePasswordFile == "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_KEY_STORE_PASSWORD", Value: opts.MountedTLSDir.KeystorePassword}) keyStorePassword = opts.MountedTLSDir.KeystorePassword } if opts.MountedTLSDir.TruststoreFile != "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE", Value: mountedTLSTruststorePath(opts.MountedTLSDir)}) trustStorePassword := opts.MountedTLSDir.TruststorePassword if trustStorePassword == "" && keyStorePassword != "" { trustStorePassword = keyStorePassword } if trustStorePassword != "" && opts.MountedTLSDir.TruststorePasswordFile == "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE_PASSWORD", Value: trustStorePassword}) } } else { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)}) if keyStorePassword != "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_TRUST_STORE_PASSWORD", Value: keyStorePassword}) } } } else { // keystore / truststore + passwords come from a secret envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_KEY_STORE")...) envVars = append(envVars, tls.truststoreEnvVars("SOLR_SSL_TRUST_STORE")...) } return envVars } // Set the SOLR_SSL_* for a Solr client process, e.g. the Exporter, which only needs a subset of SSL vars that a Solr pod would need func (tls *TLSConfig) clientEnvVars() []corev1.EnvVar { opts := tls.Options var envVars []corev1.EnvVar if opts.MountedTLSDir != nil { // passwords get exported from files in the TLS dir using an initdb wrapper script if they come from files keyStorePassword := "" if opts.MountedTLSDir.KeystoreFile != "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_KEY_STORE", Value: mountedTLSKeystorePath(opts.MountedTLSDir)}) if opts.MountedTLSDir.KeystorePassword != "" && opts.MountedTLSDir.KeystorePasswordFile == "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_KEY_STORE_PASSWORD", Value: opts.MountedTLSDir.KeystorePassword}) keyStorePassword = opts.MountedTLSDir.KeystorePassword } } if opts.MountedTLSDir.TruststoreFile != "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE", Value: mountedTLSTruststorePath(opts.MountedTLSDir)}) trustStorePassword := opts.MountedTLSDir.TruststorePassword if trustStorePassword == "" && keyStorePassword != "" { trustStorePassword = keyStorePassword } if trustStorePassword != "" && opts.MountedTLSDir.TruststorePasswordFile == "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", Value: trustStorePassword}) } } else if opts.MountedTLSDir.KeystoreFile != "" { envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE", Value: "$(SOLR_SSL_CLIENT_KEY_STORE)"}) envVars = append(envVars, corev1.EnvVar{Name: "SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", Value: keyStorePassword}) } } if opts.PKCS12Secret != nil { envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_CLIENT_KEY_STORE")...) // if no additional truststore secret provided, just use the keystore for both if opts.TrustStoreSecret == nil { envVars = append(envVars, tls.keystoreEnvVars("SOLR_SSL_CLIENT_TRUST_STORE")...) } } if opts.TrustStoreSecret != nil { envVars = append(envVars, tls.truststoreEnvVars("SOLR_SSL_CLIENT_TRUST_STORE")...) } return envVars } func (tls *TLSConfig) keystoreEnvVars(varName string) []corev1.EnvVar { return []corev1.EnvVar{ { Name: varName, Value: tls.keystoreFile(), }, { Name: varName + "_PASSWORD", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: tls.Options.KeyStorePasswordSecret}, }, } } // the keystore path depends on whether we're just loading it from the secret or whether // our initContainer has to generate it from the TLS secret using openssl // this complexity is due to the secret mount directory not being writable func (tls *TLSConfig) keystoreFile() string { var keystorePath string if tls.NeedsPkcs12InitContainer { keystorePath = DefaultWritableKeyStorePath } else { keystorePath = tls.KeystorePath } return keystorePath + "/" + DefaultPkcs12KeystoreFile } func (tls *TLSConfig) truststoreEnvVars(varName string) []corev1.EnvVar { opts := tls.Options keystoreSecretName := "" if opts.PKCS12Secret != nil { keystoreSecretName = opts.PKCS12Secret.Name } var truststoreFile string if opts.TrustStoreSecret != nil { if opts.TrustStoreSecret.Name != keystoreSecretName { // trust store is in a different secret, so will be mounted in a different dir truststoreFile = tls.TruststorePath + "/" + opts.TrustStoreSecret.Key } else { // trust store is a different key in the same secret as the keystore truststoreFile = tls.KeystorePath + "/" + DefaultPkcs12TruststoreFile } } else { // truststore is the same as the keystore truststoreFile = tls.keystoreFile() } var truststorePassFrom *corev1.EnvVarSource if opts.TrustStorePasswordSecret != nil { truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.TrustStorePasswordSecret} } else { truststorePassFrom = &corev1.EnvVarSource{SecretKeyRef: opts.KeyStorePasswordSecret} } return []corev1.EnvVar{ { Name: varName, Value: truststoreFile, }, { Name: varName + "_PASSWORD", ValueFrom: truststorePassFrom, }, } } // For the mounted dir approach, we need to customize the Prometheus exporter's entry point with a wrapper script // that reads the keystore / truststore passwords from a file and passes them via JAVA_OPTS func (tls *TLSConfig) mountTLSWrapperScriptAndInitContainer(deployment *appsv1.Deployment, initContainerImage *solr.ContainerImage) { opts := tls.Options mainContainer := &deployment.Spec.Template.Spec.Containers[0] volName := "tls-wrapper-script" mountPath := "/usr/local/solr-exporter-tls" wrapperScript := mountPath + "/launch-exporter-with-tls.sh" // the Prom exporter needs the keystore & truststore passwords in a Java system property, but the password // is stored in a file when using the mounted TLS dir approach, so we use a wrapper script around the main // container entry point to add these properties to JAVA_OPTS at runtime vol, mount := createEmptyVolumeAndMount(volName, mountPath) deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, *vol) mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *mount) entrypoint := mainContainer.Command[0] // may not have a keystore for the client cert, but must have have a truststore in that case kspJavaSysProp := "" catKsp := "" if opts.MountedTLSDir.KeystoreFile != "" { if opts.MountedTLSDir.KeystorePasswordFile != "" || opts.MountedTLSDir.KeystorePassword == "" { catKsp = fmt.Sprintf("SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=\\$(cat %s)", mountedTLSKeystorePasswordPath(opts.MountedTLSDir)) kspJavaSysProp = " -Djavax.net.ssl.keyStorePassword=\\${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}" } } tspJavaSysProp := "" catTsp := "" if opts.MountedTLSDir.TruststorePasswordFile != "" || opts.MountedTLSDir.TruststorePassword == "" { catTsp = fmt.Sprintf("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=\\$(cat %s)", mountedTLSTruststorePasswordPath(opts.MountedTLSDir)) tspJavaSysProp = " -Djavax.net.ssl.trustStorePassword=\\${SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD}" } else if catKsp != "" { tspJavaSysProp = " -Djavax.net.ssl.trustStorePassword=\\${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}" } /* Create a wrapper script like: #!/bin/bash ksp=$(cat $MOUNTED_TLS_DIR/keystore-password) tsp=$(cat $MOUNTED_TLS_DIR/truststore-password) JAVA_OPTS="${JAVA_OPTS} -Djavax.net.ssl.keyStorePassword=${ksp} -Djavax.net.ssl.trustStorePassword=${tsp}" /opt/solr/contrib/prometheus-exporter/bin/solr-exporter $@ */ writeWrapperScript := fmt.Sprintf("cat << EOF > %s\n#!/bin/bash\n%s\n%s\nJAVA_OPTS=\"\\${JAVA_OPTS}%s%s\"\n%s \\$@\nEOF\nchmod +x %s", wrapperScript, catKsp, catTsp, kspJavaSysProp, tspJavaSysProp, entrypoint, wrapperScript) createTLSWrapperScriptInitContainer := corev1.Container{ Name: "create-tls-wrapper-script", Image: initContainerImage.ToImageName(), ImagePullPolicy: initContainerImage.PullPolicy, Command: []string{"sh", "-c", writeWrapperScript}, VolumeMounts: []corev1.VolumeMount{{Name: volName, MountPath: mountPath}}, } deployment.Spec.Template.Spec.InitContainers = append(deployment.Spec.Template.Spec.InitContainers, createTLSWrapperScriptInitContainer) // Call the wrapper script to start the exporter process mainContainer.Command = []string{wrapperScript} } // Create an initContainer that generates the initdb script that exports the keystore / truststore passwords stored in // a directory to the environment; this is only needed when using the mountedTLSDir approach func (tls *TLSCerts) generateTLSInitdbScriptInitContainer() corev1.Container { exportServerKeystorePassword, exportServerTruststorePassword := "", "" if tls.ServerConfig.Options.MountedTLSDir != nil { mountedDir := tls.ServerConfig.Options.MountedTLSDir if mountedDir.KeystorePasswordFile != "" || mountedDir.KeystorePassword == "" { exportServerKeystorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_KEY_STORE_PASSWORD", mountedTLSKeystorePasswordPath(tls.ServerConfig.Options.MountedTLSDir)) exportServerTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_TRUST_STORE_PASSWORD", "${SOLR_SSL_KEY_STORE_PASSWORD}") } if mountedDir.TruststorePasswordFile != "" { exportServerTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_TRUST_STORE_PASSWORD", mountedTLSTruststorePasswordPath(tls.ServerConfig.Options.MountedTLSDir)) } else if mountedDir.TruststorePassword != "" { exportServerTruststorePassword = "" } } // Might have a client cert too ... exportClientKeystorePassword, exportClientTruststorePassword := "", "" if tls.ClientConfig != nil && tls.ClientConfig.Options.MountedTLSDir != nil { mountedDir := tls.ClientConfig.Options.MountedTLSDir if mountedDir.KeystorePasswordFile != "" || mountedDir.KeystorePassword == "" { exportClientKeystorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_KEY_STORE_PASSWORD", mountedTLSKeystorePasswordPath(mountedDir)) exportClientTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", "${SOLR_SSL_CLIENT_KEY_STORE_PASSWORD}") } if mountedDir.TruststorePasswordFile != "" { exportClientTruststorePassword = exportVarFromFileInInitdbWrapperScript("SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD", mountedTLSTruststorePasswordPath(mountedDir)) } else if mountedDir.TruststorePassword != "" { exportClientTruststorePassword = "" } } else { exportClientKeystorePassword = exportServerKeystorePassword exportClientKeystorePassword = exportServerTruststorePassword } shCmd := fmt.Sprintf("echo -e \"#!/bin/bash\\n%s%s%s%s\"", exportServerKeystorePassword, exportServerTruststorePassword, exportClientKeystorePassword, exportClientTruststorePassword) shCmd += " > /docker-entrypoint-initdb.d/export-tls-vars.sh" /* Init container creates a script like: #!/bin/bash export SOLR_SSL_KEY_STORE_PASSWORD=`cat $MOUNTED_SERVER_TLS_DIR/keystore-password` export SOLR_SSL_TRUST_STORE_PASSWORD=`cat $MOUNTED_SERVER_TLS_DIR/truststore-password` export SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=`cat $MOUNTED_CLIENT_TLS_DIR/keystore-password` export SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=`cat $MOUNTED_CLIENT_TLS_DIR/truststore-password` */ return corev1.Container{ Name: InitdbInitContainer, Image: tls.InitContainerImage.ToImageName(), ImagePullPolicy: tls.InitContainerImage.PullPolicy, Command: []string{"sh", "-c", shCmd}, VolumeMounts: []corev1.VolumeMount{{Name: "initdb", MountPath: InitdbPath}}, } } // Helper function for writing a line to the initdb wrapper script that exports an env var sourced from a file func exportVarFromFileInInitdbWrapperScript(varName string, varValue string) string { if strings.HasPrefix(varValue, "${") { // This is a pass-through variable return fmt.Sprintf("\\nexport %s=%q\\n", varName, varValue) } else { // This is a file to "cat" return fmt.Sprintf("\\nexport %s=\"$(cat %q)\"\\n", varName, varValue) } } // Returns an array of Java system properties to configure the TLS certificate used by client applications to call mTLS enabled Solr pods func (tls *TLSConfig) clientJavaOpts() []string { // for clients, we should always have a truststore but the keystore is optional javaOpts := []string{ "-Dsolr.ssl.checkPeerName=$(SOLR_SSL_CHECK_PEER_NAME)", "-Djavax.net.ssl.trustStore=$(SOLR_SSL_CLIENT_TRUST_STORE)", "-Djavax.net.ssl.trustStoreType=PKCS12", } if tls.Options.PKCS12Secret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.KeystoreFile != "") { javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStore=$(SOLR_SSL_CLIENT_KEY_STORE)") javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStoreType=PKCS12") } hasKeyStorePassword := false if tls.Options.PKCS12Secret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.KeystorePasswordFile == "" && tls.Options.MountedTLSDir.KeystorePassword != "") { hasKeyStorePassword = true javaOpts = append(javaOpts, "-Djavax.net.ssl.keyStorePassword=$(SOLR_SSL_CLIENT_KEY_STORE_PASSWORD)") } // else for mounted dir option, the password comes from the wrapper script if tls.Options.PKCS12Secret != nil || tls.Options.TrustStoreSecret != nil || (tls.Options.MountedTLSDir != nil && tls.Options.MountedTLSDir.TruststorePasswordFile == "" && (tls.Options.MountedTLSDir.TruststorePassword != "" || hasKeyStorePassword)) { javaOpts = append(javaOpts, "-Djavax.net.ssl.trustStorePassword=$(SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD)") } // else for mounted dir option, the password comes from the wrapper script return javaOpts } func (tls *TLSConfig) generatePkcs12InitContainer(imageName string, imagePullPolicy corev1.PullPolicy, mounts []corev1.VolumeMount) corev1.Container { // get the keystore password from the env for generating the keystore using openssl envVars := []corev1.EnvVar{ { Name: "SOLR_SSL_KEY_STORE_PASSWORD", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: tls.Options.KeyStorePasswordSecret}, }, } caCrtFileName := DefaultKeyStorePath + "/ca.crt" cmd := "OPTIONAL_CACRT=\"$(test -e " + caCrtFileName + " && echo ' -certfile " + caCrtFileName + "')\"; " + "openssl pkcs12 -export -in " + DefaultKeyStorePath + "/" + TLSCertKey + " $OPTIONAL_CACRT " + "-inkey " + DefaultKeyStorePath + "/tls.key -out " + DefaultKeyStorePath + "/pkcs12/" + DefaultPkcs12KeystoreFile + " -passout pass:${SOLR_SSL_KEY_STORE_PASSWORD}" return corev1.Container{ Name: "gen-pkcs12-keystore", Image: imageName, ImagePullPolicy: imagePullPolicy, TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: "File", Command: []string{"sh", "-c", cmd}, VolumeMounts: mounts, Env: envVars, } } func mountedTLSKeystorePath(tlsDir *solr.MountedTLSDirectory) string { return mountedTLSPath(tlsDir, tlsDir.KeystoreFile, DefaultPkcs12KeystoreFile) } func mountedTLSKeystorePasswordPath(tlsDir *solr.MountedTLSDirectory) string { return mountedTLSPath(tlsDir, tlsDir.KeystorePasswordFile, DefaultKeystorePasswordFile) } func mountedTLSTruststorePath(tlsDir *solr.MountedTLSDirectory) string { return mountedTLSPath(tlsDir, tlsDir.TruststoreFile, DefaultPkcs12TruststoreFile) } func mountedTLSTruststorePasswordPath(tlsDir *solr.MountedTLSDirectory) string { path := "" if tlsDir.TruststorePasswordFile != "" { path = mountedTLSPath(tlsDir, tlsDir.TruststorePasswordFile, "") } else if tlsDir.TruststorePassword == "" { path = mountedTLSKeystorePasswordPath(tlsDir) } return path } func mountedTLSPath(dir *solr.MountedTLSDirectory, fileName string, defaultName string) string { if fileName == "" { fileName = defaultName } return fmt.Sprintf("%s/%s", dir.Path, fileName) } // Command to set the urlScheme cluster prop to "https" func setUrlSchemeClusterPropCmd() string { return "/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd clusterprop -name urlScheme -val https" + "; /opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd get /clusterprops.json;" } // Appends additional Java system properties to the JAVA_OPTS environment variable of the main container and ensures JAVA_OPTS is the last env var func appendJavaOptsToEnv(mainContainer *corev1.Container, additionalJavaOpts []string) { javaOptsValue := "" javaOptsAt := -1 for i, v := range mainContainer.Env { if v.Name == "JAVA_OPTS" { javaOptsValue = v.Value javaOptsAt = i break } } javaOptsVar := corev1.EnvVar{Name: "JAVA_OPTS", Value: strings.TrimSpace(javaOptsValue + " " + strings.Join(additionalJavaOpts, " "))} if javaOptsAt == -1 { // no JAVA_OPTS, add it on the end mainContainer.Env = append(mainContainer.Env, javaOptsVar) } else { // need to move the JAVA_OPTS var to end of array, slice it out ... envVars := mainContainer.Env[0:javaOptsAt] if javaOptsAt < len(mainContainer.Env)-1 { envVars = append(envVars, mainContainer.Env[javaOptsAt+1:]...) } mainContainer.Env = append(envVars, javaOptsVar) } } // Utility func to create an empty dir and corresponding mount func createEmptyVolumeAndMount(name string, mountPath string) (*corev1.Volume, *corev1.VolumeMount) { return &corev1.Volume{Name: name, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, &corev1.VolumeMount{Name: name, MountPath: mountPath} } // The Docker Solr framework allows us to run scripts from an initdb directory before the main Solr process is started // Mount the initdb directory if it has not already been mounted by the user via custom pod options func mountInitDbIfNeeded(stateful *appsv1.StatefulSet) { // Auto-TLS uses an initContainer to create a script in the initdb, so mount that if it has not already been mounted mainContainer := &stateful.Spec.Template.Spec.Containers[0] var initdbMount *corev1.VolumeMount for _, mount := range mainContainer.VolumeMounts { if mount.MountPath == InitdbPath { initdbMount = &mount break } } if initdbMount == nil { vol, mount := createEmptyVolumeAndMount("initdb", InitdbPath) stateful.Spec.Template.Spec.Volumes = append(stateful.Spec.Template.Spec.Volumes, *vol) mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, *mount) } } // Utility method used during reconcile to verify a TLS secret exists and has the correct key // Also verifies the corresponding password secret exists and has the expected key // Used for verifying keystore and truststore configuration func verifyTLSSecretConfig(client *client.Client, secretName string, secretNamespace string, passwordSecret *corev1.SecretKeySelector) (*corev1.Secret, error) { ctx := context.TODO() reader := *client foundTLSSecret := &corev1.Secret{} lookupErr := reader.Get(ctx, types.NamespacedName{Name: secretName, Namespace: secretNamespace}, foundTLSSecret) if lookupErr != nil { return nil, lookupErr } else { if passwordSecret == nil { return nil, fmt.Errorf("no password secret configured for %s", secretName) } // Make sure the secret containing the keystore password exists as well keyStorePasswordSecret := &corev1.Secret{} err := reader.Get(ctx, types.NamespacedName{Name: passwordSecret.Name, Namespace: foundTLSSecret.Namespace}, keyStorePasswordSecret) if err != nil { return nil, err } // 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 password secret %s", passwordSecret.Key, keyStorePasswordSecret.Name) } } return foundTLSSecret, nil }