Navigation Menu

Skip to content

Commit

Permalink
Add support for SSL/TLS
Browse files Browse the repository at this point in the history
Closes #52
  • Loading branch information
zegelin committed Feb 21, 2020
1 parent d802552 commit 7bfb0a5
Show file tree
Hide file tree
Showing 25 changed files with 1,374 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -10,4 +10,5 @@ dependency-reduced-pom.xml
*.releaseBackup
release.properties

*.srl

55 changes: 54 additions & 1 deletion README.md
Expand Up @@ -110,7 +110,17 @@ The available command line options may be seen by passing `-h`/`--help`:
[--jmx-service-url=URL] [--jmx-user=NAME]
[--keyspace-metrics=FILTER] [--node-metrics=FILTER]
[--table-metrics=FILTER]
[--exclude-keyspaces=<excludedKeyspaces>]... [-g=LABEL
[--exclude-keyspaces=<excludedKeyspaces>]...
[--ssl=MODE]
[--ssl-client-authentication=CLIENT-AUTHENTICATION]
[--ssl-implementation=IMPLEMENTATION]
[--ssl-reload-interval=SECONDS]
[--ssl-server-certificate=SERVER-CERTIFICATE]
[--ssl-server-key=SERVER-KEY]
[--ssl-server-key-password=SERVER-KEY-PASSWORD]
[--ssl-trusted-certificate=TRUSTED-CERTIFICATE]
[--ssl-ciphers=CIPHER[,CIPHER...]]...
[--ssl-protocols=PROTOCOL[,PROTOCOL...]]... [-g=LABEL
[,LABEL...]]... [-l=[ADDRESS][:PORT]]... [-t=LABEL[,
LABEL...]]... [-e=EXCLUSION...]...
-g, --global-labels=LABEL[,LABEL...]
Expand Down Expand Up @@ -180,6 +190,49 @@ The available command line options may be seen by passing `-h`/`--help`:
or PORT will be interpreted as a decimal IPv4 address.
This option may be specified more than once to listen
on multiple addresses. Defaults to '0.0.0.0:9500'
--ssl=MODE Enable or disable secured communication with SSL. Valid
modes: DISABLE, ENABLE, OPTIONAL. Optional support
requires Netty version 4.0.45 or later. Defaults to
DISABLE.
--ssl-implementation=IMPLEMENTATION
SSL implementation to use for secure communication.
OpenSSL requires platform specific libraries. Valid
implementations: OPENSSL, JDK, DISCOVER. Defaults to
DISCOVER which will use OpenSSL if required libraries
are discoverable.
--ssl-ciphers=CIPHER[,CIPHER...]
A comma-separated list of SSL cipher suites to enable,
in the order of preference. Defaults to system
settings.
--ssl-protocols=PROTOCOL[,PROTOCOL...]
A comma-separated list of TLS protocol versions to
enable. Defaults to system settings.
--ssl-reload-interval=SECONDS
Interval in seconds by which keys and certificates will
be reloaded. Defaults to 0 which will disable run-time
reload of certificates.
--ssl-server-key=SERVER-KEY
Path to the private key file for the SSL server. Must be
provided together with a server-certificate. The file
should contain a PKCS#8 private key in PEM format.
--ssl-server-key-password=SERVER-KEY-PASSWORD
Path to the private key password file for the SSL
server. This is only required if the server-key is
password protected. The file should contain a clear
text password for the server-key.
--ssl-server-certificate=SERVER-CERTIFICATE
Path to the certificate chain file for the SSL server.
Must be provided together with a server-key. The file
should contain an X.509 certificate chain in PEM
format.
--ssl-client-authentication=CLIENT-AUTHENTICATION
Set SSL client authentication mode. Valid options: NONE,
OPTIONAL, REQUIRE, VALIDATE. Defaults to NONE.
--ssl-trusted-certificate=TRUSTED-CERTIFICATE
Path to trusted certificates for verifying the remote
endpoint's certificate. The file should contain an X.
509 certificate collection in PEM format. Defaults to
the system setting.
--family-help=VALUE Include or exclude metric family help in the exposition
format. AUTOMATIC excludes help strings when the user
agent is Prometheus and includes them for all other
Expand Down
Expand Up @@ -28,7 +28,7 @@ public Void call() throws Exception {

final MBeanServerInterceptorHarvester harvester = new MBeanServerInterceptorHarvester(harvesterOptions);

final Server server = Server.start(httpServerOptions.listenAddresses, harvester, httpServerOptions.helpExposition);
final Server server = Server.start(harvester, httpServerOptions);

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
Expand Down
12 changes: 12 additions & 0 deletions bin/generate_cert_for_test.sh
@@ -0,0 +1,12 @@
#!/bin/bash

RESOURCE_PATH="common/src/test/resources"

# Generate a private key and store it both unecrypted and encrypted (password protected)
# Create a self-signed certificate for the key
mkdir -p ${RESOURCE_PATH}/cert
rm -f ${RESOURCE_PATH}/cert/*
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM -out ${RESOURCE_PATH}/cert/key.pem
echo -n "password" > ${RESOURCE_PATH}/cert/protected-key.pass
openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in ${RESOURCE_PATH}/cert/key.pem -out ${RESOURCE_PATH}/cert/protected-key.pem -passout file:${RESOURCE_PATH}/cert/protected-key.pass
openssl req -x509 -new -key ${RESOURCE_PATH}/cert/key.pem -sha256 -days 10000 -out ${RESOURCE_PATH}/cert/cert.pem -subj '/CN=localhost/O=Example Company/C=SE' -nodes
22 changes: 22 additions & 0 deletions common/pom.xml
Expand Up @@ -18,6 +18,14 @@
</properties>

<dependencies>
<!-- Bring in improved tcnative detection in netty for utests-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.51.Final</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
Expand Down Expand Up @@ -46,5 +54,19 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.12.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>2.0.28.Final</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
@@ -1,11 +1,16 @@
package com.zegelin.cassandra.exporter.cli;

import com.zegelin.picocli.InetSocketAddressTypeConverter;
import com.zegelin.cassandra.exporter.netty.HttpHandler;
import com.zegelin.cassandra.exporter.netty.ssl.ClientAuthentication;
import com.zegelin.cassandra.exporter.netty.ssl.SslImplementation;
import com.zegelin.cassandra.exporter.netty.ssl.SslMode;
import com.zegelin.picocli.InetSocketAddressTypeConverter;
import picocli.CommandLine.Option;

import java.io.File;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Set;

public class HttpServerOptions {

Expand Down Expand Up @@ -33,6 +38,91 @@ protected int defaultPort() {
"Defaults to '${DEFAULT-VALUE}'")
public List<InetSocketAddress> listenAddresses;

@Option(names = "--ssl",
paramLabel = "MODE",
defaultValue = "DISABLE",
description = "Enable or disable secured communication with SSL. " +
"Valid modes: ${COMPLETION-CANDIDATES}. " +
"Optional support requires Netty version 4.0.45 or later. " +
"Defaults to ${DEFAULT-VALUE}."
)
public SslMode sslMode = SslMode.DISABLE;

@Option(names = "--ssl-implementation",
paramLabel = "IMPLEMENTATION",
defaultValue = "DISCOVER",
description = "SSL implementation to use for secure communication. " +
"OpenSSL requires platform specific libraries. " +
"Valid implementations: ${COMPLETION-CANDIDATES}. " +
"Defaults to ${DEFAULT-VALUE} which will use OpenSSL if required libraries are discoverable."
)
public SslImplementation sslImplementation = SslImplementation.DISCOVER;

@Option(names = "--ssl-ciphers",
paramLabel = "CIPHER",
split = ",",
description = "A comma-separated list of SSL cipher suites to enable, in the order of preference. " +
"Defaults to system settings."
)
public List<String> sslCiphers;

@Option(names = "--ssl-protocols",
paramLabel = "PROTOCOL",
split = ",",
description = "A comma-separated list of TLS protocol versions to enable. " +
"Defaults to system settings."
)
public Set<String> sslProtocols;

@Option(names = "--ssl-reload-interval",
paramLabel = "SECONDS",
defaultValue = "0",
description = "Interval in seconds by which keys and certificates will be reloaded. " +
"Defaults to ${DEFAULT-VALUE} which will disable run-time reload of certificates."
)
public long sslReloadIntervalInSeconds = 0L;

@Option(names = "--ssl-server-key",
paramLabel = "SERVER-KEY",
description = "Path to the private key file for the SSL server. " +
"Must be provided together with a server-certificate. " +
"The file should contain a PKCS#8 private key in PEM format."
)
public File sslServerKeyFile;

@Option(names = "--ssl-server-key-password",
paramLabel = "SERVER-KEY-PASSWORD",
description = "Path to the private key password file for the SSL server. " +
"This is only required if the server-key is password protected. " +
"The file should contain a clear text password for the server-key."
)
public File sslServerKeyPasswordFile;

@Option(names = "--ssl-server-certificate",
paramLabel = "SERVER-CERTIFICATE",
description = "Path to the certificate chain file for the SSL server. " +
"Must be provided together with a server-key. " +
"The file should contain an X.509 certificate chain in PEM format."
)
public File sslServerCertificateFile;

@Option(names = "--ssl-client-authentication",
paramLabel = "CLIENT-AUTHENTICATION",
defaultValue = "NONE",
description = "Set SSL client authentication mode. " +
"Valid options: ${COMPLETION-CANDIDATES}. " +
"Defaults to ${DEFAULT-VALUE}."
)
public ClientAuthentication sslClientAuthentication = ClientAuthentication.NONE;

@Option(names = "--ssl-trusted-certificate",
paramLabel = "TRUSTED-CERTIFICATE",
description = "Path to trusted certificates for verifying the remote endpoint's certificate. " +
"The file should contain an X.509 certificate collection in PEM format. " +
"Defaults to the system setting."
)
public File sslTrustedCertificateFile;

@Option(names = {"--family-help"},
paramLabel = "VALUE",
defaultValue = "AUTOMATIC",
Expand Down
Expand Up @@ -4,6 +4,8 @@
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.zegelin.cassandra.exporter.Harvester;
import com.zegelin.cassandra.exporter.cli.HttpServerOptions;
import com.zegelin.cassandra.exporter.netty.ssl.SslSupport;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
Expand Down Expand Up @@ -41,10 +43,12 @@ public Server(final List<Channel> channels, final EventLoopGroup eventLoopGroup)
public static class ChildInitializer extends ChannelInitializer<SocketChannel> {
private final Harvester harvester;
private final HttpHandler.HelpExposition helpExposition;
private final SslSupport sslSupport;

ChildInitializer(final Harvester harvester, final HttpHandler.HelpExposition helpExposition) {
ChildInitializer(final Harvester harvester, final HttpServerOptions httpServerOptions) {
this.harvester = harvester;
this.helpExposition = helpExposition;
this.helpExposition = httpServerOptions.helpExposition;
this.sslSupport = new SslSupport(httpServerOptions);
}

@Override
Expand All @@ -56,12 +60,12 @@ public void initChannel(final SocketChannel ch) {
.addLast(new ChunkedWriteHandler())
.addLast(new HttpHandler(harvester, helpExposition))
.addLast(new SuppressingExceptionHandler());

sslSupport.maybeAddHandler(ch);
}
}

public static Server start(final List<InetSocketAddress> listenAddresses,
final Harvester harvester,
final HttpHandler.HelpExposition helpExposition) throws InterruptedException {
public static Server start(final Harvester harvester, final HttpServerOptions httpServerOptions) throws InterruptedException {

final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setDaemon(true)
Expand All @@ -75,13 +79,13 @@ public static Server start(final List<InetSocketAddress> listenAddresses,
bootstrap.group(eventLoopGroup)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.channel(NioServerSocketChannel.class)
.childHandler(new ChildInitializer(harvester, helpExposition));
.childHandler(new ChildInitializer(harvester, httpServerOptions));

final List<Channel> serverChannels;
{
final ImmutableList.Builder<Channel> builder = ImmutableList.builder();

for (final InetSocketAddress listenAddress : listenAddresses) {
for (final InetSocketAddress listenAddress : httpServerOptions.listenAddresses) {
builder.add(bootstrap.bind(listenAddress).sync().channel());
}

Expand Down
@@ -0,0 +1,26 @@
package com.zegelin.cassandra.exporter.netty.ssl;

import io.netty.handler.ssl.ClientAuth;

public enum ClientAuthentication {
NONE(ClientAuth.NONE, false),
OPTIONAL(ClientAuth.OPTIONAL, false),
REQUIRE(ClientAuth.REQUIRE, false),
VALIDATE(ClientAuth.REQUIRE, true);

private final ClientAuth clientAuth;
private final boolean hostnameValidation;

ClientAuthentication(final ClientAuth clientAuth, final boolean hostnameValidation) {
this.clientAuth = clientAuth;
this.hostnameValidation = hostnameValidation;
}

ClientAuth getClientAuth() {
return clientAuth;
}

boolean getHostnameValidation() {
return hostnameValidation;
}
}

0 comments on commit 7bfb0a5

Please sign in to comment.