Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reload certificate configuration at runtime (#1799)
* Reload certificate configuration at runtime * Add documentation on SslReloadBundle * Update release notes for runtime certificate reload
- Loading branch information
1 parent
c48bc04
commit bd388e9
Showing
15 changed files
with
353 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
dropwizard-core/src/main/java/io/dropwizard/sslreload/SslReloadBundle.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,49 @@ | |||
package io.dropwizard.sslreload; | |||
|
|||
import com.google.common.collect.ImmutableSet; | |||
import io.dropwizard.Bundle; | |||
import io.dropwizard.jetty.MutableServletContextHandler; | |||
import io.dropwizard.jetty.SslReload; | |||
import io.dropwizard.setup.Bootstrap; | |||
import io.dropwizard.setup.Environment; | |||
import org.eclipse.jetty.util.component.AbstractLifeCycle; | |||
import org.eclipse.jetty.util.component.LifeCycle; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
|
|||
import java.util.Collection; | |||
|
|||
/** Bundle that gathers all the ssl connectors and registers an admin task that will | |||
* refresh ssl configuration on request */ | |||
public class SslReloadBundle implements Bundle { | |||
private static final Logger LOGGER = LoggerFactory.getLogger(SslReloadBundle.class); | |||
|
|||
private final SslReloadTask reloadTask = new SslReloadTask(); | |||
|
|||
@Override | |||
public void initialize(Bootstrap<?> bootstrap) { | |||
} | |||
|
|||
@Override | |||
public void run(Environment environment) { | |||
environment.getApplicationContext().addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() { | |||
@Override | |||
public void lifeCycleStarted(LifeCycle event) { | |||
final ImmutableSet<SslReload> reloaders = ImmutableSet.<SslReload>builder() | |||
.addAll(getReloaders(environment.getApplicationContext())) | |||
.addAll(getReloaders(environment.getAdminContext())) | |||
.build(); | |||
|
|||
LOGGER.info("{} ssl reloaders registered", reloaders.size()); | |||
reloadTask.setReloaders(reloaders); | |||
} | |||
}); | |||
|
|||
environment.admin().addTask(reloadTask); | |||
} | |||
|
|||
private Collection<SslReload> getReloaders(MutableServletContextHandler handler) { | |||
return handler.getServer().getBeans(SslReload.class); | |||
} | |||
} | |||
|
42 changes: 42 additions & 0 deletions
42
dropwizard-core/src/main/java/io/dropwizard/sslreload/SslReloadTask.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,42 @@ | |||
package io.dropwizard.sslreload; | |||
|
|||
import com.google.common.collect.ImmutableMultimap; | |||
import io.dropwizard.jetty.SslReload; | |||
import io.dropwizard.servlets.tasks.Task; | |||
import org.eclipse.jetty.util.ssl.SslContextFactory; | |||
|
|||
import java.io.PrintWriter; | |||
import java.util.Collection; | |||
|
|||
/** A task that will refresh all ssl factories with up to date certificate information */ | |||
public class SslReloadTask extends Task { | |||
private Collection<SslReload> reloader; | |||
|
|||
protected SslReloadTask() { | |||
super("reload-ssl"); | |||
} | |||
|
|||
@Override | |||
public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception { | |||
// Iterate through all the reloaders first to ensure valid configuration | |||
for (SslReload reloader : getReloaders()) { | |||
reloader.reload(new SslContextFactory()); | |||
} | |||
|
|||
// Now we know that configuration is valid, reload for real | |||
for (SslReload reloader : getReloaders()) { | |||
reloader.reload(); | |||
} | |||
|
|||
output.write("Reloaded certificate configuration\n"); | |||
} | |||
|
|||
public Collection<SslReload> getReloaders() { | |||
return reloader; | |||
} | |||
|
|||
public void setReloaders(Collection<SslReload> reloader) { | |||
this.reloader = reloader; | |||
} | |||
} | |||
|
18 changes: 18 additions & 0 deletions
18
dropwizard-e2e/src/main/java/com/example/sslreload/SslReloadApp.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,18 @@ | |||
package com.example.sslreload; | |||
|
|||
import io.dropwizard.Application; | |||
import io.dropwizard.Configuration; | |||
import io.dropwizard.setup.Bootstrap; | |||
import io.dropwizard.setup.Environment; | |||
import io.dropwizard.sslreload.SslReloadBundle; | |||
|
|||
public class SslReloadApp extends Application<Configuration> { | |||
@Override | |||
public void initialize(Bootstrap<Configuration> bootstrap) { | |||
bootstrap.addBundle(new SslReloadBundle()); | |||
} | |||
|
|||
@Override | |||
public void run(Configuration configuration, Environment environment) throws Exception { | |||
} | |||
} |
155 changes: 155 additions & 0 deletions
155
dropwizard-e2e/src/test/java/com/example/sslreload/SslReloadAppTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,155 @@ | |||
package com.example.sslreload; | |||
|
|||
import com.google.common.io.CharStreams; | |||
import com.google.common.io.Files; | |||
import com.google.common.io.Resources; | |||
import io.dropwizard.Configuration; | |||
import io.dropwizard.testing.ConfigOverride; | |||
import io.dropwizard.testing.ResourceHelpers; | |||
import io.dropwizard.testing.junit.DropwizardAppRule; | |||
import org.apache.http.conn.ssl.NoopHostnameVerifier; | |||
import org.junit.After; | |||
import org.junit.BeforeClass; | |||
import org.junit.ClassRule; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
|
|||
import javax.net.ssl.HttpsURLConnection; | |||
import javax.net.ssl.SSLContext; | |||
import javax.net.ssl.TrustManager; | |||
import javax.net.ssl.X509TrustManager; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
import java.net.URL; | |||
import java.nio.charset.StandardCharsets; | |||
import java.security.cert.CertificateException; | |||
import java.security.cert.X509Certificate; | |||
|
|||
import static org.assertj.core.api.Java6Assertions.assertThat; | |||
|
|||
public class SslReloadAppTest { | |||
@ClassRule | |||
public static final TemporaryFolder FOLDER = new TemporaryFolder(); | |||
|
|||
private static final X509TrustManager TRUST_ALL = new X509TrustManager() { | |||
@Override | |||
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { | |||
|
|||
} | |||
|
|||
@Override | |||
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { | |||
|
|||
} | |||
|
|||
@Override | |||
public X509Certificate[] getAcceptedIssuers() { | |||
return new X509Certificate[0]; | |||
} | |||
}; | |||
|
|||
private static File keystore; | |||
|
|||
@Rule | |||
public final DropwizardAppRule<Configuration> rule = | |||
new DropwizardAppRule<>(SslReloadApp.class, ResourceHelpers.resourceFilePath("sslreload/config.yml"), | |||
ConfigOverride.config("server.applicationConnectors[0].keyStorePath", keystore.getAbsolutePath()), | |||
ConfigOverride.config("server.adminConnectors[0].keyStorePath", keystore.getAbsolutePath())); | |||
|
|||
@BeforeClass | |||
public static void setupClass() throws IOException { | |||
keystore = FOLDER.newFile("keystore.jks"); | |||
final byte[] keystoreBytes = Resources.toByteArray(Resources.getResource("sslreload/keystore.jks")); | |||
Files.write(keystoreBytes, keystore); | |||
} | |||
|
|||
@After | |||
public void after() throws IOException { | |||
// Reset keystore to known good keystore | |||
final byte[] keystoreBytes = Resources.toByteArray(Resources.getResource("sslreload/keystore.jks")); | |||
Files.write(keystoreBytes, keystore); | |||
} | |||
|
|||
@Test | |||
public void reloadCertificateChangesTheServerCertificate() throws Exception { | |||
// Copy over our new keystore that has our new certificate to the current | |||
// location of our keystore | |||
final byte[] keystore2Bytes = Resources.toByteArray(Resources.getResource("sslreload/keystore2.jks")); | |||
Files.write(keystore2Bytes, keystore); | |||
|
|||
// Get the bytes for the first certificate, and trigger a reload | |||
byte[] firstCertBytes = certBytes(200, "Reloaded certificate configuration\n"); | |||
|
|||
// Get the bytes from our newly reloaded certificate | |||
byte[] secondCertBytes = certBytes(200, "Reloaded certificate configuration\n"); | |||
|
|||
// Get the bytes from the reloaded certificate, but it should be the same | |||
// as the second cert because we didn't change anything! | |||
byte[] thirdCertBytes = certBytes(200, "Reloaded certificate configuration\n"); | |||
|
|||
assertThat(firstCertBytes).isNotEqualTo(secondCertBytes); | |||
assertThat(secondCertBytes).isEqualTo(thirdCertBytes); | |||
} | |||
|
|||
@Test | |||
public void badReloadDoesNotChangeTheServerCertificate() throws Exception { | |||
// This keystore has a different password than what jetty has been configured with | |||
// the password is "password2" | |||
final byte[] badKeystore = Resources.toByteArray(Resources.getResource("sslreload/keystore-diff-pwd.jks")); | |||
Files.write(badKeystore, keystore); | |||
|
|||
// Get the bytes for the first certificate. The reload should fail | |||
byte[] firstCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); | |||
|
|||
// Issue another request. The returned certificate should be the same as before | |||
byte[] secondCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); | |||
|
|||
// And just to triple check, a third request will continue with | |||
// the same original certificate | |||
byte[] thirdCertBytes = certBytes(500, "Keystore was tampered with, or password was incorrect"); | |||
|
|||
assertThat(firstCertBytes) | |||
.isEqualTo(secondCertBytes) | |||
.isEqualTo(thirdCertBytes); | |||
} | |||
|
|||
/** Issues a POST against the reload ssl admin task, asserts that the code and content | |||
* are as expected, and finally returns the server certificate */ | |||
private byte[] certBytes(int code, String content) throws Exception { | |||
final URL url = new URL("https://localhost:" + rule.getAdminPort() + "/tasks/reload-ssl"); | |||
final HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); | |||
try { | |||
postIt(conn); | |||
|
|||
assertThat(conn.getResponseCode()).isEqualTo(code); | |||
if (code == 200) { | |||
assertThat(CharStreams.toString(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) | |||
.isEqualTo(content); | |||
} else { | |||
assertThat(CharStreams.toString(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) | |||
.contains(content); | |||
} | |||
|
|||
// The certificates are self signed, so are the only cert in the chain. | |||
// Thus, we return the one and only certificate. | |||
return conn.getServerCertificates()[0].getEncoded(); | |||
} finally { | |||
conn.disconnect(); | |||
} | |||
} | |||
|
|||
/** Configure SSL and POST request parameters */ | |||
private void postIt(HttpsURLConnection conn) throws Exception { | |||
final SSLContext sslCtx = SSLContext.getInstance("TLS"); | |||
sslCtx.init(null, new TrustManager[]{TRUST_ALL}, null); | |||
|
|||
conn.setHostnameVerifier(new NoopHostnameVerifier()); | |||
conn.setSSLSocketFactory(sslCtx.getSocketFactory()); | |||
|
|||
// Make it a POST | |||
conn.setDoOutput(true); | |||
conn.getOutputStream().write(new byte[]{}); | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,15 @@ | |||
server: | |||
applicationConnectors: | |||
- type: https | |||
port: 0 | |||
keyStorePath: keystore.jks | |||
keyStorePassword: password | |||
validateCerts: false | |||
validatePeers: false | |||
adminConnectors: | |||
- type: https | |||
port: 0 | |||
keyStorePath: keystore.jks | |||
keyStorePassword: password | |||
validateCerts: false | |||
validatePeers: false |
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.