blob: 93668d9d04d6fd8447039b21aabc265a13d35784 [file] [log] [blame]
/*
* 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.config;
import java.io.File;
import java.util.List;
import java.util.Objects;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.locator.IEndpointSnitch;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.security.SSLFactory;
public class EncryptionOptions
{
Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
public enum TlsEncryptionPolicy
{
UNENCRYPTED("unencrypted"), OPTIONAL("optionally encrypted"), ENCRYPTED("encrypted");
private final String description;
TlsEncryptionPolicy(String description)
{
this.description = description;
}
public String description()
{
return description;
}
}
public final String keystore;
public final String keystore_password;
public final String truststore;
public final String truststore_password;
public final List<String> cipher_suites;
protected String protocol;
protected List<String> accepted_protocols;
public final String algorithm;
public final String store_type;
public final boolean require_client_auth;
public final boolean require_endpoint_verification;
// ServerEncryptionOptions does not use the enabled flag at all instead using the existing
// internode_encryption option. So we force this private and expose through isEnabled
// so users of ServerEncryptionOptions can't accidentally use this when they should use isEnabled
// Long term we need to refactor ClientEncryptionOptions and ServerEncyrptionOptions to be separate
// classes so we can choose appropriate configuration for each.
// See CASSANDRA-15262 and CASSANDRA-15146
protected Boolean enabled;
protected Boolean optional;
// Calculated by calling applyConfig() after populating/parsing
protected Boolean isEnabled = null;
protected Boolean isOptional = null;
public EncryptionOptions()
{
keystore = "conf/.keystore";
keystore_password = "cassandra";
truststore = "conf/.truststore";
truststore_password = "cassandra";
cipher_suites = null;
protocol = null;
accepted_protocols = null;
algorithm = null;
store_type = "JKS";
require_client_auth = false;
require_endpoint_verification = false;
enabled = null;
optional = null;
}
public EncryptionOptions(String keystore, String keystore_password, String truststore, String truststore_password, List<String> cipher_suites, String protocol, List<String> accepted_protocols, String algorithm, String store_type, boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled, Boolean optional)
{
this.keystore = keystore;
this.keystore_password = keystore_password;
this.truststore = truststore;
this.truststore_password = truststore_password;
this.cipher_suites = cipher_suites;
this.protocol = protocol;
this.accepted_protocols = accepted_protocols;
this.algorithm = algorithm;
this.store_type = store_type;
this.require_client_auth = require_client_auth;
this.require_endpoint_verification = require_endpoint_verification;
this.enabled = enabled;
this.optional = optional;
}
public EncryptionOptions(EncryptionOptions options)
{
keystore = options.keystore;
keystore_password = options.keystore_password;
truststore = options.truststore;
truststore_password = options.truststore_password;
cipher_suites = options.cipher_suites;
protocol = options.protocol;
accepted_protocols = options.accepted_protocols;
algorithm = options.algorithm;
store_type = options.store_type;
require_client_auth = options.require_client_auth;
require_endpoint_verification = options.require_endpoint_verification;
enabled = options.enabled;
this.optional = options.optional;
}
/* Computes enabled and optional before use. Because the configuration can be loaded
* through pluggable mechanisms this is the only safe way to make sure that
* enabled and optional are set correctly.
*/
public EncryptionOptions applyConfig()
{
ensureConfigNotApplied();
isEnabled = this.enabled != null && enabled;
if (optional != null)
{
isOptional = optional;
}
// If someone is asking for an _insecure_ connection and not explicitly telling us to refuse
// encrypted connections AND they have a keystore file, we assume they would like to be able
// to transition to encrypted connections in the future.
else if (new File(keystore).exists())
{
isOptional = !isEnabled;
}
else
{
// Otherwise if there's no keystore, not possible to establish an optional secure connection
isOptional = false;
}
return this;
}
private void ensureConfigApplied()
{
if (isEnabled == null || isOptional == null)
throw new IllegalStateException("EncryptionOptions.applyConfig must be called first");
}
private void ensureConfigNotApplied()
{
if (isEnabled != null || isOptional != null)
throw new IllegalStateException("EncryptionOptions cannot be changed after configuration applied");
}
/**
* Indicates if the channel should be encrypted. Client and Server uses different logic to determine this
*
* @return if the channel should be encrypted
*/
public Boolean isEnabled() {
ensureConfigApplied();
return isEnabled;
}
/**
* Sets if encryption should be enabled for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating enabled state
* is probably a bad idea.
* @param enabled value to set
*/
public void setEnabled(Boolean enabled) {
ensureConfigNotApplied();
this.enabled = enabled;
}
/**
* Indicates if the channel may be encrypted (but is not required to be).
* Explicitly providing a value in the configuration take precedent.
* If no optional value is set and !isEnabled(), then optional connections are allowed
* if a keystore exists. Without it, it would be impossible to establish the connections.
*
* Return type is Boolean even though it can never be null so that snakeyaml can find it
* @return if the channel may be encrypted
*/
public Boolean isOptional()
{
ensureConfigApplied();
return isOptional;
}
/**
* Sets if encryption should be optional for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating enabled state
* is probably a bad idea.
* @param optional value to set
*/
public void setOptional(boolean optional) {
ensureConfigNotApplied();
this.optional = optional;
}
/**
* Sets accepted TLS protocol for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating protocol state
* is probably a bad idea.
* @param protocol value to set
*/
@VisibleForTesting
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* Sets accepted TLS protocols for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating protocol state
* is probably a bad idea. The function casing is required for snakeyaml to find this setter for the protected field.
* @param accepted_protocols value to set
*/
public void setaccepted_protocols(List<String> accepted_protocols) {
this.accepted_protocols = accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols);
}
/* This list is substituted in configurations that have explicitly specified the original "TLS" default,
* by extracting it from the default "TLS" SSL Context instance
*/
static private final List<String> TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution();
/**
* Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of
* explicit protocols for the previous catchall default of "TLS"
* @return array of protocol names suitable for passing to SslContextBuilder.protocols, or null if the default
*/
public List<String> acceptedProtocols()
{
if (accepted_protocols == null)
{
if (protocol == null)
{
return null;
}
// TLS is accepted by SSLContext.getInstance as a shorthand for give me an engine that
// can speak some of the TLS protocols. It is not supported by SSLEngine.setAcceptedProtocols
// so substitute if the user hasn't provided an accepted protocol configuration
else if (protocol.equalsIgnoreCase("TLS"))
{
return TLS_PROTOCOL_SUBSTITUTION;
}
else // the user was trying to limit to a single specific protocol, so try that
{
return ImmutableList.of(protocol);
}
}
if (protocol != null && !protocol.equalsIgnoreCase("TLS") &&
accepted_protocols.stream().noneMatch(ap -> ap.equalsIgnoreCase(protocol)))
{
// If the user provided a non-generic default protocol, append it to accepted_protocols - they wanted
// it after all.
return ImmutableList.<String>builder().addAll(accepted_protocols).add(protocol).build();
}
else
{
return accepted_protocols;
}
}
public String[] acceptedProtocolsArray()
{
List<String> ap = acceptedProtocols();
return ap == null ? new String[0] : ap.toArray(new String[0]);
}
public String[] cipherSuitesArray()
{
return cipher_suites == null ? new String[0] : cipher_suites.toArray(new String[0]);
}
public TlsEncryptionPolicy tlsEncryptionPolicy()
{
if (isOptional())
{
return TlsEncryptionPolicy.OPTIONAL;
}
else if (isEnabled())
{
return TlsEncryptionPolicy.ENCRYPTED;
}
else
{
return TlsEncryptionPolicy.UNENCRYPTED;
}
}
public EncryptionOptions withKeyStore(String keystore)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withKeyStorePassword(String keystore_password)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withTrustStore(String truststore)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withTrustStorePassword(String truststore_password)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withCipherSuites(List<String> cipher_suites)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withCipherSuites(String ... cipher_suites)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withProtocol(String protocol)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification, enabled, optional).applyConfig();
}
public EncryptionOptions withAlgorithm(String algorithm)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withStoreType(String store_type)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withRequireClientAuth(boolean require_client_auth)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withEnabled(boolean enabled)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withOptional(Boolean optional)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
/**
* The method is being mainly used to cache SslContexts therefore, we only consider
* fields that would make a difference when the TrustStore or KeyStore files are updated
*/
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (o == null || getClass() != o.getClass())
return false;
EncryptionOptions opt = (EncryptionOptions)o;
return enabled == opt.enabled &&
optional == opt.optional &&
require_client_auth == opt.require_client_auth &&
require_endpoint_verification == opt.require_endpoint_verification &&
Objects.equals(keystore, opt.keystore) &&
Objects.equals(keystore_password, opt.keystore_password) &&
Objects.equals(truststore, opt.truststore) &&
Objects.equals(truststore_password, opt.truststore_password) &&
Objects.equals(protocol, opt.protocol) &&
Objects.equals(accepted_protocols, opt.accepted_protocols) &&
Objects.equals(algorithm, opt.algorithm) &&
Objects.equals(store_type, opt.store_type) &&
Objects.equals(cipher_suites, opt.cipher_suites);
}
/**
* The method is being mainly used to cache SslContexts therefore, we only consider
* fields that would make a difference when the TrustStore or KeyStore files are updated
*/
@Override
public int hashCode()
{
int result = 0;
result += 31 * (keystore == null ? 0 : keystore.hashCode());
result += 31 * (keystore_password == null ? 0 : keystore_password.hashCode());
result += 31 * (truststore == null ? 0 : truststore.hashCode());
result += 31 * (truststore_password == null ? 0 : truststore_password.hashCode());
result += 31 * (protocol == null ? 0 : protocol.hashCode());
result += 31 * (accepted_protocols == null ? 0 : accepted_protocols.hashCode());
result += 31 * (algorithm == null ? 0 : algorithm.hashCode());
result += 31 * (store_type == null ? 0 : store_type.hashCode());
result += 31 * (enabled == null ? 0 : Boolean.hashCode(enabled));
result += 31 * (optional == null ? 0 : Boolean.hashCode(optional));
result += 31 * (cipher_suites == null ? 0 : cipher_suites.hashCode());
result += 31 * Boolean.hashCode(require_client_auth);
result += 31 * Boolean.hashCode(require_endpoint_verification);
return result;
}
public static class ServerEncryptionOptions extends EncryptionOptions
{
public enum InternodeEncryption
{
all, none, dc, rack
}
public final InternodeEncryption internode_encryption;
public final boolean enable_legacy_ssl_storage_port;
public ServerEncryptionOptions()
{
this.internode_encryption = InternodeEncryption.none;
this.enable_legacy_ssl_storage_port = false;
}
public ServerEncryptionOptions(String keystore, String keystore_password, String truststore,
String truststore_password, List<String> cipher_suites, String protocol,
List<String> accepted_protocols, String algorithm, String store_type,
boolean require_client_auth, boolean require_endpoint_verification,
Boolean optional, InternodeEncryption internode_encryption,
boolean enable_legacy_ssl_storage_port)
{
super(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
null, optional);
this.internode_encryption = internode_encryption;
this.enable_legacy_ssl_storage_port = enable_legacy_ssl_storage_port;
}
public ServerEncryptionOptions(ServerEncryptionOptions options)
{
super(options);
this.internode_encryption = options.internode_encryption;
this.enable_legacy_ssl_storage_port = options.enable_legacy_ssl_storage_port;
}
@Override
public EncryptionOptions applyConfig()
{
return applyConfigInternal();
}
private ServerEncryptionOptions applyConfigInternal()
{
super.applyConfig();
isEnabled = this.internode_encryption != InternodeEncryption.none;
if (this.enabled != null && this.enabled && !isEnabled)
{
logger.warn("Setting server_encryption_options.enabled has no effect, use internode_encryption");
}
if (require_client_auth && (internode_encryption == InternodeEncryption.rack || internode_encryption == InternodeEncryption.dc))
{
logger.warn("Setting require_client_auth is incompatible with 'rack' and 'dc' internode_encryption values."
+ " It is possible for an internode connection to pretend to be in the same rack/dc by spoofing"
+ " its broadcast address in the handshake and bypass authentication. To ensure that mutual TLS"
+ " authentication is not bypassed, please set internode_encryption to 'all'. Continuing with"
+ " insecure configuration.");
}
// regardless of the optional flag, if the internode encryption is set to rack or dc
// it must be optional so that unencrypted connections within the rack or dc can be established.
isOptional = super.isOptional || internode_encryption == InternodeEncryption.rack || internode_encryption == InternodeEncryption.dc;
return this;
}
public boolean shouldEncrypt(InetAddressAndPort endpoint)
{
IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch();
switch (internode_encryption)
{
case none:
return false; // if nothing needs to be encrypted then return immediately.
case all:
break;
case dc:
if (snitch.getDatacenter(endpoint).equals(snitch.getLocalDatacenter()))
return false;
break;
case rack:
// for rack then check if the DC's are the same.
if (snitch.getRack(endpoint).equals(snitch.getLocalRack())
&& snitch.getDatacenter(endpoint).equals(snitch.getLocalDatacenter()))
return false;
break;
}
return true;
}
/**
* {@link #isOptional} will be set to {@code true} implicitly for {@code internode_encryption}
* values of "dc" and "all". This method returns the explicit, raw value of {@link #optional}
* as set by the user (if set at all).
*/
public boolean isExplicitlyOptional()
{
return optional != null && optional;
}
public ServerEncryptionOptions withKeyStore(String keystore)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withKeyStorePassword(String keystore_password)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withTrustStore(String truststore)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withTrustStorePassword(String truststore_password)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withCipherSuites(List<String> cipher_suites)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withCipherSuites(String ... cipher_suites)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withProtocol(String protocol)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withAlgorithm(String algorithm)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withStoreType(String store_type)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withRequireClientAuth(boolean require_client_auth)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withOptional(boolean optional)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withInternodeEncryption(InternodeEncryption internode_encryption)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
}
}
}