| /* |
| * 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 org.apache.cassandra.security; |
| |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.util.Map; |
| import java.util.Optional; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import org.apache.commons.lang3.StringUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.cassandra.config.EncryptionOptions; |
| |
| /** |
| * Custom {@link ISslContextFactory} implementation based on Kubernetes Secrets. It allows the keystore and |
| * truststore paths to be configured from the K8 secrets via volumeMount and passwords via K8 secrets environment |
| * variables. The official Kubernetes Secret Spec can be found <a href="https://kubernetes.io/docs/concepts/configuration/secret/ ">here</a>. |
| * |
| * When keystore or truststore is updated, this implementation can detect that based on updated K8 secrets |
| * at the mounted paths ({@code KEYSTORE_UPDATED_TIMESTAMP_PATH} for the keystore and {@code |
| * TRUSTSTORE_UPDATED_TIMESTAMP_PATH} for the truststore. The values in those paths are expected to be numeric values. |
| * The most obvious choice might be to just use the time in nano/milli-seconds precision but any other strategy would work |
| * as well, as far as the comparison of those values can be done in a consistent/predictable manner. Again, those |
| * values do not have to necessarily reflect actual file's update timestamps, using the actual file's timestamps is |
| * just one of the valid options to signal updates. |
| * |
| * Defaults: |
| * <pre> |
| * keystore path = /etc/my-ssl-store/keystore |
| * keystore password = cassandra |
| * keystore updated timestamp path = /etc/my-ssl-store/keystore-last-updatedtime |
| * truststore path = /etc/my-ssl-store/truststore |
| * truststore password = cassandra |
| * truststore updated timestamp path = /etc/my-ssl-store/truststore-last-updatedtime |
| * </pre> |
| * |
| * Customization: In order to customize the K8s secret configuration, override appropriate values in the below Cassandra |
| * configuration. The similar configuration can be applied to {@code client_encryption_options}. |
| * <pre> |
| * server_encryption_options: |
| * internode_encryption: none |
| * ssl_context_factory: |
| * class_name: org.apache.cassandra.security.KubernetesSecretsSslContextFactory |
| * parameters: |
| * KEYSTORE_PASSWORD_ENV_VAR: KEYSTORE_PASSWORD |
| * KEYSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/keystore-last-updatedtime |
| * TRUSTSTORE_PASSWORD_ENV_VAR: TRUSTSTORE_PASSWORD |
| * TRUSTSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/truststore-last-updatedtime |
| * keystore: /etc/my-ssl-store/keystore |
| * truststore: /etc/my-ssl-store/truststore |
| * </pre> |
| * |
| * Below is the corresponding sample YAML configuration for K8 env. |
| * <pre> |
| * apiVersion: v1 |
| * kind: Pod |
| * metadata: |
| * name: my-pod |
| * labels: |
| * app: my-app |
| * spec: |
| * containers: |
| * - name: my-app |
| * image: my-app:latest |
| * imagePullPolicy: Always |
| * env: |
| * - name: KEYSTORE_PASSWORD |
| * valueFrom: |
| * secretKeyRef: |
| * name: my-ssl-store |
| * key: keystore-password |
| * - name: TRUSTSTORE_PASSWORD |
| * valueFrom: |
| * secretKeyRef: |
| * name: my-ssl-store |
| * key: truststore-password |
| * volumeMounts: |
| * - name: my-ssl-store |
| * mountPath: "/etc/my-ssl-store" |
| * readOnly: true |
| * volumes: |
| * - name: my-ssl-store |
| * secret: |
| * secretName: my-ssl-store |
| * items: |
| * - key: cassandra_ssl_keystore |
| * path: keystore |
| * - key: keystore-last-updatedtime |
| * path: keystore-last-updatedtime |
| * - key: cassandra_ssl_truststore |
| * path: truststore |
| * - key: truststore-last-updatedtime |
| * path: truststore-last-updatedtime |
| * </pre> |
| */ |
| public class KubernetesSecretsSslContextFactory extends FileBasedSslContextFactory |
| { |
| private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsSslContextFactory.class); |
| |
| /** |
| * Use below config-keys to configure this factory. |
| */ |
| public interface ConfigKeys { |
| String KEYSTORE_PASSWORD_ENV_VAR = "KEYSTORE_PASSWORD_ENV_VAR"; |
| String TRUSTSTORE_PASSWORD_ENV_VAR = "TRUSTSTORE_PASSWORD_ENV_VAR"; |
| String KEYSTORE_UPDATED_TIMESTAMP_PATH = "KEYSTORE_UPDATED_TIMESTAMP_PATH"; |
| String TRUSTSTORE_UPDATED_TIMESTAMP_PATH = "TRUSTSTORE_UPDATED_TIMESTAMP_PATH"; |
| } |
| |
| public static final String DEFAULT_KEYSTORE_PASSWORD = ""; |
| public static final String DEFAULT_TRUSTSTORE_PASSWORD = ""; |
| |
| @VisibleForTesting |
| static final String DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME = "KEYSTORE_PASSWORD"; |
| @VisibleForTesting |
| static final String DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME = "TRUSTSTORE_PASSWORD"; |
| |
| private static final String KEYSTORE_PATH_VALUE = "/etc/my-ssl-store/keystore"; |
| private static final String TRUSTSTORE_PATH_VALUE = "/etc/my-ssl-store/truststore"; |
| private static final String KEYSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME; |
| private static final String KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/keystore-last-updatedtime"; |
| private static final String TRUSTSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME; |
| private static final String TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/truststore-last-updatedtime"; |
| |
| private final String keystoreUpdatedTimeSecretKeyPath; |
| private final String truststoreUpdatedTimeSecretKeyPath; |
| private long keystoreLastUpdatedTime; |
| private long truststoreLastUpdatedTime; |
| |
| public KubernetesSecretsSslContextFactory() |
| { |
| keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE); |
| keystore_password = getValueFromEnv(KEYSTORE_PASSWORD_ENV_VAR_NAME, |
| DEFAULT_KEYSTORE_PASSWORD); |
| truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE); |
| truststore_password = getValueFromEnv(TRUSTSTORE_PASSWORD_ENV_VAR_NAME, |
| DEFAULT_TRUSTSTORE_PASSWORD); |
| keystoreLastUpdatedTime = System.nanoTime(); |
| keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH, |
| KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE); |
| truststoreLastUpdatedTime = keystoreLastUpdatedTime; |
| truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH, |
| TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE); |
| } |
| |
| public KubernetesSecretsSslContextFactory(Map<String, Object> parameters) |
| { |
| super(parameters); |
| keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE); |
| keystore_password = getValueFromEnv(getString(ConfigKeys.KEYSTORE_PASSWORD_ENV_VAR, |
| KEYSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_KEYSTORE_PASSWORD); |
| truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE); |
| truststore_password = getValueFromEnv(getString(ConfigKeys.TRUSTSTORE_PASSWORD_ENV_VAR, |
| TRUSTSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_TRUSTSTORE_PASSWORD); |
| keystoreLastUpdatedTime = System.nanoTime(); |
| keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH, |
| KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE); |
| truststoreLastUpdatedTime = keystoreLastUpdatedTime; |
| truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH, |
| TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE); |
| } |
| |
| @Override |
| public synchronized void initHotReloading() { |
| // No-op |
| } |
| |
| /** |
| * Checks mounted paths for {@code KEYSTORE_UPDATED_TIMESTAMP_PATH} and {@code TRUSTSTORE_UPDATED_TIMESTAMP_PATH} |
| * and compares the values for those variables with the current timestamps. In case the mounted paths are |
| * not valid (either they are not initialized yet, got removed or got corrupted in-flight), this method considers |
| * that nothing has changed. |
| * @return {@code true} if either of the timestamps (keystore or truststore) got updated;{@code false} otherwise |
| */ |
| @Override |
| public boolean shouldReload() |
| { |
| return hasKeystoreUpdated() || hasTruststoreUpdated(); |
| } |
| |
| @VisibleForTesting |
| String getValueFromEnv(String envVarName, String defaultValue) { |
| String valueFromEnv = StringUtils.isEmpty(envVarName) ? null : System.getenv(envVarName); |
| return StringUtils.isEmpty(valueFromEnv) ? defaultValue : valueFromEnv; |
| } |
| |
| private boolean hasKeystoreUpdated() { |
| long keystoreUpdatedTime = getKeystoreLastUpdatedTime(); |
| logger.info("Comparing keystore timestamps oldValue {} and newValue {}", keystoreLastUpdatedTime, |
| keystoreUpdatedTime); |
| if (keystoreUpdatedTime > keystoreLastUpdatedTime) { |
| logger.info("Updating the keystoreLastUpdatedTime from oldValue {} to newValue {}", |
| keystoreLastUpdatedTime, keystoreUpdatedTime); |
| keystoreLastUpdatedTime = keystoreUpdatedTime; |
| return true; |
| } else { |
| logger.info("Based on the comparision, no keystore update needed"); |
| return false; |
| } |
| } |
| |
| private boolean hasTruststoreUpdated() { |
| long truststoreUpdatedTime = getTruststoreLastUpdatedTime(); |
| logger.info("Comparing truststore timestamps oldValue {} and newValue {}", truststoreLastUpdatedTime, |
| truststoreUpdatedTime); |
| if (truststoreUpdatedTime > truststoreLastUpdatedTime) { |
| logger.info("Updating the truststoreLastUpdatedTime from oldValue {} to newValue {}", |
| truststoreLastUpdatedTime, truststoreUpdatedTime); |
| truststoreLastUpdatedTime = truststoreUpdatedTime; |
| return true; |
| } else { |
| logger.info("Based on the comparision, no truststore update needed"); |
| return false; |
| } |
| } |
| |
| private long getKeystoreLastUpdatedTime() { |
| Optional<String> keystoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(keystoreUpdatedTimeSecretKeyPath); |
| if (keystoreUpdatedTimeSecretKeyValue.isPresent()) |
| { |
| return parseLastUpdatedTime(keystoreUpdatedTimeSecretKeyValue.get(), keystoreLastUpdatedTime); |
| } |
| else |
| { |
| logger.warn("Failed to load {}'s value. Will use existing value {}", keystoreUpdatedTimeSecretKeyPath, |
| keystoreLastUpdatedTime); |
| return keystoreLastUpdatedTime; |
| } |
| } |
| |
| private long getTruststoreLastUpdatedTime() { |
| Optional<String> truststoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(truststoreUpdatedTimeSecretKeyPath); |
| if (truststoreUpdatedTimeSecretKeyValue.isPresent()) |
| { |
| return parseLastUpdatedTime(truststoreUpdatedTimeSecretKeyValue.get(), truststoreLastUpdatedTime); |
| } |
| else |
| { |
| logger.warn("Failed to load {}'s value. Will use existing value {}", truststoreUpdatedTimeSecretKeyPath, |
| truststoreLastUpdatedTime); |
| return truststoreLastUpdatedTime; |
| } |
| } |
| |
| private Optional<String> readSecretFromMountedVolume(String secretKeyPath) { |
| try |
| { |
| return Optional.of(new String(Files.readAllBytes(Paths.get(secretKeyPath)))); |
| } |
| catch (IOException e) |
| { |
| logger.warn(String.format("Failed to read secretKeyPath %s from the mounted volume: %s", secretKeyPath, e.getMessage())); |
| return Optional.empty(); |
| } |
| } |
| |
| private long parseLastUpdatedTime(String latestUpdatedTime, long currentUpdatedTime) { |
| try |
| { |
| return Long.parseLong(latestUpdatedTime); |
| } catch(NumberFormatException e) { |
| logger.warn("Failed to parse the latestUpdatedTime {}. Will use current time {}", latestUpdatedTime, |
| currentUpdatedTime, e); |
| return currentUpdatedTime; |
| } |
| } |
| } |