cassandra-trunk/examples/ssl-factory/src/org/apache/cassandra/security/KubernetesSecretsPEMSslContextFactory.java
/*
 * 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.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;

import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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>.
 * <p>
 * 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.
 * <p>
 * Defaults:
 * <pre>
 *     private key password = cassandra
 *     keystore updated timestamp path = /etc/my-ssl-store/keystore-last-updatedtime
 *     truststore updated timestamp path = /etc/my-ssl-store/truststore-last-updatedtime
 * </pre>
 * <p>
 * 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.KubernetesSecretsPEMSslContextFactory
 *         parameters:
 *           PRIVATE_KEY_ENV_VAR: PRIVATE_KEY
 *           PRIVATE_KEY_PASSWORD_ENV_VAR: PRIVATE_KEY_PASSWORD
 *           KEYSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/keystore-last-updatedtime
 *           TRUSTED_CERTIFICATES_ENV_VAR: TRUSTED_CERTIFICATES
 *           TRUSTSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/truststore-last-updatedtime
 * </pre>
 * <p>
 * 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: PRIVATE_KEY
 *         valueFrom:
 *           secretKeyRef:
 *             name: my-ssl-store
 *             key: private-key
 *       - name: PRIVATE_KEY_PASSWORD
 *         valueFrom:
 *           secretKeyRef:
 *             name: my-ssl-store
 *             key: private-key-password
 *       - name: TRUSTED_CERTIFICATES
 *         valueFrom:
 *           secretKeyRef:
 *             name: my-ssl-store
 *             key: trusted-certificates
 *     volumeMounts:
 *     - name: my-ssl-store
 *       mountPath: "/etc/my-ssl-store"
 *       readOnly: true
 *   volumes:
 *   - name: my-ssl-store
 *     secret:
 *       secretName: my-ssl-store
 *       items:
 *         - key: keystore-last-updatedtime
 *           path: keystore-last-updatedtime
 *         - key: truststore-last-updatedtime
 *           path: truststore-last-updatedtime
 * </pre>
 */
public class KubernetesSecretsPEMSslContextFactory extends KubernetesSecretsSslContextFactory
{
    public static final String DEFAULT_PRIVATE_KEY = "";
    public static final String DEFAULT_PRIVATE_KEY_PASSWORD = "";
    public static final String DEFAULT_TRUSTED_CERTIFICATES = "";

    @VisibleForTesting
    static final String DEFAULT_PRIVATE_KEY_ENV_VAR_NAME = "PRIVATE_KEY";
    @VisibleForTesting
    static final String DEFAULT_PRIVATE_KEY_PASSWORD_ENV_VAR_NAME = "PRIVATE_KEY_PASSWORD";
    @VisibleForTesting
    static final String DEFAULT_TRUSTED_CERTIFICATES_ENV_VAR_NAME = "TRUSTED_CERTIFICATES";

    private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsPEMSslContextFactory.class);
    private String pemEncodedKey;
    private String keyPassword;
    private String pemEncodedCertificates;
    private PEMBasedSslContextFactory pemBasedSslContextFactory;

    public boolean checkedExpiry = false;

    public KubernetesSecretsPEMSslContextFactory()
    {
        pemBasedSslContextFactory = new PEMBasedSslContextFactory();
    }

    public KubernetesSecretsPEMSslContextFactory(Map<String, Object> parameters)
    {
        super(parameters);

        pemEncodedKey = getValueFromEnv(getString(PEMConfigKey.PRIVATE_KEY_ENV_VAR, DEFAULT_PRIVATE_KEY_ENV_VAR_NAME),
                                        DEFAULT_PRIVATE_KEY);
        keyPassword = getValueFromEnv(getString(PEMConfigKey.PRIVATE_KEY_PASSWORD_ENV_VAR,
                                                DEFAULT_PRIVATE_KEY_PASSWORD_ENV_VAR_NAME),
                                      DEFAULT_PRIVATE_KEY_PASSWORD);
        pemEncodedCertificates = getValueFromEnv(getString(PEMConfigKey.TRUSTED_CERTIFICATE_ENV_VAR, DEFAULT_TRUSTED_CERTIFICATES_ENV_VAR_NAME),
                                                 DEFAULT_TRUSTED_CERTIFICATES);

        parameters.put(PEMBasedSslContextFactory.ConfigKey.ENCODED_KEY.getKeyName(), pemEncodedKey);
        parameters.put(PEMBasedSslContextFactory.ConfigKey.KEY_PASSWORD.getKeyName(), keyPassword);
        parameters.put(PEMBasedSslContextFactory.ConfigKey.ENCODED_CERTIFICATES.getKeyName(), pemEncodedCertificates);

        pemBasedSslContextFactory = new PEMBasedSslContextFactory(parameters);
    }

    @Override
    public synchronized void initHotReloading()
    {
        // No-op
    }

    @Override
    public boolean hasKeystore()
    {
        return pemBasedSslContextFactory.hasKeystore();
    }

    @Override
    protected KeyManagerFactory buildKeyManagerFactory() throws SSLException
    {
        KeyManagerFactory kmf = pemBasedSslContextFactory.buildKeyManagerFactory();
        checkedExpiry = pemBasedSslContextFactory.keystoreContext.checkedExpiry;
        return kmf;
    }

    @Override
    protected TrustManagerFactory buildTrustManagerFactory() throws SSLException
    {
        return pemBasedSslContextFactory.buildTrustManagerFactory();
    }

    public interface PEMConfigKey
    {
        String PRIVATE_KEY_ENV_VAR = "PRIVATE_KEY_ENV_VAR";
        String PRIVATE_KEY_PASSWORD_ENV_VAR = "PRIVATE_KEY_PASSWORD_ENV_VAR";
        String TRUSTED_CERTIFICATE_ENV_VAR = "TRUSTED_CERTIFICATE_ENV_VAR";
    }
}
