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

PEM format support #303

Merged
merged 12 commits into from
Sep 30, 2022
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Add metric for failed and succeeded repair tasks - Issue #295
* Remove deprecated v1 REST interface
* Migrate to datastax driver-4.14.1 - Issue #269
* Add PEM format support - Issue #300

## Version 3.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.ericsson.bss.cassandra.ecchronos.application.config.TLSConfig;
import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -35,6 +38,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

Expand All @@ -55,17 +59,18 @@ public SSLEngine newSslEngine(EndPoint remoteEndpoint)
{
Context context = getContext();
TLSConfig tlsConfig = context.getTlsConfig();
SSLContext sslContext = context.getSSLContext();
SslContext sslContext = context.getSSLContext();

SSLEngine sslEngine;
if (remoteEndpoint != null)
{
InetSocketAddress socketAddress = (InetSocketAddress) remoteEndpoint.resolve();
sslEngine = sslContext.createSSLEngine(socketAddress.getHostName(), socketAddress.getPort());
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT, socketAddress.getHostName(),
socketAddress.getPort());
}
else
{
sslEngine = sslContext.createSSLEngine();
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT);
}
sslEngine.setUseClientMode(true);

Expand Down Expand Up @@ -118,7 +123,7 @@ public void close() throws Exception
protected static final class Context
{
private final TLSConfig tlsConfig;
private final SSLContext sslContext;
private final SslContext sslContext;

Context(TLSConfig tlsConfig) throws NoSuchAlgorithmException, IOException, UnrecoverableKeyException,
CertificateException, KeyStoreException, KeyManagementException
Expand All @@ -137,22 +142,41 @@ boolean sameConfig(TLSConfig tlsConfig)
return this.tlsConfig.equals(tlsConfig);
}

SSLContext getSSLContext()
SslContext getSSLContext()
{
return sslContext;
}
}

protected static SSLContext createSSLContext(TLSConfig tlsConfig) throws IOException, NoSuchAlgorithmException,
KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException
protected static SslContext createSSLContext(TLSConfig tlsConfig) throws IOException, NoSuchAlgorithmException,
KeyStoreException, CertificateException, UnrecoverableKeyException
{
SSLContext sslContext = SSLContext.getInstance(tlsConfig.getProtocol());
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig);

sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
SslContextBuilder builder = SslContextBuilder.forClient();

return sslContext;
if (tlsConfig.getCertificate().isPresent() &&
tlsConfig.getCertificateKey().isPresent() &&
tlsConfig.getCertificateAuthorities().isPresent())
{
File certificateFile = new File(tlsConfig.getCertificate().get());
File certificateKeyFile = new File(tlsConfig.getCertificateKey().get());
File certificateAuthorityFile = new File(tlsConfig.getCertificateAuthorities().get());

valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
builder.keyManager(certificateFile, certificateKeyFile);
builder.trustManager(certificateAuthorityFile);
}
else
{
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig);
builder.keyManager(keyManagerFactory);
builder.trustManager(trustManagerFactory);
}
if (tlsConfig.getCipherSuites().isPresent())
{
builder.ciphers(Arrays.asList(tlsConfig.getCipherSuites().get()));
}
return builder.protocols(tlsConfig.getProtocols()).build();
}

protected static KeyManagerFactory getKeyManagerFactory(TLSConfig tlsConfig) throws IOException,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public class TLSConfig
private String truststore;
private String truststore_password;

private String certificate;
private String certificate_key;
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
private String certificate_authorities;

private String protocol;
private String algorithm;
private String store_type;
Expand Down Expand Up @@ -85,11 +89,46 @@ public void setTruststore_password(String truststore_password)
this.truststore_password = truststore_password;
}

public Optional<String> getCertificate()
{
return Optional.ofNullable(certificate);
}

public void setCertificate(String certificate)
{
this.certificate = certificate;
}

