Skip to content

Commit

Permalink
Add TLS socket logging appender
Browse files Browse the repository at this point in the history
Add support for sending logs to a server that requires a secure means
of communication via SSL/TLS. This change adds a new logging factory
which produces a new socket factory for creating secure client SSL/TLS
sockets.

* The default configuration allows to communicate with a server with
a certificate signed by a CA. In that case the administrator only need
to change the port and the type of the appender.

* If the server is deployed in a internal network with a self-signed
certificate, the factory allows to specify a path to a trust store
with the server certificate.

* If the server requires the client to authenticate itself, the factory
allows to specify a path to a key store with a client certificate
that will be sent to the server during negotiation.

* The factory allows to configure allowed cipher suites and SSL/TLS
protocols.
  • Loading branch information
arteam committed Mar 26, 2018
1 parent 5c5aba4 commit 777dba2
Show file tree
Hide file tree
Showing 9 changed files with 661 additions and 53 deletions.

Large diffs are not rendered by default.

Expand Up @@ -3,3 +3,4 @@ io.dropwizard.logging.FileAppenderFactory
io.dropwizard.logging.SyslogAppenderFactory io.dropwizard.logging.SyslogAppenderFactory
io.dropwizard.logging.TcpSocketAppenderFactory io.dropwizard.logging.TcpSocketAppenderFactory
io.dropwizard.logging.UdpSocketAppenderFactory io.dropwizard.logging.UdpSocketAppenderFactory
io.dropwizard.logging.TlsSocketAppenderFactory
@@ -0,0 +1,83 @@
package io.dropwizard.logging;

import org.junit.rules.ExternalResource;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;

import static org.assertj.core.api.Assertions.assertThat;

class TcpServer extends ExternalResource {

private final Thread thread;
private final ServerSocket serverSocket;
private final int messageCount = 100;
private final CountDownLatch latch = new CountDownLatch(messageCount);

TcpServer(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
Socket socket;
try {
socket = serverSocket.accept();
} catch (SocketException e) {
break;
} catch (IOException e) {
e.printStackTrace();
continue;
}
new Thread(() -> readAndVerifyData(socket)).start();
}
});
}

ServerSocket getServerSocket() {
return serverSocket;
}

int getMessageCount() {
return messageCount;
}

CountDownLatch getLatch() {
return latch;
}

@Override
protected void before() throws Throwable {
thread.start();
}

