diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2017-11-14 17:32:37 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2017-11-14 17:32:37 +0100 |
commit | 59f7db352ea4ef2dd6a96fe986a18b61f28211f7 (patch) | |
tree | 0116444c65ad0240aad9b68ce68df4620b7b1489 /jdisc_http_service | |
parent | 5f83aa475181547481e7c830f238233e718b1e4b (diff) |
Add interface for custom keystore implementation with hot-reloading
Diffstat (limited to 'jdisc_http_service')
8 files changed, 218 insertions, 66 deletions
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java index 64d77e41f3c..8255e16e0ee 100644 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java @@ -5,9 +5,9 @@ import com.google.inject.Inject; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.http.ConnectorConfig; import com.yahoo.jdisc.http.ConnectorConfig.Ssl; -import com.yahoo.jdisc.http.ConnectorConfig.Ssl.PemKeyStore; import com.yahoo.jdisc.http.SecretStore; -import com.yahoo.jdisc.http.ssl.pem.PemSslKeyStore; +import com.yahoo.jdisc.http.ssl.DefaultSslKeyStoreContext; +import com.yahoo.jdisc.http.ssl.SslKeyStoreConfigurator; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -17,16 +17,7 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.ssl.SslContextFactory; -import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.channels.ServerSocketChannel; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyStore; -import java.util.logging.Logger; - -import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.JKS; -import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM; /** * @author Einar M R Rosenvinge @@ -34,14 +25,17 @@ import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM; */ public class ConnectorFactory { - private final static Logger log = Logger.getLogger(ConnectorFactory.class.getName()); private final ConnectorConfig connectorConfig; private final SecretStore secretStore; + private final SslKeyStoreConfigurator sslKeyStoreConfigurator; @Inject - public ConnectorFactory(ConnectorConfig connectorConfig, SecretStore secretStore) { + public ConnectorFactory(ConnectorConfig connectorConfig, + SecretStore secretStore, + SslKeyStoreConfigurator sslKeyStoreConfigurator) { this.connectorConfig = connectorConfig; this.secretStore = secretStore; + this.sslKeyStoreConfigurator = sslKeyStoreConfigurator; if (connectorConfig.ssl().enabled()) validateSslConfig(connectorConfig); @@ -50,30 +44,6 @@ public class ConnectorFactory { // TODO: can be removed when we have dedicated SSL config in services.xml private static void validateSslConfig(ConnectorConfig config) { ConnectorConfig.Ssl ssl = config.ssl(); - - if (ssl.keyStoreType() == JKS) { - if (!ssl.pemKeyStore().keyPath().isEmpty() || ! ssl.pemKeyStore().certificatePath().isEmpty()) { - throw new IllegalArgumentException("pemKeyStore attributes can not be set when keyStoreType is JKS."); - } - if (ssl.keyDbKey().isEmpty()) { - throw new IllegalArgumentException("Missing password for JKS keystore"); - } - } - if (ssl.keyStoreType() == PEM) { - if (! ssl.keyStorePath().isEmpty()) { - throw new IllegalArgumentException("keyStorePath can not be set when keyStoreType is PEM"); - } - if (!ssl.keyDbKey().isEmpty()) { - // TODO Make an error once there are separate passwords for truststore and keystore - log.warning("Encrypted PEM key stores are not supported. Password is only applied to truststore"); - } - if (ssl.pemKeyStore().certificatePath().isEmpty()) { - throw new IllegalArgumentException("Missing certificate path."); - } - if (ssl.pemKeyStore().keyPath().isEmpty()) { - throw new IllegalArgumentException("Missing key path."); - } - } if (!ssl.trustStorePath().isEmpty() && ssl.useTrustStorePassword() && ssl.keyDbKey().isEmpty()) { throw new IllegalArgumentException("Missing password for JKS truststore"); } @@ -128,6 +98,9 @@ public class ConnectorFactory { Ssl sslConfig = connectorConfig.ssl(); SslContextFactory factory = new SslContextFactory(); + + sslKeyStoreConfigurator.configure(new DefaultSslKeyStoreContext(factory)); + switch (sslConfig.clientAuth()) { case NEED_AUTH: factory.setNeedClientAuth(true); @@ -172,16 +145,6 @@ public class ConnectorFactory { } String keyDbPassword = sslConfig.keyDbKey(); - switch (sslConfig.keyStoreType()) { - case PEM: - factory.setKeyStore(createPemKeyStore(sslConfig.pemKeyStore())); - break; - case JKS: - factory.setKeyStorePath(sslConfig.keyStorePath()); - factory.setKeyStoreType(sslConfig.keyStoreType().toString()); - factory.setKeyStorePassword(secretStore.getSecret(keyDbPassword)); - break; - } if (!sslConfig.trustStorePath().isEmpty()) { factory.setTrustStorePath(sslConfig.trustStorePath()); @@ -196,16 +159,4 @@ public class ConnectorFactory { return new SslConnectionFactory(factory, HttpVersion.HTTP_1_1.asString()); } - private static KeyStore createPemKeyStore(PemKeyStore pemKeyStore) { - try { - Path certificatePath = Paths.get(pemKeyStore.certificatePath()); - Path keyPath = Paths.get(pemKeyStore.keyPath()); - return new PemSslKeyStore(certificatePath, keyPath).loadJavaKeyStore(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (Exception e) { - throw new RuntimeException("Failed setting up key store for " + pemKeyStore.keyPath() + ", " + pemKeyStore.certificatePath(), e); - } - } - } diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreConfigurator.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreConfigurator.java new file mode 100644 index 00000000000..fb0a5869bb3 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreConfigurator.java @@ -0,0 +1,95 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import com.google.inject.Inject; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.ssl.pem.PemSslKeyStore; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.util.logging.Logger; + +/** + * @author bjorncs + */ +public class DefaultSslKeyStoreConfigurator implements SslKeyStoreConfigurator { + + private static final Logger log = Logger.getLogger(DefaultSslKeyStoreConfigurator.class.getName()); + + private final SecretStore secretStore; + private final ConnectorConfig.Ssl config; + + @Inject + public DefaultSslKeyStoreConfigurator(ConnectorConfig config, SecretStore secretStore) { + validateConfig(config.ssl()); + this.secretStore = secretStore; + this.config = config.ssl(); + } + + private static void validateConfig(ConnectorConfig.Ssl config) { + if (!config.enabled()) return; + switch (config.keyStoreType()) { + case JKS: + validateJksConfig(config); + break; + case PEM: + validatePemConfig(config); + break; + } + } + + @Override + public void configure(SslKeyStoreContext context) { + if (!config.enabled()) return; + switch (config.keyStoreType()) { + case JKS: + context.updateKeyStore(config.keyStorePath(), "JKS", secretStore.getSecret(config.keyDbKey())); + break; + case PEM: + context.updateKeyStore(createPemKeyStore(config.pemKeyStore())); + break; + } + } + + private static void validateJksConfig(ConnectorConfig.Ssl ssl) { + if (!ssl.pemKeyStore().keyPath().isEmpty() || ! ssl.pemKeyStore().certificatePath().isEmpty()) { + throw new IllegalArgumentException("pemKeyStore attributes can not be set when keyStoreType is JKS."); + } + if (ssl.keyDbKey().isEmpty()) { + throw new IllegalArgumentException("Missing password for JKS keystore"); + } + } + + private static void validatePemConfig(ConnectorConfig.Ssl ssl) { + if (! ssl.keyStorePath().isEmpty()) { + throw new IllegalArgumentException("keyStorePath can not be set when keyStoreType is PEM"); + } + if (!ssl.keyDbKey().isEmpty()) { + // TODO Make an error once there are separate passwords for truststore and keystore + log.warning("Encrypted PEM key stores are not supported. Password is only applied to truststore"); + } + if (ssl.pemKeyStore().certificatePath().isEmpty()) { + throw new IllegalArgumentException("Missing certificate path."); + } + if (ssl.pemKeyStore().keyPath().isEmpty()) { + throw new IllegalArgumentException("Missing key path."); + } + } + + private static KeyStore createPemKeyStore(ConnectorConfig.Ssl.PemKeyStore pemKeyStore) { + try { + Path certificatePath = Paths.get(pemKeyStore.certificatePath()); + Path keyPath = Paths.get(pemKeyStore.keyPath()); + return new PemSslKeyStore(certificatePath, keyPath).loadJavaKeyStore(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (Exception e) { + throw new RuntimeException("Failed setting up key store for " + pemKeyStore.keyPath() + ", " + pemKeyStore.certificatePath(), e); + } + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreContext.java new file mode 100644 index 00000000000..8a95893eaeb --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/DefaultSslKeyStoreContext.java @@ -0,0 +1,51 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import java.security.KeyStore; +import java.util.function.Consumer; + +/** + * @author bjorncs + */ +public class DefaultSslKeyStoreContext implements SslKeyStoreContext { + + private final SslContextFactory sslContextFactory; + + public DefaultSslKeyStoreContext(SslContextFactory sslContextFactory) { + this.sslContextFactory = sslContextFactory; + } + + @Override + public void updateKeyStore(KeyStore keyStore) { + updateKeyStore(keyStore, null); + } + + @Override + public void updateKeyStore(KeyStore keyStore, String password) { + updateKeyStore(sslContextFactory -> { + sslContextFactory.setKeyStore(keyStore); + if (password != null) { + sslContextFactory.setKeyStorePassword(null); + } + }); + } + + @Override + public void updateKeyStore(String keyStorePath, String keyStoreType, String keyStorePassword) { + updateKeyStore(sslContextFactory -> { + sslContextFactory.setKeyStorePath(keyStorePath); + sslContextFactory.setKeyStoreType(keyStoreType); + sslContextFactory.setKeyStorePassword(keyStorePassword); + }); + } + + private void updateKeyStore(Consumer<SslContextFactory> reloader) { + try { + sslContextFactory.reload(reloader); + } catch (Exception e) { + throw new RuntimeException("Could not update keystore: " + e.getMessage(), e); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreConfigurator.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreConfigurator.java new file mode 100644 index 00000000000..619f4a636ed --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreConfigurator.java @@ -0,0 +1,14 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +/** + * An interface for an component that can configure an {@link SslKeyStoreContext}. The implementor can assume that + * the {@link SslKeyStoreContext} instance is thread-safe and be updated at any time + * during and after the call to{@link #configure(SslKeyStoreContext)}. + * Modifying the {@link SslKeyStoreContext} instance will trigger a hot reload of the keystore in JDisc. + * + * @author bjorncs + */ +public interface SslKeyStoreConfigurator { + void configure(SslKeyStoreContext context); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreContext.java new file mode 100644 index 00000000000..2a25f6d78b5 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreContext.java @@ -0,0 +1,16 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import java.security.KeyStore; + +/** + * An interface to update the keystore in JDisc. Any update will trigger a hot reload and new connections will + * immediately see the new certificate chain. + * + * @author bjorncs + */ +public interface SslKeyStoreContext { + void updateKeyStore(KeyStore keyStore); + void updateKeyStore(KeyStore keyStore, String password); + void updateKeyStore(String keyStorePath, String keyStoreType, String keyStorePassword); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java new file mode 100644 index 00000000000..5f817d4cfc2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author bjorncs + */ +@ExportPackage +package com.yahoo.jdisc.http.ssl; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java index 1200a06be2c..0d8f433cc39 100644 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java @@ -11,6 +11,7 @@ import com.yahoo.jdisc.http.ConnectorConfig.Builder; import com.yahoo.jdisc.http.SecretStore; import com.yahoo.jdisc.http.server.jetty.ConnectorFactory; import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import com.yahoo.jdisc.http.ssl.DefaultSslKeyStoreConfigurator; /** * Guice module for test ConnectorFactories @@ -46,7 +47,9 @@ public class ConnectorFactoryRegistryModule implements Module { private static class StaticKeyDbConnectorFactory extends ConnectorFactory { public StaticKeyDbConnectorFactory(ConnectorConfig connectorConfig) { - super(connectorConfig, new MockSecretStore()); + super(connectorConfig, + new MockSecretStore(), + new DefaultSslKeyStoreConfigurator(connectorConfig, new MockSecretStore())); } } diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java index fceec51231a..70037a59b32 100644 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java @@ -4,6 +4,7 @@ package com.yahoo.jdisc.http.server.jetty; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.http.ConnectorConfig; import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.ssl.DefaultSslKeyStoreConfigurator; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; @@ -23,7 +24,7 @@ import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM; import static org.hamcrest.CoreMatchers.equalTo; /** - * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + * @author Einar M R Rosenvinge */ public class ConnectorFactoryTest { @@ -38,7 +39,10 @@ public class ConnectorFactoryTest { new Ssl.PemKeyStore.Builder() .keyPath("nonEmpty")))); - ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSecretStore()); + ThrowingSecretStore secretStore = new ThrowingSecretStore(); + ConnectorFactory willThrowException = new ConnectorFactory(config, + secretStore, + new DefaultSslKeyStoreConfigurator(config, secretStore)); } @Test(expectedExceptions = IllegalArgumentException.class) @@ -50,15 +54,21 @@ public class ConnectorFactoryTest { .keyStoreType(PEM) .keyStorePath("nonEmpty"))); - ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSecretStore()); + ThrowingSecretStore secretStore = new ThrowingSecretStore(); + ConnectorFactory willThrowException = new ConnectorFactory(config, + secretStore, + new DefaultSslKeyStoreConfigurator(config, secretStore)); } @Test public void requireThatNoPreBoundChannelWorks() throws Exception { Server server = new Server(); try { - ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()), - new ThrowingSecretStore()); + ConnectorConfig config = new ConnectorConfig(new ConnectorConfig.Builder()); + ThrowingSecretStore secretStore = new ThrowingSecretStore(); + ConnectorFactory factory = new ConnectorFactory(config, + secretStore, + new DefaultSslKeyStoreConfigurator(config, secretStore)); JDiscServerConnector connector = (JDiscServerConnector)factory.createConnector(new DummyMetric(), server, null); server.addConnector(connector); @@ -85,7 +95,11 @@ public class ConnectorFactoryTest { ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.socket().bind(new InetSocketAddress(0)); - ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()), new ThrowingSecretStore()); + ConnectorConfig config = new ConnectorConfig(new ConnectorConfig.Builder()); + ThrowingSecretStore secretStore = new ThrowingSecretStore(); + ConnectorFactory factory = new ConnectorFactory(config, + secretStore, + new DefaultSslKeyStoreConfigurator(config, secretStore)); JDiscServerConnector connector = (JDiscServerConnector) factory.createConnector(new DummyMetric(), server, serverChannel); server.addConnector(connector); server.setHandler(new HelloWorldHandler()); |