public Optional<String> getCertificateKey()
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
return Optional.ofNullable(certificate_key);
}

public void setCertificate_key(String certificate_key)
{
this.certificate_key = certificate_key;
}

public Optional<String> getCertificateAuthorities()
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
return Optional.ofNullable(certificate_authorities);
}

public void setCertificate_authorities(String certificate_authorities)
{
this.certificate_authorities = certificate_authorities;
}

public String getProtocol()
{
return protocol;
}

public String[] getProtocols()
{
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
return protocol.split(",");
}

public void setProtocol(String protocol)
{
this.protocol = protocol;
Expand Down Expand Up @@ -159,6 +198,9 @@ public boolean equals(Object o)
Objects.equals(keystore_password, tlsConfig.keystore_password) &&
Objects.equals(truststore, tlsConfig.truststore) &&
Objects.equals(truststore_password, tlsConfig.truststore_password) &&
Objects.equals(certificate, tlsConfig.certificate) &&
Objects.equals(certificate_key, tlsConfig.certificate_key) &&
Objects.equals(certificate_authorities, tlsConfig.certificate_authorities) &&
Objects.equals(protocol, tlsConfig.protocol) &&
Objects.equals(algorithm, tlsConfig.algorithm) &&
Objects.equals(store_type, tlsConfig.store_type) &&
Expand All @@ -168,8 +210,8 @@ public boolean equals(Object o)
@Override
public int hashCode()
{
int result = Objects.hash(enabled, keystore, keystore_password, truststore, truststore_password, protocol,
algorithm, store_type, require_endpoint_verification);
int result = Objects.hash(enabled, keystore, keystore_password, truststore, truststore_password, certificate,
certificate_key, certificate_authorities, protocol, algorithm, store_type, require_endpoint_verification);
result = 31 * result + Arrays.hashCode(cipher_suites);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@


import org.apache.coyote.http11.Http11NioProtocol;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.jsse.PEMFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;

@Component
@EnableScheduling
public class TomcatWebServerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>
Expand All @@ -32,27 +42,90 @@ public class TomcatWebServerCustomizer implements WebServerFactoryCustomizer<Tom
@Value("${server.ssl.enabled:false}")
private Boolean sslIsEnabled;

@Value("${server.ssl.certificate:#{null}}")
private String certificate;

@Value("${server.ssl.certificate-key:#{null}}")
private String certificateKey;

@Value("${server.ssl.certificate-authorities:#{null}}")
private String certificateAuthorities;

@Value("${server.ssl.client-auth:none}")
private String clientAuth;

private SSLHostConfig sslHostConfig;

private static final Logger LOG = LoggerFactory.getLogger(TomcatWebServerCustomizer.class);

@Override
public void customize(TomcatServletWebServerFactory factory)
{
if (sslIsEnabled)
{
if (certificate != null)
{
factory.getSsl().setEnabled(false);
}

factory.addConnectorCustomizers(connector ->
{
http11NioProtocol = (Http11NioProtocol) connector.getProtocolHandler();
if (certificate != null) {
sslHostConfig = getSslHostConfiguration();
http11NioProtocol.addSslHostConfig(sslHostConfig);
http11NioProtocol.setSSLEnabled(true);
}
});
}
}

private SSLHostConfig getSslHostConfiguration()
{
SSLHostConfig sslHostConfig = new SSLHostConfig();
SSLHostConfigCertificate certificateConfig = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.DEFAULT_TYPE);
certificateConfig.setCertificateFile(certificate);
certificateConfig.setCertificateKeyFile(certificateKey);
sslHostConfig.addCertificate(certificateConfig);
sslHostConfig.setTrustStore(getTrustStore());
sslHostConfig.setCertificateVerification("need".equals(clientAuth) ? "true" : "false");
return sslHostConfig;
}

private KeyStore getTrustStore()
{
KeyStore trustStore = null;
try
{
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);

PEMFile certificateBundle = new PEMFile(certificateAuthorities);
for (X509Certificate certificate : certificateBundle.getCertificates())
{
trustStore.setCertificateEntry(certificate.getSerialNumber().toString(), certificate);
}
}
catch(GeneralSecurityException | IOException exception)
{
LOG.warn("Unable to load certificate authorities", exception);
}
return trustStore;
}