@Override
protected void after() {
thread.interrupt();
try {
serverSocket.close();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}

private void readAndVerifyData(Socket socket) {
try (Socket s = socket; BufferedReader reader = new BufferedReader(new InputStreamReader(
s.getInputStream(), StandardCharsets.UTF_8))) {
for (int i = 0; i < messageCount; i++) {
String line = reader.readLine();
if (line == null) {
break;
}
assertThat(line).startsWith("INFO").contains("com.example.app: Application log " + i);
latch.countDown();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Expand Up @@ -9,34 +9,25 @@
import io.dropwizard.util.Duration; import io.dropwizard.util.Duration;
import io.dropwizard.util.Size; import io.dropwizard.util.Size;
import io.dropwizard.validation.BaseValidator; import io.dropwizard.validation.BaseValidator;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;


import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;


import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;


public class TcpSocketAppenderFactoryTest { public class TcpSocketAppenderFactoryTest {
private static final int TCP_PORT = 24562; private static final int TCP_PORT = 24562;


private Thread thread; @Rule
private ServerSocket ss; public TcpServer tcpServer = new TcpServer(createServerSocket());

private int messageCount = 100;
private CountDownLatch latch = new CountDownLatch(messageCount);


private ObjectMapper objectMapper = Jackson.newObjectMapper(); private ObjectMapper objectMapper = Jackson.newObjectMapper();
private YamlConfigurationFactory<DefaultLoggingFactory> yamlConfigurationFactory = new YamlConfigurationFactory<>( private YamlConfigurationFactory<DefaultLoggingFactory> yamlConfigurationFactory = new YamlConfigurationFactory<>(
Expand All @@ -45,47 +36,16 @@ public class TcpSocketAppenderFactoryTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
objectMapper.getSubtypeResolver().registerSubtypes(TcpSocketAppenderFactory.class); objectMapper.getSubtypeResolver().registerSubtypes(TcpSocketAppenderFactory.class);
ss = new ServerSocket(TCP_PORT);
thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
Socket socket;
try {
socket = ss.accept();
} catch (SocketException e) {
break;
} catch (IOException e) {
e.printStackTrace();
continue;
}
new Thread(() -> readAndVerifyData(socket)).start();
}
});
thread.start();
} }


private void readAndVerifyData(Socket socket) { private ServerSocket createServerSocket() {
try (Socket s = socket; BufferedReader reader = new BufferedReader(new InputStreamReader( try {
s.getInputStream(), StandardCharsets.UTF_8))) { return new ServerSocket(TCP_PORT);
for (int i = 0; i < messageCount; i++) {
String line = reader.readLine();
if (line == null) {
break;
}
assertThat(line).startsWith("INFO").contains("com.example.app: Application log " + i);
latch.countDown();
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); throw new IllegalStateException(e);
} }
} }



@After
public void tearDown() throws Exception {
thread.interrupt();
ss.close();
}

private static File resourcePath(String path) throws URISyntaxException { private static File resourcePath(String path) throws URISyntaxException {
return new File(Resources.getResource(path).toURI()); return new File(Resources.getResource(path).toURI());
} }
Expand All @@ -109,12 +69,12 @@ public void testTestTcpLogging() throws Exception {
loggingFactory.configure(new MetricRegistry(), "tcp-test"); loggingFactory.configure(new MetricRegistry(), "tcp-test");


Logger logger = LoggerFactory.getLogger("com.example.app"); Logger logger = LoggerFactory.getLogger("com.example.app");
for (int i = 0; i < messageCount; i++) { for (int i = 0; i < tcpServer.getMessageCount(); i++) {
logger.info("Application log {}", i); logger.info("Application log {}", i);
} }


latch.await(5, TimeUnit.SECONDS); tcpServer.getLatch().await(5, TimeUnit.SECONDS);
assertThat(latch.getCount()).isEqualTo(0); assertThat(tcpServer.getLatch().getCount()).isEqualTo(0);
loggingFactory.reset(); loggingFactory.reset();
} }


Expand All @@ -125,13 +85,13 @@ public void testBufferingTcpLogging() throws Exception {
loggingFactory.configure(new MetricRegistry(), "tcp-test"); loggingFactory.configure(new MetricRegistry(), "tcp-test");


Logger logger = LoggerFactory.getLogger("com.example.app"); Logger logger = LoggerFactory.getLogger("com.example.app");
for (int i = 0; i < messageCount; i++) { for (int i = 0; i < tcpServer.getMessageCount(); i++) {
logger.info("Application log {}", i); logger.info("Application log {}", i);
} }
// We have to flush the buffer manually // We have to flush the buffer manually
loggingFactory.reset(); loggingFactory.reset();


latch.await(5, TimeUnit.SECONDS); tcpServer.getLatch().await(5, TimeUnit.SECONDS);
assertThat(latch.getCount()).isEqualTo(0); assertThat(tcpServer.getLatch().getCount()).isEqualTo(0);
} }
} }
@@ -0,0 +1,108 @@
package io.dropwizard.logging;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import io.dropwizard.configuration.ResourceConfigurationSourceProvider;
import io.dropwizard.configuration.SubstitutingSourceProvider;
import io.dropwizard.configuration.YamlConfigurationFactory;
import io.dropwizard.jackson.Jackson;
import io.dropwizard.validation.BaseValidator;
import org.apache.commons.text.StrSubstitutor;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

