Skip to content

Commit 40bfde4

Browse files
committed
Add support for SSL/TLS
Closes #51
1 parent 3ddb30f commit 40bfde4

File tree

25 files changed

+1374
-11
lines changed

25 files changed

+1374
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ dependency-reduced-pom.xml
1010
*.releaseBackup
1111
release.properties
1212

13+
*.srl
1314

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,17 @@ The available command line options may be seen by passing `-h`/`--help`:
110110
[--jmx-service-url=URL] [--jmx-user=NAME]
111111
[--keyspace-metrics=FILTER] [--node-metrics=FILTER]
112112
[--table-metrics=FILTER]
113-
[--exclude-keyspaces=<excludedKeyspaces>]... [-g=LABEL
113+
[--exclude-keyspaces=<excludedKeyspaces>]...
114+
[--ssl=MODE]
115+
[--ssl-client-authentication=CLIENT-AUTHENTICATION]
116+
[--ssl-implementation=IMPLEMENTATION]
117+
[--ssl-reload-interval=SECONDS]
118+
[--ssl-server-certificate=SERVER-CERTIFICATE]
119+
[--ssl-server-key=SERVER-KEY]
120+
[--ssl-server-key-password=SERVER-KEY-PASSWORD]
121+
[--ssl-trusted-certificate=TRUSTED-CERTIFICATE]
122+
[--ssl-ciphers=CIPHER[,CIPHER...]]...
123+
[--ssl-protocols=PROTOCOL[,PROTOCOL...]]... [-g=LABEL
114124
[,LABEL...]]... [-l=[ADDRESS][:PORT]]... [-t=LABEL[,
115125
LABEL...]]... [-e=EXCLUSION...]...
116126
-g, --global-labels=LABEL[,LABEL...]
@@ -180,6 +190,49 @@ The available command line options may be seen by passing `-h`/`--help`:
180190
or PORT will be interpreted as a decimal IPv4 address.
181191
This option may be specified more than once to listen
182192
on multiple addresses. Defaults to '0.0.0.0:9500'
193+
--ssl=MODE Enable or disable secured communication with SSL. Valid
194+
modes: DISABLE, ENABLE, OPTIONAL. Optional support
195+
requires Netty version 4.0.45 or later. Defaults to
196+
DISABLE.
197+
--ssl-implementation=IMPLEMENTATION
198+
SSL implementation to use for secure communication.
199+
OpenSSL requires platform specific libraries. Valid
200+
implementations: OPENSSL, JDK, DISCOVER. Defaults to
201+
DISCOVER which will use OpenSSL if required libraries
202+
are discoverable.
203+
--ssl-ciphers=CIPHER[,CIPHER...]
204+
A comma-separated list of SSL cipher suites to enable,
205+
in the order of preference. Defaults to system
206+
settings.
207+
--ssl-protocols=PROTOCOL[,PROTOCOL...]
208+
A comma-separated list of TLS protocol versions to
209+
enable. Defaults to system settings.
210+
--ssl-reload-interval=SECONDS
211+
Interval in seconds by which keys and certificates will
212+
be reloaded. Defaults to 0 which will disable run-time
213+
reload of certificates.
214+
--ssl-server-key=SERVER-KEY
215+
Path to the private key file for the SSL server. Must be
216+
provided together with a server-certificate. The file
217+
should contain a PKCS#8 private key in PEM format.
218+
--ssl-server-key-password=SERVER-KEY-PASSWORD
219+
Path to the private key password file for the SSL
220+
server. This is only required if the server-key is
221+
password protected. The file should contain a clear
222+
text password for the server-key.
223+
--ssl-server-certificate=SERVER-CERTIFICATE
224+
Path to the certificate chain file for the SSL server.
225+
Must be provided together with a server-key. The file
226+
should contain an X.509 certificate chain in PEM
227+
format.
228+
--ssl-client-authentication=CLIENT-AUTHENTICATION
229+
Set SSL client authentication mode. Valid options: NONE,
230+
OPTIONAL, REQUIRE, VALIDATE. Defaults to NONE.
231+
--ssl-trusted-certificate=TRUSTED-CERTIFICATE
232+
Path to trusted certificates for verifying the remote
233+
endpoint's certificate. The file should contain an X.
234+
509 certificate collection in PEM format. Defaults to
235+
the system setting.
183236
--family-help=VALUE Include or exclude metric family help in the exposition
184237
format. AUTOMATIC excludes help strings when the user
185238
agent is Prometheus and includes them for all other

agent/src/main/java/com/zegelin/cassandra/exporter/Agent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public Void call() throws Exception {
2828

2929
final MBeanServerInterceptorHarvester harvester = new MBeanServerInterceptorHarvester(harvesterOptions);
3030

31-
final Server server = Server.start(httpServerOptions.listenAddresses, harvester, httpServerOptions.helpExposition);
31+
final Server server = Server.start(harvester, httpServerOptions);
3232

3333
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
3434
try {

bin/generate_cert_for_test.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
RESOURCE_PATH="common/src/test/resources"
4+
5+
# Generate a private key and store it both unecrypted and encrypted (password protected)
6+
# Create a self-signed certificate for the key
7+
mkdir -p ${RESOURCE_PATH}/cert
8+
rm -f ${RESOURCE_PATH}/cert/*
9+
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM -out ${RESOURCE_PATH}/cert/key.pem
10+
echo -n "password" > ${RESOURCE_PATH}/cert/protected-key.pass
11+
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
12+
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

common/pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
</properties>
1919

2020
<dependencies>
21+
<!-- Bring in improved tcnative detection in netty for utests-->
22+
<dependency>
23+
<groupId>io.netty</groupId>
24+
<artifactId>netty-all</artifactId>
25+
<version>4.0.51.Final</version>
26+
<scope>provided</scope>
27+
</dependency>
28+
2129
<dependency>
2230
<groupId>org.apache.cassandra</groupId>
2331
<artifactId>cassandra-all</artifactId>
@@ -46,5 +54,19 @@
4654
<scope>test</scope>
4755
</dependency>
4856

57+
<dependency>
58+
<groupId>org.assertj</groupId>
59+
<artifactId>assertj-core</artifactId>
60+
<version>3.12.0</version>
61+
<scope>test</scope>
62+
</dependency>
63+
64+
<dependency>
65+
<groupId>io.netty</groupId>
66+
<artifactId>netty-tcnative-boringssl-static</artifactId>
67+
<version>2.0.28.Final</version>
68+
<scope>test</scope>
69+
</dependency>
70+
4971
</dependencies>
5072
</project>

common/src/main/java/com/zegelin/cassandra/exporter/cli/HttpServerOptions.java

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.zegelin.cassandra.exporter.cli;
22

3-
import com.zegelin.picocli.InetSocketAddressTypeConverter;
43
import com.zegelin.cassandra.exporter.netty.HttpHandler;
4+
import com.zegelin.cassandra.exporter.netty.ssl.ClientAuthentication;
5+
import com.zegelin.cassandra.exporter.netty.ssl.SslImplementation;
6+
import com.zegelin.cassandra.exporter.netty.ssl.SslMode;
7+
import com.zegelin.picocli.InetSocketAddressTypeConverter;
58
import picocli.CommandLine.Option;
69

10+
import java.io.File;
711
import java.net.InetSocketAddress;
812
import java.util.List;
13+
import java.util.Set;
914

1015
public class HttpServerOptions {
1116

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

41+
@Option(names = "--ssl",
42+
paramLabel = "MODE",
43+
defaultValue = "DISABLE",
44+
description = "Enable or disable secured communication with SSL. " +
45+
"Valid modes: ${COMPLETION-CANDIDATES}. " +
46+
"Optional support requires Netty version 4.0.45 or later. " +
47+
"Defaults to ${DEFAULT-VALUE}."
48+
)
49+
public SslMode sslMode = SslMode.DISABLE;
50+
51+
@Option(names = "--ssl-implementation",
52+
paramLabel = "IMPLEMENTATION",
53+
defaultValue = "DISCOVER",
54+
description = "SSL implementation to use for secure communication. " +
55+
"OpenSSL requires platform specific libraries. " +
56+
"Valid implementations: ${COMPLETION-CANDIDATES}. " +
57+
"Defaults to ${DEFAULT-VALUE} which will use OpenSSL if required libraries are discoverable."
58+
)
59+
public SslImplementation sslImplementation = SslImplementation.DISCOVER;
60+
61+
@Option(names = "--ssl-ciphers",
62+
paramLabel = "CIPHER",
63+
split = ",",
64+
description = "A comma-separated list of SSL cipher suites to enable, in the order of preference. " +
65+
"Defaults to system settings."
66+
)
67+
public List<String> sslCiphers;
68+
69+
@Option(names = "--ssl-protocols",
70+
paramLabel = "PROTOCOL",
71+
split = ",",
72+
description = "A comma-separated list of TLS protocol versions to enable. " +
73+
"Defaults to system settings."
74+
)
75+
public Set<String> sslProtocols;
76+
77+
@Option(names = "--ssl-reload-interval",
78+
paramLabel = "SECONDS",
79+
defaultValue = "0",
80+
description = "Interval in seconds by which keys and certificates will be reloaded. " +
81+
"Defaults to ${DEFAULT-VALUE} which will disable run-time reload of certificates."
82+
)
83+
public long sslReloadIntervalInSeconds = 0L;
84+
85+
@Option(names = "--ssl-server-key",
86+
paramLabel = "SERVER-KEY",
87+
description = "Path to the private key file for the SSL server. " +
88+
"Must be provided together with a server-certificate. " +
89+
"The file should contain a PKCS#8 private key in PEM format."
90+
)
91+
public File sslServerKeyFile;
92+
93+
@Option(names = "--ssl-server-key-password",
94+
paramLabel = "SERVER-KEY-PASSWORD",
95+
description = "Path to the private key password file for the SSL server. " +
96+
"This is only required if the server-key is password protected. " +
97+
"The file should contain a clear text password for the server-key."
98+
)
99+
public File sslServerKeyPasswordFile;
100+
101+
@Option(names = "--ssl-server-certificate",
102+
paramLabel = "SERVER-CERTIFICATE",
103+
description = "Path to the certificate chain file for the SSL server. " +
104+
"Must be provided together with a server-key. " +
105+
"The file should contain an X.509 certificate chain in PEM format."
106+
)
107+
public File sslServerCertificateFile;
108+
109+
@Option(names = "--ssl-client-authentication",
110+
paramLabel = "CLIENT-AUTHENTICATION",
111+
defaultValue = "NONE",
112+
description = "Set SSL client authentication mode. " +
113+
"Valid options: ${COMPLETION-CANDIDATES}. " +
114+
"Defaults to ${DEFAULT-VALUE}."
115+
)
116+
public ClientAuthentication sslClientAuthentication = ClientAuthentication.NONE;
117+
118+
@Option(names = "--ssl-trusted-certificate",
119+
paramLabel = "TRUSTED-CERTIFICATE",
120+
description = "Path to trusted certificates for verifying the remote endpoint's certificate. " +
121+
"The file should contain an X.509 certificate collection in PEM format. " +
122+
"Defaults to the system setting."
123+
)
124+
public File sslTrustedCertificateFile;
125+
36126
@Option(names = {"--family-help"},
37127
paramLabel = "VALUE",
38128
defaultValue = "AUTOMATIC",

common/src/main/java/com/zegelin/cassandra/exporter/netty/Server.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.google.common.collect.ImmutableList;
55
import com.google.common.util.concurrent.ThreadFactoryBuilder;
66
import com.zegelin.cassandra.exporter.Harvester;
7+
import com.zegelin.cassandra.exporter.cli.HttpServerOptions;
8+
import com.zegelin.cassandra.exporter.netty.ssl.SslSupport;
79
import io.netty.bootstrap.ServerBootstrap;
810
import io.netty.buffer.PooledByteBufAllocator;
911
import io.netty.channel.Channel;
@@ -41,10 +43,12 @@ public Server(final List<Channel> channels, final EventLoopGroup eventLoopGroup)
4143
public static class ChildInitializer extends ChannelInitializer<SocketChannel> {
4244
private final Harvester harvester;
4345
private final HttpHandler.HelpExposition helpExposition;
46+
private final SslSupport sslSupport;
4447

45-
ChildInitializer(final Harvester harvester, final HttpHandler.HelpExposition helpExposition) {
48+
ChildInitializer(final Harvester harvester, final HttpServerOptions httpServerOptions) {
4649
this.harvester = harvester;
47-
this.helpExposition = helpExposition;
50+
this.helpExposition = httpServerOptions.helpExposition;
51+
this.sslSupport = new SslSupport(httpServerOptions);
4852
}
4953

5054
@Override
@@ -56,12 +60,12 @@ public void initChannel(final SocketChannel ch) {
5660
.addLast(new ChunkedWriteHandler())
5761
.addLast(new HttpHandler(harvester, helpExposition))
5862
.addLast(new SuppressingExceptionHandler());
63+
64+
sslSupport.maybeAddHandler(ch);
5965
}
6066
}
6167

62-
public static Server start(final List<InetSocketAddress> listenAddresses,
63-
final Harvester harvester,
64-
final HttpHandler.HelpExposition helpExposition) throws InterruptedException {
68+
public static Server start(final Harvester harvester, final HttpServerOptions httpServerOptions) throws InterruptedException {
6569

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

8084
final List<Channel> serverChannels;
8185
{
8286
final ImmutableList.Builder<Channel> builder = ImmutableList.builder();
8387

84-
for (final InetSocketAddress listenAddress : listenAddresses) {
88+
for (final InetSocketAddress listenAddress : httpServerOptions.listenAddresses) {
8589
builder.add(bootstrap.bind(listenAddress).sync().channel());
8690
}
8791

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.zegelin.cassandra.exporter.netty.ssl;
2+
3+
import io.netty.handler.ssl.ClientAuth;
4+
5+
public enum ClientAuthentication {
6+
NONE(ClientAuth.NONE, false),
7+
OPTIONAL(ClientAuth.OPTIONAL, false),
8+
REQUIRE(ClientAuth.REQUIRE, false),
9+
VALIDATE(ClientAuth.REQUIRE, true);
10+
11+
private final ClientAuth clientAuth;
12+
private final boolean hostnameValidation;
13+
14+
ClientAuthentication(final ClientAuth clientAuth, final boolean hostnameValidation) {
15+
this.clientAuth = clientAuth;
16+
this.hostnameValidation = hostnameValidation;
17+
}
18+
19+
ClientAuth getClientAuth() {
20+
return clientAuth;
21+
}
22+
23+
boolean getHostnameValidation() {
24+
return hostnameValidation;
25+
}
26+
}

0 commit comments

Comments
 (0)