/**
* Reload the {@code SSLHostConfig} if SSL is enabled. Doing so should update ssl settings and fetch certificates from Keystores
* Reload the {@code SSLHostConfig} if SSL is enabled. Doing so should update ssl settings and reload certificates
* It reloads them every 60 seconds by default
*/
@Scheduled (initialDelayString = "${server.ssl.refresh-rate-in-ms:60000}", fixedRateString = "${server.ssl.refresh-rate-in-ms:60000}")
public void reloadSslContext()
{
if (sslIsEnabled && http11NioProtocol != null)
{
if (sslHostConfig != null)
{
sslHostConfig.setTrustStore(getTrustStore());
}
http11NioProtocol.reloadSslHostConfigs();
}
}
Expand Down
8 changes: 7 additions & 1 deletion application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ springdoc:
#server:
# ssl:
# enabled: false
# client-auth: <client authentication>
# SSL configuration using certificate stores
# key-store: <path to keystore>
# key-store-password: <password>
# key-store-type: <keystore type>
# key-alias: <key alias>
# key-password: <key password>
# Rate at which certificate are reloaded automatically
# SSL configuration using certificates in PEM format
# certificate: <path to certificate>
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
# certificate-key: <path to certificate private key>
# certificate-authorities: <path to certificate bundle>
# Rate at which certificates are reloaded automatically
# refresh-rate-in-ms: 60000
3 changes: 3 additions & 0 deletions application/src/main/resources/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ cql:
keystore_password: ecchronos
truststore: /path/to/truststore
truststore_password: ecchronos
certificate:
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
certificate_key:
certificate_authorities:
protocol: TLSv1.2
algorithm:
store_type: JKS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public void testDefault() throws Exception
cqlTlsConfig.setKeystore_password("ecchronos");
cqlTlsConfig.setTruststore("/path/to/truststore");
cqlTlsConfig.setTruststore_password("ecchronos");
cqlTlsConfig.setCertificate(null);
cqlTlsConfig.setCertificate_key(null);
cqlTlsConfig.setCertificate_authorities(null);
cqlTlsConfig.setProtocol("TLSv1.2");
cqlTlsConfig.setAlgorithm(null);
cqlTlsConfig.setStore_type("JKS");
Expand Down Expand Up @@ -102,4 +105,29 @@ public void testEnabled() throws Exception
assertThat(config.getCql().getTls()).isEqualTo(cqlTlsConfig);
assertThat(config.getJmx().getTls()).isEqualTo(jmxTlsConfig);
}

@Test
public void testEnabledWithCertificate() throws Exception
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
File file = new File(classLoader.getResource("enabled_certificate_security.yml").getFile());

ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());

Security config = objectMapper.readValue(file, Security.class);

Credentials expectedCqlCredentials = new Credentials(true, "cqluser", "cqlpassword");

TLSConfig cqlTlsConfig = new TLSConfig();
cqlTlsConfig.setEnabled(true);
cqlTlsConfig.setCertificate("/path/to/cql/certificate");
cqlTlsConfig.setCertificate_key("/path/to/cql/certificate_key");
cqlTlsConfig.setCertificate_authorities("/path/to/cql/certificate_authorities");
cqlTlsConfig.setProtocol("TLSv1.2");
cqlTlsConfig.setCipher_suites("VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2");
cqlTlsConfig.setRequire_endpoint_verification(true);

assertThat(config.getCql().getCredentials()).isEqualTo(expectedCqlCredentials);
assertThat(config.getCql().getTls()).isEqualTo(cqlTlsConfig);
}
}
Loading