This is an automated email from the ASF dual-hosted git repository. apucher pushed a commit to branch listener-tls-customization in repository https://gitbox.apache.org/repos/asf/pinot.git
commit 7a18a16e9fe160130f9a72864dbb7938963e0d4f Author: Alexander Pucher <apuc...@apache.org> AuthorDate: Thu Jan 27 15:32:11 2022 -0800 prototype --- .../org/apache/pinot/core/transport/TlsConfig.java | 23 +- .../apache/pinot/core/util/ListenerConfigUtil.java | 59 +++- .../java/org/apache/pinot/core/util/TlsUtils.java | 77 +++-- .../tests/BasicAuthTlsRealtimeIntegrationTest.java | 83 ++--- .../integration/tests/TlsIntegrationTest.java | 354 +++++++++++++++++++++ .../CertBasedTlsChannelAccessControlFactory.java | 3 +- .../src/test/resources/empty.jks | Bin 0 -> 32 bytes .../src/test/resources/empty.p12 | Bin 0 -> 103 bytes .../src/test/resources/tlstest.jks | Bin 2277 -> 2283 bytes .../src/test/resources/tlstest.p12 | Bin 10155 -> 2645 bytes 10 files changed, 487 insertions(+), 112 deletions(-) diff --git a/pinot-core/src/main/java/org/apache/pinot/core/transport/TlsConfig.java b/pinot-core/src/main/java/org/apache/pinot/core/transport/TlsConfig.java index c8de43b..9b86c34 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/transport/TlsConfig.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/transport/TlsConfig.java @@ -18,6 +18,8 @@ */ package org.apache.pinot.core.transport; +import io.netty.handler.ssl.SslProvider; +import java.security.KeyStore; import org.apache.commons.lang3.StringUtils; @@ -26,13 +28,28 @@ import org.apache.commons.lang3.StringUtils; */ public class TlsConfig { private boolean _clientAuthEnabled; - private String _keyStoreType; + private String _keyStoreType = KeyStore.getDefaultType(); private String _keyStorePath; private String _keyStorePassword; - private String _trustStoreType; + private String _trustStoreType = KeyStore.getDefaultType(); private String _trustStorePath; private String _trustStorePassword; - private String _sslProvider; + private String _sslProvider = SslProvider.JDK.toString(); + + public TlsConfig() { + // left blank + } + + public TlsConfig(TlsConfig tlsConfig) { + _clientAuthEnabled = tlsConfig._clientAuthEnabled; + _keyStoreType = tlsConfig._keyStoreType; + _keyStorePath = tlsConfig._keyStorePath; + _keyStorePassword = tlsConfig._keyStorePassword; + _trustStoreType = tlsConfig._trustStoreType; + _trustStorePath = tlsConfig._trustStorePath; + _trustStorePassword = tlsConfig._trustStorePassword; + _sslProvider = tlsConfig._sslProvider; + } public boolean isClientAuthEnabled() { return _clientAuthEnabled; diff --git a/pinot-core/src/main/java/org/apache/pinot/core/util/ListenerConfigUtil.java b/pinot-core/src/main/java/org/apache/pinot/core/util/ListenerConfigUtil.java index 0211c29..0fd1143 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/util/ListenerConfigUtil.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/util/ListenerConfigUtil.java @@ -19,7 +19,14 @@ package org.apache.pinot.core.util; import com.google.common.base.Preconditions; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; +import java.net.URL; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -28,6 +35,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.pinot.core.transport.ListenerConfig; import org.apache.pinot.core.transport.TlsConfig; @@ -76,9 +84,7 @@ public final class ListenerConfigUtil { String[] protocols = config.getProperty(namespace + DOT_ACCESS_PROTOCOLS).split(","); - return Arrays.stream(protocols).peek(protocol -> Preconditions - .checkArgument(SUPPORTED_PROTOCOLS.contains(protocol), "Unsupported protocol '%s' in config namespace '%s'", - protocol, namespace)).map(protocol -> buildListenerConfig(config, namespace, protocol, tlsDefaults)) + return Arrays.stream(protocols).map(protocol -> buildListenerConfig(config, namespace, protocol, tlsDefaults)) .collect(Collectors.toList()); } @@ -167,24 +173,35 @@ public final class ListenerConfigUtil { return listeners; } - private static ListenerConfig buildListenerConfig(PinotConfiguration config, String namespace, String protocol, + private static ListenerConfig buildListenerConfig(PinotConfiguration config, String namespace, String name, TlsConfig tlsConfig) { - String protocolNamespace = namespace + DOT_ACCESS_PROTOCOLS + "." + protocol; + String protocolNamespace = namespace + DOT_ACCESS_PROTOCOLS + "." + name; - return new ListenerConfig(protocol, getHost(config.getProperty(protocolNamespace + ".host", DEFAULT_HOST)), - getPort(config.getProperty(protocolNamespace + ".port")), protocol, tlsConfig); + return new ListenerConfig(name, getHost(config.getProperty(protocolNamespace + ".host", DEFAULT_HOST)), + getPort(config.getProperty(protocolNamespace + ".port")), getProtocol(config.getProperty(protocolNamespace + ".protocol"), name), + TlsUtils.extractTlsConfig(config, namespace + ".tls", tlsConfig)); } private static String getHost(String configuredHost) { - return Optional.ofNullable(configuredHost).filter(host -> !host.trim().isEmpty()) + return Optional.ofNullable(configuredHost).map(String::trim).filter(host -> !host.isEmpty()) .orElseThrow(() -> new IllegalArgumentException(configuredHost + " is not a valid host")); } private static int getPort(String configuredPort) { - return Optional.ofNullable(configuredPort).filter(port -> !port.trim().isEmpty()).<Integer>map(Integer::valueOf) + return Optional.ofNullable(configuredPort).map(String::trim).filter(port -> !port.isEmpty()).map(Integer::valueOf) .orElseThrow(() -> new IllegalArgumentException(configuredPort + " is not a valid port")); } + private static String getProtocol(String configuredProtocol, String listenerName) { + Optional<String> optProtocol = Optional.ofNullable(configuredProtocol).map(String::trim).filter(protocol -> !protocol.isEmpty()); + if (optProtocol.isEmpty()) { + return Optional.of(listenerName).filter(SUPPORTED_PROTOCOLS::contains) + .orElseThrow(() -> new IllegalArgumentException("No protocol set for listener" + listenerName + " and '" + listenerName + "' is not a valid protocol either")); + } + return optProtocol.filter(SUPPORTED_PROTOCOLS::contains) + .orElseThrow(() -> new IllegalArgumentException(configuredProtocol + " is not a valid protocol")); + } + public static HttpServer buildHttpServer(ResourceConfig resConfig, List<ListenerConfig> listenerConfigs) { Preconditions.checkNotNull(listenerConfigs); @@ -213,23 +230,24 @@ public final class ListenerConfigUtil { if (CommonConstants.HTTPS_PROTOCOL.equals(listenerConfig.getProtocol())) { listener.setSecure(true); - listener.setSSLEngineConfig(buildSSLConfig(listenerConfig.getTlsConfig())); + listener.setSSLEngineConfig(buildSSLEngineConfigurator(listenerConfig.getTlsConfig())); } httpServer.addListener(listener); } - private static SSLEngineConfigurator buildSSLConfig(TlsConfig tlsConfig) { + private static SSLEngineConfigurator buildSSLEngineConfigurator(TlsConfig tlsConfig) { SSLContextConfigurator sslContextConfigurator = new SSLContextConfigurator(); if (tlsConfig.getKeyStorePath() != null) { Preconditions.checkNotNull(tlsConfig.getKeyStorePassword(), "key store password required"); - sslContextConfigurator.setKeyStoreFile(tlsConfig.getKeyStorePath()); + sslContextConfigurator.setKeyStoreFile(cacheInTempFile(tlsConfig.getKeyStorePath()).getAbsolutePath()); sslContextConfigurator.setKeyStorePass(tlsConfig.getKeyStorePassword()); } + if (tlsConfig.getTrustStorePath() != null) { Preconditions.checkNotNull(tlsConfig.getKeyStorePassword(), "trust store password required"); - sslContextConfigurator.setTrustStoreFile(tlsConfig.getTrustStorePath()); + sslContextConfigurator.setTrustStoreFile(cacheInTempFile(tlsConfig.getTrustStorePath()).getAbsolutePath()); sslContextConfigurator.setTrustStorePass(tlsConfig.getTrustStorePassword()); } @@ -242,4 +260,19 @@ public final class ListenerConfigUtil { .map(listener -> String.format("%s://%s:%d", listener.getProtocol(), listener.getHost(), listener.getPort())) .toArray(), ", "); } + + private static File cacheInTempFile(String sourceUrl) { + try { + File tempFile = Files.createTempFile("keystore", "cache").toFile(); + tempFile.deleteOnExit(); + + try (InputStream is = TlsUtils.makeKeyStoreUrl(sourceUrl).openStream(); OutputStream os = new FileOutputStream(tempFile)) { + IOUtils.copy(is, os); + } + + return tempFile; + } catch (Exception e) { + throw new IllegalStateException(String.format("Could not retrieve and cache keystore from '%s'", sourceUrl), e); + } + } } diff --git a/pinot-core/src/main/java/org/apache/pinot/core/util/TlsUtils.java b/pinot-core/src/main/java/org/apache/pinot/core/util/TlsUtils.java index a004404..5bf3395 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/util/TlsUtils.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/util/TlsUtils.java @@ -19,14 +19,16 @@ package org.apache.pinot.core.util; import com.google.common.base.Preconditions; -import io.netty.handler.ssl.SslProvider; -import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.Socket; import java.net.SocketAddress; -import java.net.UnknownHostException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import javax.net.ssl.HttpsURLConnection; @@ -36,10 +38,10 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import org.apache.commons.httpclient.ConnectTimeoutException; import org.apache.commons.httpclient.params.HttpConnectionParams; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.lang.StringUtils; import org.apache.pinot.common.utils.FileUploadDownloadClient; import org.apache.pinot.core.transport.TlsConfig; import org.apache.pinot.spi.env.PinotConfiguration; @@ -72,31 +74,28 @@ public final class TlsUtils { * @return TlsConfig instance */ public static TlsConfig extractTlsConfig(PinotConfiguration pinotConfig, String namespace) { - TlsConfig tlsConfig = new TlsConfig(); - - tlsConfig.setClientAuthEnabled(pinotConfig.getProperty(key(namespace, CLIENT_AUTH_ENABLED), false)); - - tlsConfig.setKeyStoreType(pinotConfig.getProperty(key(namespace, KEYSTORE_TYPE), KeyStore.getDefaultType())); - - if (pinotConfig.containsKey(key(namespace, KEYSTORE_PATH))) { - tlsConfig.setKeyStorePath(pinotConfig.getProperty(key(namespace, KEYSTORE_PATH))); - } - - if (pinotConfig.containsKey(key(namespace, KEYSTORE_PASSWORD))) { - tlsConfig.setKeyStorePassword(pinotConfig.getProperty(key(namespace, KEYSTORE_PASSWORD))); - } - - tlsConfig.setTrustStoreType(pinotConfig.getProperty(key(namespace, TRUSTSTORE_TYPE), KeyStore.getDefaultType())); - - if (pinotConfig.containsKey(key(namespace, TRUSTSTORE_PATH))) { - tlsConfig.setTrustStorePath(pinotConfig.getProperty(key(namespace, TRUSTSTORE_PATH))); - } - - if (pinotConfig.containsKey(key(namespace, TRUSTSTORE_PASSWORD))) { - tlsConfig.setTrustStorePassword(pinotConfig.getProperty(key(namespace, TRUSTSTORE_PASSWORD))); - } + return extractTlsConfig(pinotConfig, namespace, new TlsConfig()); + } - tlsConfig.setSslProvider(pinotConfig.getProperty(key(namespace, SSL_PROVIDER), SslProvider.JDK.toString())); + /** + * Extract a TlsConfig instance from a namespaced set of configuration keys, based on a default config + * + * @param pinotConfig pinot configuration + * @param namespace namespace prefix + * @param defaultConfig TLS config defaults + * + * @return TlsConfig instance + */ + public static TlsConfig extractTlsConfig(PinotConfiguration pinotConfig, String namespace, TlsConfig defaultConfig) { + TlsConfig tlsConfig = new TlsConfig(defaultConfig); + tlsConfig.setClientAuthEnabled(pinotConfig.getProperty(key(namespace, CLIENT_AUTH_ENABLED), defaultConfig.isClientAuthEnabled())); + tlsConfig.setKeyStoreType(pinotConfig.getProperty(key(namespace, KEYSTORE_TYPE), defaultConfig.getKeyStoreType())); + tlsConfig.setKeyStorePath(pinotConfig.getProperty(key(namespace, KEYSTORE_PATH), defaultConfig.getKeyStorePath())); + tlsConfig.setKeyStorePassword(pinotConfig.getProperty(key(namespace, KEYSTORE_PASSWORD), defaultConfig.getKeyStorePassword())); + tlsConfig.setTrustStoreType(pinotConfig.getProperty(key(namespace, TRUSTSTORE_TYPE), defaultConfig.getTrustStoreType())); + tlsConfig.setTrustStorePath(pinotConfig.getProperty(key(namespace, TRUSTSTORE_PATH), defaultConfig.getTrustStorePath())); + tlsConfig.setTrustStorePassword(pinotConfig.getProperty(key(namespace, TRUSTSTORE_PASSWORD), defaultConfig.getTrustStorePassword())); + tlsConfig.setSslProvider(pinotConfig.getProperty(key(namespace, SSL_PROVIDER), defaultConfig.getSslProvider())); return tlsConfig; } @@ -128,7 +127,7 @@ public final class TlsUtils { try { KeyStore keyStore = KeyStore.getInstance(keyStoreType); - try (FileInputStream is = new FileInputStream(keyStorePath)) { + try (InputStream is = makeKeyStoreUrl(keyStorePath).openStream()) { keyStore.load(is, keyStorePassword.toCharArray()); } @@ -168,7 +167,7 @@ public final class TlsUtils { try { KeyStore keyStore = KeyStore.getInstance(trustStoreType); - try (FileInputStream is = new FileInputStream(trustStorePath)) { + try (InputStream is = makeKeyStoreUrl(trustStorePath).openStream()) { keyStore.load(is, trustStorePassword.toCharArray()); } @@ -237,6 +236,18 @@ public final class TlsUtils { return namespace + "." + suffix; } + public static URL makeKeyStoreUrl(String storePath) + throws URISyntaxException, MalformedURLException { + URI inputUri = new URI(storePath); + if (StringUtils.isBlank(inputUri.getScheme())) { + if (storePath.startsWith("/")) { + return new URL("file://" + storePath); + } + return new URL("file://./" + storePath); + } + return inputUri.toURL(); + } + /** * Adapted from: https://svn.apache.org/viewvc/httpcomponents/oac * .hc3x/trunk/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLProtocolSocketFactory.java?view=markup @@ -250,14 +261,14 @@ public final class TlsUtils { @Override public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) - throws IOException, UnknownHostException { + throws IOException { return _sslSocketFactory.createSocket(host, port, localAddress, localPort); } @Override public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params) - throws IOException, UnknownHostException, ConnectTimeoutException { + throws IOException { Preconditions.checkNotNull(params); int timeout = params.getConnectionTimeout(); @@ -275,7 +286,7 @@ public final class TlsUtils { @Override public Socket createSocket(String host, int port) - throws IOException, UnknownHostException { + throws IOException { return _sslSocketFactory.createSocket(host, port); } } diff --git a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BasicAuthTlsRealtimeIntegrationTest.java b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BasicAuthTlsRealtimeIntegrationTest.java index 003a1d2..354c55a 100644 --- a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BasicAuthTlsRealtimeIntegrationTest.java +++ b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/BasicAuthTlsRealtimeIntegrationTest.java @@ -19,20 +19,16 @@ package org.apache.pinot.integration.tests; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Preconditions; import groovy.lang.IntRange; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; import org.apache.pinot.client.Connection; import org.apache.pinot.client.ConnectionFactory; import org.apache.pinot.client.JsonAsyncHttpPinotClientTransportFactory; @@ -62,17 +58,12 @@ import static org.apache.pinot.integration.tests.BasicAuthTestUtils.AUTH_HEADER; * to go, you can manually run BasicAuthRealtimeIntegrationTest which tests the auth aspect only. */ public class BasicAuthTlsRealtimeIntegrationTest extends BaseClusterIntegrationTest { - private final File _tempDirTls = new File(FileUtils.getTempDirectory(), getClass().getSimpleName() + "-cert"); - private final File _tlsStore = _tempDirTls.toPath().resolve("tlsstore.p12").toFile(); - private final File _tlsStoreJKS = _tempDirTls.toPath().resolve("tlsstore.jks").toFile(); + private final URL _tlsStorePKCS12 = TlsIntegrationTest.class.getResource("/tlstest.p12"); @BeforeClass public void setUp() throws Exception { TestUtils.ensureDirectoriesExistAndEmpty(_tempDir); - TestUtils.ensureDirectoriesExistAndEmpty(_tempDirTls); - - prepareTlsStore(); // Start Zookeeper startZk(); @@ -107,34 +98,38 @@ public class BasicAuthTlsRealtimeIntegrationTest extends BaseClusterIntegrationT stopKafka(); stopZk(); FileUtils.deleteDirectory(_tempDir); - FileUtils.deleteDirectory(_tempDirTls); } @Override public Map<String, Object> getDefaultControllerConfiguration() { Map<String, Object> prop = super.getDefaultControllerConfiguration(); - prop.put("controller.tls.keystore.path", _tlsStore.getAbsolutePath()); + prop.put("controller.tls.keystore.path", _tlsStorePKCS12); prop.put("controller.tls.keystore.password", "changeit"); - prop.put("controller.tls.truststore.path", _tlsStore.getAbsolutePath()); + prop.put("controller.tls.keystore.type", "PKCS12"); + prop.put("controller.tls.truststore.path", _tlsStorePKCS12); prop.put("controller.tls.truststore.password", "changeit"); + prop.put("controller.tls.truststore.type", "PKCS12"); - prop.remove("controller.port"); prop.put("controller.access.protocols", "https"); prop.put("controller.access.protocols.https.port", DEFAULT_CONTROLLER_PORT); prop.put("controller.broker.protocol", "https"); prop.put("controller.vip.protocol", "https"); prop.put("controller.vip.port", DEFAULT_CONTROLLER_PORT); + prop.remove("controller.port"); + return BasicAuthTestUtils.addControllerConfiguration(prop); } @Override protected PinotConfiguration getDefaultBrokerConfiguration() { Map<String, Object> prop = super.getDefaultBrokerConfiguration().toMap(); - prop.put("pinot.broker.tls.keystore.path", _tlsStore.getAbsolutePath()); + prop.put("pinot.broker.tls.keystore.path", _tlsStorePKCS12); prop.put("pinot.broker.tls.keystore.password", "changeit"); - prop.put("pinot.broker.tls.truststore.path", _tlsStore.getAbsolutePath()); + prop.put("pinot.server.tls.keystore.type", "PKCS12"); + prop.put("pinot.broker.tls.truststore.path", _tlsStorePKCS12); prop.put("pinot.broker.tls.truststore.password", "changeit"); + prop.put("pinot.broker.tls.truststore.type", "PKCS12"); prop.put("pinot.broker.client.access.protocols", "https"); prop.put("pinot.broker.client.access.protocols.https.port", DEFAULT_BROKER_PORT); @@ -146,15 +141,16 @@ public class BasicAuthTlsRealtimeIntegrationTest extends BaseClusterIntegrationT @Override protected PinotConfiguration getDefaultServerConfiguration() { Map<String, Object> prop = super.getDefaultServerConfiguration().toMap(); - prop.put("pinot.server.tls.keystore.path", _tlsStoreJKS.getAbsolutePath()); + prop.put("pinot.server.tls.keystore.path", _tlsStorePKCS12); prop.put("pinot.server.tls.keystore.password", "changeit"); prop.put("pinot.server.tls.keystore.type", "PKCS12"); - prop.put("pinot.server.tls.truststore.path", _tlsStore.getAbsolutePath()); + prop.put("pinot.server.tls.truststore.path", _tlsStorePKCS12); prop.put("pinot.server.tls.truststore.password", "changeit"); - prop.put("pinot.server.admin.access.control.factory.class", - CertBasedTlsChannelAccessControlFactory.class.getName()); + prop.put("pinot.server.tls.truststore.type", "PKCS12"); prop.put("pinot.server.tls.client.auth.enabled", "true"); + prop.put("pinot.server.admin.access.control.factory.class", + CertBasedTlsChannelAccessControlFactory.class.getName()); prop.put("pinot.server.adminapi.access.protocols", "https"); prop.put("pinot.server.adminapi.access.protocols.https.port", "7443"); prop.put("pinot.server.netty.enabled", "false"); @@ -168,10 +164,12 @@ public class BasicAuthTlsRealtimeIntegrationTest extends BaseClusterIntegrationT @Override protected PinotConfiguration getDefaultMinionConfiguration() { Map<String, Object> prop = super.getDefaultMinionConfiguration().toMap(); - prop.put("pinot.minion.tls.keystore.path", _tlsStore.getAbsolutePath()); + prop.put("pinot.minion.tls.keystore.path", _tlsStorePKCS12); prop.put("pinot.minion.tls.keystore.password", "changeit"); - prop.put("pinot.minion.tls.truststore.path", _tlsStore.getAbsolutePath()); + prop.put("pinot.server.tls.keystore.type", "PKCS12"); + prop.put("pinot.minion.tls.truststore.path", _tlsStorePKCS12); prop.put("pinot.minion.tls.truststore.password", "changeit"); + prop.put("pinot.server.tls.truststore.type", "PKCS12"); return BasicAuthTestUtils.addMinionConfiguration(prop); } @@ -260,43 +258,4 @@ public class BasicAuthTlsRealtimeIntegrationTest extends BaseClusterIntegrationT > 200000); // download segment } } - - void prepareTlsStore() - throws Exception { - try (OutputStream os = new FileOutputStream(_tlsStore); - /* - * Command to generate the tlstest.jks file (generate key pairs for both IPV4 and IPV6 addresses): - * ``` - * keytool -genkeypair -keystore tlstest.jks -dname "CN=test, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, \ - * C=Unknown" -keypass changeit -storepass changeit -keyalg RSA -alias localhost-ipv4 -ext \ - * SAN=dns:localhost,ip:127.0.0.1 - * - * keytool -genkeypair -keystore tlstest.jks -dname "CN=test, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, \ - * C=Unknown" -keypass changeit -storepass changeit -keyalg RSA -alias localhost-ipv6 -ext \ - * SAN=dns:localhost,ip:0:0:0:0:0:0:0:1 - * ``` - */ - InputStream is = getClass().getResourceAsStream("/tlstest.p12")) { - Preconditions.checkNotNull(is, "tlstest.p12 must be on the classpath"); - IOUtils.copy(is, os); - } - - try (OutputStream osJKS = new FileOutputStream(_tlsStoreJKS); - /* - * Command to generate the tlstest.pkcs file (generate key pairs for both IPV4 and IPV6 addresses): - * ``` - * keytool -genkeypair -storetype JKS -keystore tlstest.p12 -dname "CN=test, OU=Unknown, O=Unknown, \ - * L=Unknown, ST=Unknown, C=Unknown" -keypass changeit -storepass changeit -keyalg RSA \ - * -alias localhost-ipv4 -ext SAN=dns:localhost,ip:127.0.0.1 - * - * keytool -genkeypair -storetype JKS -keystore tlstest.p12 -dname "CN=test, OU=Unknown, O=Unknown, \ - * L=Unknown, ST=Unknown, C=Unknown" -keypass changeit -storepass changeit -keyalg RSA \ - * -alias localhost-ipv6 -ext SAN=dns:localhost,ip:0:0:0:0:0:0:0:1 - * ``` - */ - InputStream isJKS = getClass().getResourceAsStream("/tlstest.jks")) { - Preconditions.checkNotNull(isJKS, "tlstest.jks must be on the classpath"); - IOUtils.copy(isJKS, osJKS); - } - } } diff --git a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/TlsIntegrationTest.java b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/TlsIntegrationTest.java new file mode 100644 index 0000000..a6e3c5c --- /dev/null +++ b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/TlsIntegrationTest.java @@ -0,0 +1,354 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.integration.tests; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHeader; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.pinot.client.Connection; +import org.apache.pinot.client.ConnectionFactory; +import org.apache.pinot.client.JsonAsyncHttpPinotClientTransportFactory; +import org.apache.pinot.common.utils.FileUploadDownloadClient; +import org.apache.pinot.integration.tests.access.CertBasedTlsChannelAccessControlFactory; +import org.apache.pinot.spi.config.table.TableConfig; +import org.apache.pinot.spi.data.Schema; +import org.apache.pinot.spi.env.PinotConfiguration; +import org.apache.pinot.spi.utils.CommonConstants; +import org.apache.pinot.spi.utils.builder.TableNameBuilder; +import org.apache.pinot.util.TestUtils; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.apache.pinot.integration.tests.BasicAuthTestUtils.AUTH_HEADER; +import static org.apache.pinot.integration.tests.BasicAuthTestUtils.AUTH_TOKEN; + + +public class TlsIntegrationTest extends BaseClusterIntegrationTest { + private static final String PASSWORD = "changeit"; + private static final char[] PASSWORD_CHAR = PASSWORD.toCharArray(); + private static final Header CLIENT_HEADER = new BasicHeader("Authorization", AUTH_TOKEN); + + private static final int INTERNAL_CONTROLLER_PORT = DEFAULT_CONTROLLER_PORT + 1; + private static final int INTERNAL_BROKER_PORT = DEFAULT_BROKER_PORT + 1; + private static final String PKCS_12 = "PKCS12"; + private static final String JKS = "JKS"; + + private final URL _tlsStoreEmptyPKCS12 = TlsIntegrationTest.class.getResource("/empty.p12"); + private final URL _tlsStoreEmptyJKS = TlsIntegrationTest.class.getResource("/empty.jks"); + private final URL _tlsStorePKCS12 = TlsIntegrationTest.class.getResource("/tlstest.p12"); + private final URL _tlsStoreJKS = TlsIntegrationTest.class.getResource("/tlstest.jks"); + + @BeforeClass + public void setUp() + throws Exception { + TestUtils.ensureDirectoriesExistAndEmpty(_tempDir); + + // Start Zookeeper + startZk(); + // Start Pinot cluster + startKafka(); + startController(); + startBrokerHttps(); + startServerHttps(); + + // Unpack the Avro files + List<File> avroFiles = unpackAvroData(_tempDir); + + // Create and upload the schema and table config + addSchema(createSchema()); + addTableConfig(createRealtimeTableConfig(avroFiles.get(0))); + addTableConfig(createOfflineTableConfig()); + + // Push data into Kafka + pushAvroIntoKafka(avroFiles); + waitForAllDocsLoaded(600_000L); + + System.out.println("hello world!"); + + Thread.sleep(600000); + } + + @AfterClass(alwaysRun = true) + public void tearDown() + throws Exception { + dropRealtimeTable(getTableName()); + stopServer(); + stopBroker(); + stopController(); + stopKafka(); + stopZk(); + FileUtils.deleteDirectory(_tempDir); + } + + @Override + public Map<String, Object> getDefaultControllerConfiguration() { + Map<String, Object> prop = super.getDefaultControllerConfiguration(); + prop.put("controller.tls.keystore.path", _tlsStorePKCS12); + prop.put("controller.tls.keystore.password", PASSWORD); + prop.put("controller.tls.keystore.type", PKCS_12); + prop.put("controller.tls.truststore.path", _tlsStorePKCS12); + prop.put("controller.tls.truststore.password", PASSWORD); + prop.put("controller.tls.truststore.type", PKCS_12); + +// prop.put("controller.access.protocols", "https"); +// prop.put("controller.access.protocols.https.port", DEFAULT_CONTROLLER_PORT); + prop.put("controller.access.protocols", "https,internal"); + prop.put("controller.access.protocols.https.port", DEFAULT_CONTROLLER_PORT); + prop.put("controller.access.protocols.https.tls.keystore.path", _tlsStoreJKS); + prop.put("controller.access.protocols.https.tls.keystore.type", JKS); + prop.put("controller.access.protocols.https.tls.truststore.path", _tlsStoreJKS); + prop.put("controller.access.protocols.https.tls.truststore.type", JKS); + prop.put("controller.access.protocols.internal.protocol", "https"); + prop.put("controller.access.protocols.internal.port", INTERNAL_CONTROLLER_PORT); + prop.put("controller.access.protocols.internal.tls.client.auth.enabled", "true"); + + prop.put("controller.broker.protocol", "https"); + prop.put("controller.broker.port.override", INTERNAL_BROKER_PORT); + + // announce external only + prop.put("controller.vip.protocol", "https"); + prop.put("controller.vip.port", DEFAULT_CONTROLLER_PORT); + + prop.remove("controller.port"); + + return BasicAuthTestUtils.addControllerConfiguration(prop); + } + + @Override + protected PinotConfiguration getDefaultBrokerConfiguration() { + Map<String, Object> prop = super.getDefaultBrokerConfiguration().toMap(); + prop.put("pinot.broker.tls.keystore.path", _tlsStorePKCS12); + prop.put("pinot.broker.tls.keystore.password", PASSWORD); + prop.put("pinot.broker.tls.keystore.type", PKCS_12); + prop.put("pinot.broker.tls.truststore.path", _tlsStorePKCS12); + prop.put("pinot.broker.tls.truststore.password", PASSWORD); + prop.put("pinot.broker.tls.truststore.type", PKCS_12); + +// prop.put("pinot.broker.client.access.protocols", "https"); +// prop.put("pinot.broker.client.access.protocols.https.port", DEFAULT_BROKER_PORT); + prop.put("pinot.broker.client.access.protocols", "https,internal"); + prop.put("pinot.broker.client.access.protocols.https.port", DEFAULT_BROKER_PORT); + prop.put("pinot.broker.client.access.protocols.https.tls.keystore.path", _tlsStoreJKS); + prop.put("pinot.broker.client.access.protocols.https.tls.keystore.type", JKS); + prop.put("pinot.broker.client.access.protocols.https.tls.truststore.path", _tlsStoreJKS); + prop.put("pinot.broker.client.access.protocols.https.tls.truststore.type", JKS); + prop.put("pinot.broker.client.access.protocols.internal.protocol", "https"); + prop.put("pinot.broker.client.access.protocols.internal.port", INTERNAL_BROKER_PORT); + prop.put("pinot.broker.client.access.protocols.internal.tls.client.auth.enabled", "true"); + + prop.put("pinot.broker.nettytls.enabled", "true"); + + return BasicAuthTestUtils.addBrokerConfiguration(prop); + } + + @Override + protected PinotConfiguration getDefaultServerConfiguration() { + Map<String, Object> prop = super.getDefaultServerConfiguration().toMap(); + prop.put("pinot.server.tls.keystore.path", _tlsStorePKCS12); + prop.put("pinot.server.tls.keystore.password", PASSWORD); + prop.put("pinot.server.tls.keystore.type", PKCS_12); + prop.put("pinot.server.tls.truststore.path", _tlsStorePKCS12); + prop.put("pinot.server.tls.truststore.password", PASSWORD); + prop.put("pinot.server.tls.truststore.type", PKCS_12); + prop.put("pinot.server.tls.client.auth.enabled", "true"); + + prop.put("pinot.server.admin.access.control.factory.class", + CertBasedTlsChannelAccessControlFactory.class.getName()); +// prop.put("pinot.server.adminapi.access.protocols", "https"); +// prop.put("pinot.server.adminapi.access.protocols.https.port", "7443"); + prop.put("pinot.server.adminapi.access.protocols", "internal"); + prop.put("pinot.server.adminapi.access.protocols.internal.protocol", "https"); + prop.put("pinot.server.adminapi.access.protocols.internal.port", "7443"); + prop.put("pinot.server.netty.enabled", "false"); + prop.put("pinot.server.nettytls.enabled", "true"); + prop.put("pinot.server.nettytls.port", "8089"); + prop.put("pinot.server.segment.uploader.protocol", "https"); + + return BasicAuthTestUtils.addServerConfiguration(prop); + } + + @Override + protected boolean useLlc() { + return true; + } + + @Override + protected void addSchema(Schema schema) + throws IOException { + PostMethod response = + sendMultipartPostRequest(_controllerRequestURLBuilder.forSchemaCreate(), schema.toSingleLineJsonString(), + AUTH_HEADER); + Assert.assertEquals(response.getStatusCode(), 200); + } + + @Override + protected void addTableConfig(TableConfig tableConfig) + throws IOException { + sendPostRequest(_controllerRequestURLBuilder.forTableCreate(), tableConfig.toJsonString(), AUTH_HEADER); + } + + @Override + protected Connection getPinotConnection() { + if (_pinotConnection == null) { + JsonAsyncHttpPinotClientTransportFactory factory = new JsonAsyncHttpPinotClientTransportFactory(); + factory.setHeaders(AUTH_HEADER); + factory.setScheme(CommonConstants.HTTPS_PROTOCOL); + factory.setSslContext(FileUploadDownloadClient._defaultSSLContext); + + _pinotConnection = + ConnectionFactory.fromZookeeper(getZkUrl() + "/" + getHelixClusterName(), factory.buildTransport()); + } + return _pinotConnection; + } + + @Override + protected void dropRealtimeTable(String tableName) + throws IOException { + sendDeleteRequest( + _controllerRequestURLBuilder.forTableDelete(TableNameBuilder.REALTIME.tableNameWithType(tableName)), + AUTH_HEADER); + } + + @Test + public void testQueryControllerExternalTrustedServer() + throws Exception { + try (CloseableHttpClient client = makeClient(JKS, _tlsStoreJKS, _tlsStoreJKS)) { + HttpUriRequest request = new HttpGet("https://localhost:" + DEFAULT_CONTROLLER_PORT + "/tables"); + request.addHeader(CLIENT_HEADER); + + try (CloseableHttpResponse response = client.execute(request)) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), 200); + String output = IOUtils.toString(response.getEntity().getContent()); + System.out.println(output); + } + } + } + + @Test + public void testQueryControllerExternalUntrustedServer() + throws Exception { + try (CloseableHttpClient client = makeClient(JKS, _tlsStoreJKS, _tlsStoreEmptyJKS)) { + HttpUriRequest request = new HttpGet("https://localhost:" + DEFAULT_CONTROLLER_PORT + "/tables"); + request.addHeader(CLIENT_HEADER); + + try { + client.execute(request); + Assert.fail("Must not allow connection to untrusted server"); + } catch (IOException ignore) { + // this should fail + } + } + } + + @Test + public void testQueryControllerInternalTrustedClient() + throws Exception { + try (CloseableHttpClient client = makeClient(PKCS_12, _tlsStorePKCS12, _tlsStorePKCS12)) { + HttpUriRequest request = new HttpGet("https://localhost:" + INTERNAL_CONTROLLER_PORT + "/tables"); + request.addHeader(CLIENT_HEADER); + + try (CloseableHttpResponse response = client.execute(request)) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), 200); + String output = IOUtils.toString(response.getEntity().getContent()); + System.out.println(output); + } + } + } + + @Test + public void testQueryControllerInternalUntrustedClient() + throws Exception { + try (CloseableHttpClient client = makeClient(PKCS_12, _tlsStoreEmptyPKCS12, _tlsStorePKCS12)) { + HttpUriRequest request = new HttpGet("https://localhost:" + INTERNAL_CONTROLLER_PORT + "/tables"); + request.addHeader(CLIENT_HEADER); + + try { + client.execute(request); + Assert.fail("Must not allow connection from untrusted client"); + } catch (IOException ignore) { + // this should fail + } + } + } + + @Test + public void testQueryBrokerExternal() + throws Exception { + Assert.fail("not implemented yet"); + } + + @Test + public void testQueryBrokerInternal() + throws Exception { + Assert.fail("not implemented yet"); + } + + private static CloseableHttpClient makeClient(String keyStoreType, URL keyStoreUrl, URL trustStoreUrl) { + try { + SSLContextBuilder sslContextBuilder = SSLContextBuilder.create(); + sslContextBuilder.setKeyStoreType(keyStoreType); + sslContextBuilder.loadKeyMaterial(keyStoreUrl, PASSWORD_CHAR, PASSWORD_CHAR); + sslContextBuilder.loadTrustMaterial(trustStoreUrl, PASSWORD_CHAR); + return HttpClientBuilder.create().setSSLContext(sslContextBuilder.build()).build(); + } catch (Exception e) { + throw new IllegalStateException("Could not create HTTPS client"); + } + } + + /* + * Command to generate the tlstest.jks file (generate key pairs for both IPV4 and IPV6 addresses): + * ``` + * keytool -genkeypair -keystore tlstest.jks -dname "CN=test, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, \ + * C=Unknown" -keypass changeit -storepass changeit -keyalg RSA -alias localhost-ipv4 -ext \ + * SAN=dns:localhost,ip:127.0.0.1 + * + * keytool -genkeypair -keystore tlstest.jks -dname "CN=test, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, \ + * C=Unknown" -keypass changeit -storepass changeit -keyalg RSA -alias localhost-ipv6 -ext \ + * SAN=dns:localhost,ip:0:0:0:0:0:0:0:1 + * ``` + */ + + /* + * Command to generate the tlstest.pkcs file (generate key pairs for both IPV4 and IPV6 addresses): + * ``` + * keytool -genkeypair -storetype JKS -keystore tlstest.p12 -dname "CN=test, OU=Unknown, O=Unknown, \ + * L=Unknown, ST=Unknown, C=Unknown" -keypass changeit -storepass changeit -keyalg RSA \ + * -alias localhost-ipv4 -ext SAN=dns:localhost,ip:127.0.0.1 + * + * keytool -genkeypair -storetype JKS -keystore tlstest.p12 -dname "CN=test, OU=Unknown, O=Unknown, \ + * L=Unknown, ST=Unknown, C=Unknown" -keypass changeit -storepass changeit -keyalg RSA \ + * -alias localhost-ipv6 -ext SAN=dns:localhost,ip:0:0:0:0:0:0:0:1 + * ``` + */ +} diff --git a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/access/CertBasedTlsChannelAccessControlFactory.java b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/access/CertBasedTlsChannelAccessControlFactory.java index 99f9501..dd65e77 100644 --- a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/access/CertBasedTlsChannelAccessControlFactory.java +++ b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/access/CertBasedTlsChannelAccessControlFactory.java @@ -46,7 +46,8 @@ public class CertBasedTlsChannelAccessControlFactory implements AccessControlFac private final Logger _logger = LoggerFactory.getLogger(CertBasedTlsChannelAccessControl.class); private final Set<String> _aclPrincipalAllowlist = new HashSet<String>() {{ - add("CN=test, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"); + add("CN=test-jks, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"); + add("CN=test-p12, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"); }}; @Override diff --git a/pinot-integration-tests/src/test/resources/empty.jks b/pinot-integration-tests/src/test/resources/empty.jks new file mode 100644 index 0000000..c408465 Binary files /dev/null and b/pinot-integration-tests/src/test/resources/empty.jks differ diff --git a/pinot-integration-tests/src/test/resources/empty.p12 b/pinot-integration-tests/src/test/resources/empty.p12 new file mode 100644 index 0000000..4f5baf1 Binary files /dev/null and b/pinot-integration-tests/src/test/resources/empty.p12 differ diff --git a/pinot-integration-tests/src/test/resources/tlstest.jks b/pinot-integration-tests/src/test/resources/tlstest.jks index 12f28e2..a43845f 100644 Binary files a/pinot-integration-tests/src/test/resources/tlstest.jks and b/pinot-integration-tests/src/test/resources/tlstest.jks differ diff --git a/pinot-integration-tests/src/test/resources/tlstest.p12 b/pinot-integration-tests/src/test/resources/tlstest.p12 index 7750790..23c49e4 100644 Binary files a/pinot-integration-tests/src/test/resources/tlstest.p12 and b/pinot-integration-tests/src/test/resources/tlstest.p12 differ --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org