diff options
6 files changed, 103 insertions, 158 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/ConfigServerInfo.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/ConfigServerInfo.java index 218b2947a21..5dd80961062 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/ConfigServerInfo.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/ConfigServerInfo.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; import java.net.URI; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,6 +29,7 @@ public class ConfigServerInfo { private final Optional<KeyStoreOptions> keyStoreOptions; private final Optional<KeyStoreOptions> trustStoreOptions; private final Optional<AthenzIdentity> athenzIdentity; + private final Optional<ConfigServerConfig.Sia> siaConfig; public ConfigServerInfo(ConfigServerConfig config) { this.configServerHostNames = config.hosts(); @@ -46,6 +48,7 @@ public class ConfigServerInfo { this.athenzIdentity = createAthenzIdentity( config.athenzDomain(), config.serviceName()); + this.siaConfig = verifySiaConfig(config.sia()); } public List<String> getConfigServerHostNames() { @@ -77,6 +80,10 @@ public class ConfigServerInfo { return athenzIdentity; } + public Optional<ConfigServerConfig.Sia> getSiaConfig() { + return siaConfig; + } + private static Map<String, URI> createConfigServerUris( String scheme, List<String> configServerHosts, @@ -86,6 +93,18 @@ public class ConfigServerInfo { hostname -> URI.create(scheme + "://" + hostname + ":" + port))); } + private static Optional<ConfigServerConfig.Sia> verifySiaConfig(ConfigServerConfig.Sia sia) { + List<String> configParams = Arrays.asList( + sia.credentialsPath(), sia.configserverIdentityName(), sia.hostIdentityName(), sia.trustStoreFile()); + if (configParams.stream().allMatch(String::isEmpty)) { + return Optional.empty(); + } else if (configParams.stream().noneMatch(String::isEmpty)) { + return Optional.of(sia); + } else { + throw new IllegalArgumentException("Inconsistent sia config: " + sia); + } + } + private static Optional<KeyStoreOptions> createKeyStoreOptions(String pathToKeyStore, char[] password, String type) { return Optional.ofNullable(pathToKeyStore) .filter(path -> !Strings.isNullOrEmpty(path)) diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/RealConfigServerClients.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/RealConfigServerClients.java index 1405a518625..13af642af4a 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/RealConfigServerClients.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/RealConfigServerClients.java @@ -29,7 +29,7 @@ public class RealConfigServerClients implements ConfigServerClients { private final ConfigServerInfo configServerInfo; public RealConfigServerClients(Environment environment) { - this(environment.getConfigServerInfo(), environment.getParentHostHostname()); + this(environment.getConfigServerInfo()); } /** @@ -39,9 +39,9 @@ public class RealConfigServerClients implements ConfigServerClients { * and kept up to date. On failure, this constructor will throw an exception and * the caller may retry later. */ - public RealConfigServerClients(ConfigServerInfo info, String hostname) { + public RealConfigServerClients(ConfigServerInfo info) { this.configServerInfo = info; - updater = SslConnectionSocketFactoryUpdater.createAndRefreshKeyStoreIfNeeded(info, hostname); + updater = SslConnectionSocketFactoryUpdater.createAndRefreshKeyStoreIfNeeded(info); configServerApi = ConfigServerApiImpl.create(info, updater); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryCreator.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryCreator.java deleted file mode 100644 index 85b2b426954..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryCreator.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.configserver; - -import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; -import com.yahoo.vespa.athenz.tls.SslContextBuilder; -import com.yahoo.vespa.hosted.node.admin.component.ConfigServerInfo; -import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import java.util.Collections; -import java.util.Optional; - -/** - * @author hakon - */ -class SslConnectionSocketFactoryCreator { - SSLConnectionSocketFactory createSocketFactory( - ConfigServerInfo configServerInfo, - Optional<KeyStoreOptions> keyStoreOptions) { - SSLContext context = makeSslContext(configServerInfo, keyStoreOptions); - return new SSLConnectionSocketFactory(context, makeHostnameVerifier(configServerInfo)); - } - - private static SSLContext makeSslContext( - ConfigServerInfo configServerInfo, - Optional<KeyStoreOptions> keyStoreOptions) { - SslContextBuilder sslContextBuilder = new SslContextBuilder(); - configServerInfo.getTrustStoreOptions() - .map(KeyStoreOptions::loadKeyStore) - .ifPresent(sslContextBuilder::withTrustStore); - keyStoreOptions.ifPresent(options -> - sslContextBuilder.withKeyStore(options.loadKeyStore(), options.password)); - - return sslContextBuilder.build(); - } - - private static HostnameVerifier makeHostnameVerifier(ConfigServerInfo configServerInfo) { - return configServerInfo.getAthenzIdentity() - .map(identity -> (HostnameVerifier) new AthenzIdentityVerifier(Collections.singleton(identity))) - .orElseGet(SSLConnectionSocketFactory::getDefaultHostnameVerifier); - } - -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdater.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdater.java index 57fa5526d73..5a97174762c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdater.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdater.java @@ -1,91 +1,86 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.configserver; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identity.SiaIdentityProvider; +import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; +import com.yahoo.vespa.athenz.tls.SslContextBuilder; +import com.yahoo.vespa.athenz.utils.AthenzIdentities; import com.yahoo.vespa.hosted.node.admin.component.ConfigServerInfo; -import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresher; -import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresherFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.io.File; +import java.nio.file.Paths; import java.util.HashSet; -import java.util.Optional; import java.util.Set; +import static java.util.Collections.singleton; + /** - * Responsible for updating SSLConnectionSocketFactory on ConfigServerApiImpl asynchronously - * and as required by embedded certificate expiry + * Responsible for updating {@link SSLConnectionSocketFactory} on {@link ConfigServerApiImpl} asynchronously + * using SIA based certificates (through {@link SiaIdentityProvider}). * + * @author bjorncs * @author hakon */ public class SslConnectionSocketFactoryUpdater implements AutoCloseable { - private final ConfigServerInfo configServerInfo; - private final SslConnectionSocketFactoryCreator socketFactoryCreator; - // Internal ConfigServerApi used to refresh the key store - private final ConfigServerApiImpl configServerApi; - private final Optional<ConfigServerKeyStoreRefresher> keyStoreRefresher; private final Object monitor = new Object(); - private SSLConnectionSocketFactory socketFactory = null; + private final HostnameVerifier configServerHostnameVerifier; + private final SiaIdentityProvider sia; + private final Set<ConfigServerApi> configServerApis = new HashSet<>(); + private SSLConnectionSocketFactory socketFactory; /** * Creates an updater with valid initial {@link SSLConnectionSocketFactory} * - * @param hostname the hostname of localhost * @throws RuntimeException if e.g. key store options have been specified, but was unable * create a create a key store with a valid certificate */ - public static SslConnectionSocketFactoryUpdater createAndRefreshKeyStoreIfNeeded( - ConfigServerInfo configServerInfo, String hostname) { - return new SslConnectionSocketFactoryUpdater( - configServerInfo, - hostname, - ConfigServerKeyStoreRefresher::new, - new SslConnectionSocketFactoryCreator()); + public static SslConnectionSocketFactoryUpdater createAndRefreshKeyStoreIfNeeded(ConfigServerInfo configServerInfo) { + SiaIdentityProvider siaIdentityProvider = configServerInfo.getSiaConfig() + .map(siaConfig -> + new SiaIdentityProvider( + (AthenzService) AthenzIdentities.from(siaConfig.hostIdentityName()), + Paths.get(siaConfig.credentialsPath()), + new File(siaConfig.trustStoreFile()))) + .orElse(null); + HostnameVerifier configServerHostnameVerifier = configServerInfo.getSiaConfig() + .map(siaConfig -> createHostnameVerifier(AthenzIdentities.from(siaConfig.configserverIdentityName()))) + .orElseGet(SSLConnectionSocketFactory::getDefaultHostnameVerifier); + return new SslConnectionSocketFactoryUpdater(siaIdentityProvider, configServerHostnameVerifier); } - /** Non-private for testing only */ - SslConnectionSocketFactoryUpdater( - ConfigServerInfo configServerInfo, - String hostname, - ConfigServerKeyStoreRefresherFactory refresherFactory, - SslConnectionSocketFactoryCreator socketFactoryCreator) { - this.configServerInfo = configServerInfo; - this.socketFactoryCreator = socketFactoryCreator; - - // ConfigServerApi used to refresh the key store. Does not itself rely on a socket - // factory with key store, of course. - SSLConnectionSocketFactory socketFactoryWithoutKeyStore = - socketFactoryCreator.createSocketFactory(configServerInfo, Optional.empty()); - configServerApi = ConfigServerApiImpl.createWithSocketFactory( - configServerInfo.getConfigServerUris(), socketFactoryWithoutKeyStore); - - // If we have keystore options, we should make sure we use the keystore with the latest certificate, - // start the keystore refresher. - keyStoreRefresher = configServerInfo.getKeyStoreOptions().map(keyStoreOptions -> { - ConfigServerKeyStoreRefresher keyStoreRefresher = refresherFactory.create( - keyStoreOptions, - this::updateSslConnectionSocketFactory, - configServerApi, - hostname); - - // Run the refresh once manually to make sure that we have a valid certificate, otherwise fail. - try { - keyStoreRefresher.refreshKeyStoreIfNeeded(); - updateSslConnectionSocketFactory(); - } catch (Exception e) { - throw new RuntimeException("Failed to acquire certificate to config server", e); - } - - keyStoreRefresher.start(); - return keyStoreRefresher; - }); + SslConnectionSocketFactoryUpdater(SiaIdentityProvider siaIdentityProvider, + HostnameVerifier configServerHostnameVerifier) { + this.configServerHostnameVerifier = configServerHostnameVerifier; + this.sia = siaIdentityProvider; + if (siaIdentityProvider != null) { + siaIdentityProvider.addReloadListener(this::updateSocketFactory); + socketFactory = createSocketFactory(siaIdentityProvider.getIdentitySslContext()); + } else { + socketFactory = createDefaultSslConnectionSocketFactory(); + } + } + + private void updateSocketFactory(SSLContext sslContext) { + synchronized (monitor) { + socketFactory = createSocketFactory(sslContext); + configServerApis.forEach(api -> api.setSSLConnectionSocketFactory(socketFactory)); + } } public SSLConnectionSocketFactory getCurrentSocketFactory() { - return socketFactory; + synchronized (monitor) { + return socketFactory; + } } - /** Register a {@link ConfigServerApi} whose SSLConnectionSocketFactory will be kept up to date */ + /** Register a {@link ConfigServerApi} whose {@link SSLConnectionSocketFactory} will be kept up to date */ public void registerConfigServerApi(ConfigServerApi configServerApi) { synchronized (monitor) { configServerApi.setSSLConnectionSocketFactory(socketFactory); @@ -101,17 +96,22 @@ public class SslConnectionSocketFactoryUpdater implements AutoCloseable { @Override public void close() { - keyStoreRefresher.ifPresent(ConfigServerKeyStoreRefresher::stop); - configServerApi.close(); + if (sia != null) { + sia.deconstruct(); + } } - private void updateSslConnectionSocketFactory() { - synchronized (monitor) { - socketFactory = socketFactoryCreator.createSocketFactory( - configServerInfo, - configServerInfo.getKeyStoreOptions()); + private SSLConnectionSocketFactory createSocketFactory(SSLContext sslContext) { + return new SSLConnectionSocketFactory(sslContext, configServerHostnameVerifier); + } - configServerApis.forEach(api -> api.setSSLConnectionSocketFactory(socketFactory)); - } + private SSLConnectionSocketFactory createDefaultSslConnectionSocketFactory() { + SSLContext sslContext = new SslContextBuilder().build(); + return createSocketFactory(sslContext); + } + + private static HostnameVerifier createHostnameVerifier(AthenzIdentity identity) { + return new AthenzIdentityVerifier(singleton(identity)); } + } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java index 587e6212ad4..f9d0736fe21 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.node.admin.provider; import com.google.inject.Inject; import com.yahoo.concurrent.classlock.ClassLocking; import com.yahoo.container.di.componentgraph.Provider; -import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.component.ConfigServerInfo; @@ -22,8 +21,7 @@ public class NodeAdminProvider implements Provider<NodeAdminStateUpdater> { MetricReceiverWrapper metricReceiver, ClassLocking classLocking) { ConfigServerClients clients = new RealConfigServerClients( - new ConfigServerInfo(configServerConfig), - Defaults.getDefaults().vespaHostname()); + new ConfigServerInfo(configServerConfig)); dockerAdmin = new DockerAdminComponent(configServerConfig, docker, diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdaterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdaterTest.java index 490f45b094c..6c6f8cf40fe 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdaterTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConnectionSocketFactoryUpdaterTest.java @@ -1,58 +1,31 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.configserver; -import com.yahoo.vespa.hosted.node.admin.component.ConfigServerInfo; -import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresher; -import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresherFactory; -import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.junit.Before; +import com.yahoo.vespa.athenz.identity.SiaIdentityProvider; +import com.yahoo.vespa.athenz.tls.SslContextBuilder; import org.junit.Test; -import java.util.Optional; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** - * @author hakon + * @author bjorncs */ public class SslConnectionSocketFactoryUpdaterTest { - private final ConfigServerInfo configServerInfo = mock(ConfigServerInfo.class); - private final String hostname = "host.oath.com"; - private final ConfigServerKeyStoreRefresherFactory refresherFactory = - mock(ConfigServerKeyStoreRefresherFactory.class); - private final ConfigServerKeyStoreRefresher refresher = - mock(ConfigServerKeyStoreRefresher.class); - private final SslConnectionSocketFactoryCreator socketFactoryCreator = - mock(SslConnectionSocketFactoryCreator.class); - private final SSLConnectionSocketFactory socketFactory = mock(SSLConnectionSocketFactory.class); - @Before - public void setUp() { - KeyStoreOptions keyStoreOptions = mock(KeyStoreOptions.class); - when(configServerInfo.getKeyStoreOptions()).thenReturn(Optional.of(keyStoreOptions)); - when(refresherFactory.create(any(), any(), any(), any())).thenReturn(refresher); - when(socketFactoryCreator.createSocketFactory(any(), any())) - .thenReturn(socketFactory); + @Test + public void creates_default_ssl_connection_factory_when_no_sia_provided() { + SslConnectionSocketFactoryUpdater updater = + new SslConnectionSocketFactoryUpdater(null, (hostname, session) -> true); + assertNotNull(updater.getCurrentSocketFactory()); } @Test - public void testSettingOfSocketFactory() { - SslConnectionSocketFactoryUpdater updater = new SslConnectionSocketFactoryUpdater( - configServerInfo, - hostname, - refresherFactory, - socketFactoryCreator); - - assertTrue(socketFactory == updater.getCurrentSocketFactory()); - - ConfigServerApi api = mock(ConfigServerApi.class); - updater.registerConfigServerApi(api); - verify(api, times(1)).setSSLConnectionSocketFactory(socketFactory); + public void creates_ssl_connection_factory_when_sia_provided() { + SiaIdentityProvider sia = mock(SiaIdentityProvider.class); + when(sia.getIdentitySslContext()).thenReturn(new SslContextBuilder().build()); + SslConnectionSocketFactoryUpdater updater = new SslConnectionSocketFactoryUpdater(sia, (hostname, session) -> true); + assertNotNull(updater.getCurrentSocketFactory()); } }
\ No newline at end of file |