public class TlsSocketAppenderFactoryTest {

@Rule
public TcpServer tcpServer = new TcpServer(createServerSocket());

private ObjectMapper objectMapper = Jackson.newObjectMapper();
private YamlConfigurationFactory<DefaultLoggingFactory> yamlConfigurationFactory = new YamlConfigurationFactory<>(
DefaultLoggingFactory.class, BaseValidator.newValidator(), objectMapper, "dw-ssl");

@Before
public void setUp() throws Exception {
objectMapper.getSubtypeResolver().registerSubtypes(TcpSocketAppenderFactory.class);
}

private ServerSocket createServerSocket() {
try {
return createSslContextFactory().newSslServerSocket("localhost", 0, 0);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

private SslContextFactory createSslContextFactory() throws Exception {
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath(resourcePath("stores/tls_server.jks").getAbsolutePath());
sslContextFactory.setKeyStorePassword("server_pass");
sslContextFactory.start();
return sslContextFactory;
}

private static File resourcePath(String path) throws URISyntaxException {
return new File(Resources.getResource(path).toURI());
}

@Test
public void testTlsLogging() throws Exception {
DefaultLoggingFactory loggingFactory = yamlConfigurationFactory.build(new SubstitutingSourceProvider(
new ResourceConfigurationSourceProvider(), new StrSubstitutor(ImmutableMap.of(
"tls.trust_store.path", resourcePath("stores/tls_client.jks").getAbsolutePath(),
"tls.trust_store.pass", "client_pass",
"tls.server_port", tcpServer.getServerSocket().getLocalPort()
))), "yaml/logging-tls.yml");
loggingFactory.configure(new MetricRegistry(), "tls-appender-test");

Logger logger = LoggerFactory.getLogger("com.example.app");
for (int i = 0; i < tcpServer.getMessageCount(); i++) {
logger.info("Application log {}", i);
}

tcpServer.getLatch().await(5, TimeUnit.SECONDS);
assertThat(tcpServer.getLatch().getCount()).isEqualTo(0);
loggingFactory.reset();
}

@Test
public void testParseCustomConfiguration() throws Exception {
DefaultLoggingFactory loggingFactory = yamlConfigurationFactory.build(
resourcePath("yaml/logging-tls-custom.yml"));
assertThat(loggingFactory.getAppenders()).hasSize(1);
TlsSocketAppenderFactory<ILoggingEvent> appenderFactory = (TlsSocketAppenderFactory<ILoggingEvent>)
loggingFactory.getAppenders().get(0);
assertThat(appenderFactory.getHost()).isEqualTo("172.16.11.244");
assertThat(appenderFactory.getPort()).isEqualTo(17002);
assertThat(appenderFactory.getKeyStorePath()).isEqualTo("/path/to/keystore.p12");
assertThat(appenderFactory.getKeyStorePassword()).isEqualTo("keystore_pass");
assertThat(appenderFactory.getKeyStoreType()).isEqualTo("PKCS12");
assertThat(appenderFactory.getKeyStoreProvider()).isEqualTo("BC");
assertThat(appenderFactory.getTrustStorePath()).isEqualTo("/path/to/trust_store.jks");
assertThat(appenderFactory.getTrustStorePassword()).isEqualTo("trust_store_pass");
assertThat(appenderFactory.getTrustStoreType()).isEqualTo("JKS");
assertThat(appenderFactory.getTrustStoreProvider()).isEqualTo("SUN");
assertThat(appenderFactory.getJceProvider()).isEqualTo("Conscrypt");
assertThat(appenderFactory.isValidateCerts()).isTrue();
assertThat(appenderFactory.isValidatePeers()).isTrue();
assertThat(appenderFactory.getSupportedProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
assertThat(appenderFactory.getExcludedProtocols()).isEmpty();
assertThat(appenderFactory.getSupportedCipherSuites()).containsExactly("ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256");
assertThat(appenderFactory.getExcludedCipherSuites()).isEmpty();
}
}
Binary file not shown.
Binary file not shown.
20 changes: 20 additions & 0 deletions dropwizard-logging/src/test/resources/yaml/logging-tls-custom.yml
@@ -0,0 +1,20 @@
level: INFO
appenders:
- type: tls
host: 172.16.11.244
port: 17002
keyStorePath: '/path/to/keystore.p12'
keyStorePassword: keystore_pass
keyStoreType: PKCS12
keyStoreProvider: BC
trustStorePath: '/path/to/trust_store.jks'
trustStorePassword: trust_store_pass
trustStoreType: JKS
trustStoreProvider: SUN
jceProvider: Conscrypt
validateCerts: true
validatePeers: true
supportedProtocols: [TLSv1.1, TLSv1.2]
excludedProtocols: []
supportedCipherSuites: [ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256]
excludedCipherSuites: []
7 changes: 7 additions & 0 deletions dropwizard-logging/src/test/resources/yaml/logging-tls.yml
@@ -0,0 +1,7 @@
level: INFO
appenders:
- type: tls
host: localhost
port: ${tls.server_port}
trustStorePath: ${tls.trust_store.path}
trustStorePassword: ${tls.trust_store.pass}

0 comments on commit 777dba2

Please sign in to comment.