diff --git a/http-server/src/main/java/com/facebook/airlift/http/server/HttpServer.java b/http-server/src/main/java/com/facebook/airlift/http/server/HttpServer.java index db14e329c7..cd143c2e70 100644 --- a/http-server/src/main/java/com/facebook/airlift/http/server/HttpServer.java +++ b/http-server/src/main/java/com/facebook/airlift/http/server/HttpServer.java @@ -52,6 +52,7 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.ssl.KeyStoreScanner; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.Scheduler; @@ -245,6 +246,9 @@ public HttpServer(HttpServerInfo httpServerInfo, if (config.getKeyManagerPassword() != null) { sslContextFactory.setKeyManagerPassword(config.getKeyManagerPassword()); } + KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory); + keyStoreScanner.setScanInterval(config.getKeystoreScanInterval()); + server.addBean(keyStoreScanner); } if (config.getTrustStorePath() != null) { Optional pemTrustStore = tryLoadPemTrustStore(config); @@ -329,6 +333,9 @@ public HttpServer(HttpServerInfo httpServerInfo, -1, sslConnectionFactory, new HttpConnectionFactory(adminConfiguration)); + KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory); + keyStoreScanner.setScanInterval(config.getKeystoreScanInterval()); + adminConnector.addBean(keyStoreScanner); } else { HttpConnectionFactory http1 = new HttpConnectionFactory(adminConfiguration); diff --git a/http-server/src/main/java/com/facebook/airlift/http/server/HttpServerConfig.java b/http-server/src/main/java/com/facebook/airlift/http/server/HttpServerConfig.java index 8bdce2788f..f39ee26107 100644 --- a/http-server/src/main/java/com/facebook/airlift/http/server/HttpServerConfig.java +++ b/http-server/src/main/java/com/facebook/airlift/http/server/HttpServerConfig.java @@ -136,6 +136,8 @@ public enum AuthorizationPolicy private Set defaultAllowedRoles = ImmutableSet.of(); private boolean allowUnsecureRequestsInAuthorizer; + private int keyStoreScanIntervalSeconds; + public boolean isHttpEnabled() { return httpEnabled; @@ -823,4 +825,17 @@ public List getHttpComplianceViolations() { return httpComplianceViolations; } + + @Config("http-server.https.keystore.scan-interval-seconds") + @ConfigDescription("Interval (in seconds) at which the server checks for updates to the HTTPS keystore file") + public HttpServerConfig setKeystoreScanInterval(int keyStoreScanIntervalSeconds) + { + this.keyStoreScanIntervalSeconds = keyStoreScanIntervalSeconds; + return this; + } + + public int getKeystoreScanInterval() + { + return keyStoreScanIntervalSeconds; + } } diff --git a/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerConfig.java b/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerConfig.java index 2484cf5919..afecbd37da 100644 --- a/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerConfig.java +++ b/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerConfig.java @@ -90,7 +90,8 @@ public void testDefaults() .setAllowUnsecureRequestsInAuthorizer(false) .setSniHostCheck(true) .setUriComplianceMode(UriCompliance.DEFAULT) - .setHttpComplianceViolations("")); + .setHttpComplianceViolations("") + .setKeystoreScanInterval(0)); } @Test @@ -149,6 +150,7 @@ public void testExplicitPropertyMappings() .put("http-server.https.sni-host-check", "false") .put("http-server.uri-compliance.mode", "LEGACY") .put("http-server.http-compliance.violations", "UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY") + .put("http-server.https.keystore.scan-interval-seconds", "2") .build(); HttpServerConfig expected = new HttpServerConfig() @@ -203,7 +205,8 @@ public void testExplicitPropertyMappings() .setAllowUnsecureRequestsInAuthorizer(true) .setSniHostCheck(false) .setUriComplianceMode(UriCompliance.LEGACY) - .setHttpComplianceViolations("UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY"); + .setHttpComplianceViolations("UNSAFE_HOST_HEADER,MISMATCHED_AUTHORITY") + .setKeystoreScanInterval(2); ConfigAssertions.assertFullMapping(properties, expected); } diff --git a/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerProvider.java b/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerProvider.java index 1c4bc9b045..9c978c95a3 100644 --- a/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerProvider.java +++ b/http-server/src/test/java/com/facebook/airlift/http/server/TestHttpServerProvider.java @@ -35,13 +35,16 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeSuite; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.cert.X509Certificate; import java.util.Base64; import java.util.concurrent.ExecutionException; @@ -82,6 +85,12 @@ public void setupSuite() Logging.initialize(); } + @DataProvider(name = "adminMode") + public static Object[][] adminMode() + { + return new Object[][] {{true}, {false}}; + } + @BeforeMethod public void setup() throws IOException @@ -310,16 +319,24 @@ public void testAuth() } } - @Test - public void testClientCertificateJava() + @Test(dataProvider = "adminMode") + public void testClientCertificateJava(boolean adminMode) throws Exception { + tempDir = createTempDirectory("test-keystore").toFile().getCanonicalFile(); + String tempKeyStorePath = tempDir.toPath().resolve("server.keystore").toAbsolutePath().toString(); + String originalKeyStorePath = getResource("clientcert-java/server.keystore").getPath(); + String replacementKeyStorePath = getResource("clientcert-java/replacementServer.keystore").getPath(); + + Files.copy(Path.of(originalKeyStorePath), Path.of(tempKeyStorePath), StandardCopyOption.REPLACE_EXISTING); + config.setHttpEnabled(false) - .setAdminEnabled(false) + .setAdminEnabled(adminMode) .setHttpsEnabled(true) .setHttpsPort(0) - .setKeystorePath(getResource("clientcert-java/server.keystore").getPath()) - .setKeystorePassword("airlift"); + .setKeystorePath(tempKeyStorePath) + .setKeystorePassword("airlift") + .setKeystoreScanInterval(1); createAndStartServer(createCertTestServlet()); @@ -337,6 +354,26 @@ public void testClientCertificateJava() assertEquals(response.getStatusCode(), HttpServletResponse.SC_OK); assertEquals(response.getBody(), "CN=testing,OU=Client,O=Airlift,L=Palo Alto,ST=CA,C=US"); } + + Files.copy(Path.of(replacementKeyStorePath), Path.of(tempKeyStorePath), StandardCopyOption.REPLACE_EXISTING); + + // Wait for the KeyStoreScanner to detect the file change. Scan interval is 1 second, so sleeping for 2.5 seconds + Thread.sleep(2500); + + HttpClientConfig clientConfig2 = new HttpClientConfig() + .setKeyStorePath(getResource("clientcert-java/replacementClient.keystore").getPath()) + .setKeyStorePassword("airlift") + .setTrustStorePath(getResource("clientcert-java/replacementClient.truststore").getPath()) + .setTrustStorePassword("airlift"); + + try (JettyHttpClient httpClient = new JettyHttpClient(clientConfig2)) { + StringResponse response = httpClient.execute( + prepareGet().setUri(httpServerInfo.getHttpsUri()).build(), + createStringResponseHandler()); + + assertEquals(response.getStatusCode(), HttpServletResponse.SC_OK); + assertEquals(response.getBody(), "CN=testing,OU=Client,O=Replacement,L=Palo Alto,ST=CA,C=US"); + } } @Test diff --git a/http-server/src/test/resources/clientcert-java/clientcert.sh b/http-server/src/test/resources/clientcert-java/clientcert.sh index 43263c8224..269e97926e 100755 --- a/http-server/src/test/resources/clientcert-java/clientcert.sh +++ b/http-server/src/test/resources/clientcert-java/clientcert.sh @@ -26,3 +26,23 @@ openssl pkcs12 -name client -inkey client.key -in client.crt -export -passout pa # create client truststore keytool -import -noprompt -alias ca -file ca.crt -storetype pkcs12 -storepass airlift -keystore client.truststore + +#A kesytore for testing the KeyStoreScanner's ability to transparently update the ssl config without needing a restart + +# create replacement server cert +openssl req -new -key server.key -subj "/C=US/ST=CA/L=Palo Alto/O=Replacement/OU=Server/CN=localhost" -out replacementServer.csr +openssl x509 -req -days 9999 -in replacementServer.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out replacementServer.crt + +# create replacement server keystore +openssl pkcs12 -name server -inkey server.key -in replacementServer.crt -export -passout pass:airlift -out replacementServer.keystore +keytool -import -noprompt -alias ca -file ca.crt -storetype pkcs12 -storepass airlift -keystore replacementServer.keystore + +# create client cert +openssl req -new -key client.key -subj "/C=US/ST=CA/L=Palo Alto/O=Replacement/OU=Client/CN=testing" -out replacementClient.csr +openssl x509 -req -days 9999 -in replacementClient.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out replacementClient.crt + +# create replacement client keystore +openssl pkcs12 -name client -inkey client.key -in replacementClient.crt -export -passout pass:airlift -out replacementClient.keystore + +# create replacement client truststore +keytool -import -noprompt -alias ca -file ca.crt -storetype pkcs12 -storepass airlift -keystore replacementClient.truststore \ No newline at end of file diff --git a/http-server/src/test/resources/clientcert-java/replacementClient.keystore b/http-server/src/test/resources/clientcert-java/replacementClient.keystore new file mode 100644 index 0000000000..fe6f7e4299 Binary files /dev/null and b/http-server/src/test/resources/clientcert-java/replacementClient.keystore differ diff --git a/http-server/src/test/resources/clientcert-java/replacementClient.truststore b/http-server/src/test/resources/clientcert-java/replacementClient.truststore new file mode 100644 index 0000000000..2a7f3e7212 Binary files /dev/null and b/http-server/src/test/resources/clientcert-java/replacementClient.truststore differ diff --git a/http-server/src/test/resources/clientcert-java/replacementServer.keystore b/http-server/src/test/resources/clientcert-java/replacementServer.keystore new file mode 100644 index 0000000000..8ae3027b12 Binary files /dev/null and b/http-server/src/test/resources/clientcert-java/replacementServer.keystore differ