Skip to content

Commit 6c5d200

Browse files
TLS: allow protocol allowlist; harden JVM SSL init (#17776)
* Made TLS configurable * Made TLS configurable * Address PR feedback: trim protocols, restore diagnostics, add tests * Address PR feedback: trim protocols, restore diagnostics, add tests * Address PR feedback: trim protocols, restore diagnostics, add tests --------- Co-authored-by: Shivam Phutela <shvmphutela05@gmail.com>
1 parent 337faad commit 6c5d200

File tree

7 files changed

+234
-10
lines changed

7 files changed

+234
-10
lines changed

pinot-common/src/main/java/org/apache/pinot/common/config/TlsConfig.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
import io.netty.handler.ssl.SslProvider;
2222
import java.security.KeyStore;
23+
import java.util.Arrays;
2324
import java.util.Objects;
25+
import javax.annotation.Nullable;
2426
import org.apache.commons.lang3.StringUtils;
2527

2628

@@ -38,6 +40,9 @@ public class TlsConfig {
3840
private String _sslProvider = SslProvider.JDK.toString();
3941
// If true, the client will not verify the server's certificate
4042
private boolean _insecure = false;
43+
// Allowed TLS protocols (optional, if not set uses default)
44+
@Nullable
45+
private String[] _allowedProtocols = null;
4146

4247
public TlsConfig() {
4348
// left blank
@@ -52,6 +57,10 @@ public TlsConfig(TlsConfig tlsConfig) {
5257
_trustStorePath = tlsConfig._trustStorePath;
5358
_trustStorePassword = tlsConfig._trustStorePassword;
5459
_sslProvider = tlsConfig._sslProvider;
60+
_insecure = tlsConfig._insecure;
61+
_allowedProtocols = tlsConfig._allowedProtocols != null
62+
? Arrays.copyOf(tlsConfig._allowedProtocols, tlsConfig._allowedProtocols.length)
63+
: null;
5564
}
5665

5766
public boolean isClientAuthEnabled() {
@@ -130,6 +139,17 @@ public void setInsecure(boolean insecure) {
130139
_insecure = insecure;
131140
}
132141

142+
@Nullable
143+
public String[] getAllowedProtocols() {
144+
return _allowedProtocols != null ? Arrays.copyOf(_allowedProtocols, _allowedProtocols.length) : null;
145+
}
146+
147+
public void setAllowedProtocols(@Nullable String[] allowedProtocols) {
148+
_allowedProtocols = allowedProtocols != null
149+
? Arrays.copyOf(allowedProtocols, allowedProtocols.length)
150+
: null;
151+
}
152+
133153
@Override
134154
public boolean equals(Object o) {
135155
if (this == o) {
@@ -139,16 +159,23 @@ public boolean equals(Object o) {
139159
return false;
140160
}
141161
TlsConfig tlsConfig = (TlsConfig) o;
142-
return _clientAuthEnabled == tlsConfig._clientAuthEnabled && _insecure == tlsConfig._insecure && Objects.equals(
143-
_keyStoreType, tlsConfig._keyStoreType) && Objects.equals(_keyStorePath, tlsConfig._keyStorePath)
144-
&& Objects.equals(_keyStorePassword, tlsConfig._keyStorePassword) && Objects.equals(_trustStoreType,
145-
tlsConfig._trustStoreType) && Objects.equals(_trustStorePath, tlsConfig._trustStorePath) && Objects.equals(
146-
_trustStorePassword, tlsConfig._trustStorePassword) && Objects.equals(_sslProvider, tlsConfig._sslProvider);
162+
return _clientAuthEnabled == tlsConfig._clientAuthEnabled
163+
&& _insecure == tlsConfig._insecure
164+
&& Objects.equals(_keyStoreType, tlsConfig._keyStoreType)
165+
&& Objects.equals(_keyStorePath, tlsConfig._keyStorePath)
166+
&& Objects.equals(_keyStorePassword, tlsConfig._keyStorePassword)
167+
&& Objects.equals(_trustStoreType, tlsConfig._trustStoreType)
168+
&& Objects.equals(_trustStorePath, tlsConfig._trustStorePath)
169+
&& Objects.equals(_trustStorePassword, tlsConfig._trustStorePassword)
170+
&& Objects.equals(_sslProvider, tlsConfig._sslProvider)
171+
&& Arrays.equals(_allowedProtocols, tlsConfig._allowedProtocols);
147172
}
148173

149174
@Override
150175
public int hashCode() {
151-
return Objects.hash(_clientAuthEnabled, _keyStoreType, _keyStorePath, _keyStorePassword, _trustStoreType,
176+
int result = Objects.hash(_clientAuthEnabled, _keyStoreType, _keyStorePath, _keyStorePassword, _trustStoreType,
152177
_trustStorePath, _trustStorePassword, _sslProvider, _insecure);
178+
result = 31 * result + Arrays.hashCode(_allowedProtocols);
179+
return result;
153180
}
154181
}

pinot-common/src/main/java/org/apache/pinot/common/utils/tls/JvmDefaultSslContext.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ private JvmDefaultSslContext() {
4949
* system property and they are files:
5050
* set the default SSL context to the default SSL context created by SSLFactory, and enable auto renewal of
5151
* SSLFactory when either key store or trust store file changes.
52+
*
53+
* <p>This method is called from a static block in {@code PinotAdministrator}. Failures here must not
54+
* prevent the JVM from starting — for example when security providers are not yet registered or
55+
* the keystore/truststore is temporarily unavailable. In such cases, a warning is logged and Pinot
56+
* components will configure TLS individually during their own startup.</p>
57+
*
5258
* TODO: need to support "javax.net.ssl.keyStoreProvider", "javax.net.ssl.trustStoreProvider", "https.protocols" and
5359
* "https.cipherSuites" system properties.
5460
*/
@@ -59,6 +65,21 @@ public static synchronized void initDefaultSslContext() {
5965
return;
6066
}
6167

68+
try {
69+
initDefaultSslContextInternal();
70+
} catch (Exception e) {
71+
// Do NOT rethrow — this runs in PinotAdministrator's static initializer.
72+
// Rethrowing would cause ExceptionInInitializerError and prevent the JVM from starting.
73+
// Pinot components re-initialize TLS individually during their own startup.
74+
LOGGER.warn("Failed to initialize JVM default SSL context. This is non-fatal — Pinot components "
75+
+ "will configure TLS individually during startup. trustStoreType='{}', trustStore='{}'. Error: {}",
76+
System.getProperty(JVM_TRUST_STORE_TYPE), System.getProperty(JVM_TRUST_STORE), e.getMessage());
77+
LOGGER.debug("Full stack trace for SSL context initialization failure:", e);
78+
}
79+
_initialized = true;
80+
}
81+
82+
private static void initDefaultSslContextInternal() {
6283
String jvmKeyStorePath = System.getProperty(JVM_KEY_STORE);
6384
String jvmTrustStorePath = System.getProperty(JVM_TRUST_STORE);
6485

@@ -101,8 +122,10 @@ public static synchronized void initDefaultSslContext() {
101122
.map(String::trim).filter(StringUtils::isNotBlank).orElse(null);
102123
RenewableTlsUtils.enableAutoRenewalFromFileStoreForSSLFactory(jvmSslFactory, jvmKeystoreType, jvmKeyStorePath,
103124
jvmKeystorePassword, jvmTrustStoreType, jvmTrustStorePath, jvmTrustStorePassword, null, null, () -> false);
125+
LOGGER.info("Successfully initialized JVM default SSL context");
126+
} else {
127+
LOGGER.info("No key store or trust store specified via system properties, "
128+
+ "skipping JVM default SSL context setup");
104129
}
105-
_initialized = true;
106-
LOGGER.info("Successfully initialized mvm default SSL context");
107130
}
108131
}

pinot-common/src/main/java/org/apache/pinot/common/utils/tls/TlsUtils.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public final class TlsUtils {
7272
private static final String FILE_SCHEME_PREFIX = FILE_SCHEME + "://";
7373
private static final String FILE_SCHEME_PREFIX_WITHOUT_SLASH = FILE_SCHEME + ":";
7474
private static final String INSECURE = "insecure";
75+
private static final String PROTOCOLS = "protocols";
7576

7677
private static final AtomicReference<SSLContext> SSL_CONTEXT_REF = new AtomicReference<>();
7778
private static final Set<String> LOGGED_TLS_DIAGNOSTICS_KEYS = ConcurrentHashMap.newKeySet();
@@ -122,6 +123,22 @@ public static TlsConfig extractTlsConfig(PinotConfiguration pinotConfig, String
122123
tlsConfig.setInsecure(
123124
pinotConfig.getProperty(key(namespace, INSECURE), defaultConfig.isInsecure()));
124125

126+
// Read allowed TLS protocols from config (e.g., "TLSv1.2,TLSv1.3")
127+
String protocolsConfig = pinotConfig.getProperty(key(namespace, PROTOCOLS));
128+
if (StringUtils.isNotBlank(protocolsConfig)) {
129+
String[] protocols = Arrays.stream(protocolsConfig.split(","))
130+
.map(String::trim)
131+
.filter(StringUtils::isNotBlank)
132+
.toArray(String[]::new);
133+
if (protocols.length > 0) {
134+
tlsConfig.setAllowedProtocols(protocols);
135+
} else if (defaultConfig.getAllowedProtocols() != null) {
136+
tlsConfig.setAllowedProtocols(defaultConfig.getAllowedProtocols());
137+
}
138+
} else if (defaultConfig.getAllowedProtocols() != null) {
139+
tlsConfig.setAllowedProtocols(defaultConfig.getAllowedProtocols());
140+
}
141+
125142
return tlsConfig;
126143
}
127144

@@ -326,6 +343,14 @@ public static SslContext buildClientContext(TlsConfig tlsConfig) {
326343
SslContextBuilder.forClient().sslProvider(SslProvider.valueOf(tlsConfig.getSslProvider()));
327344
sslFactory.getKeyManagerFactory().ifPresent(sslContextBuilder::keyManager);
328345
sslFactory.getTrustManagerFactory().ifPresent(sslContextBuilder::trustManager);
346+
347+
// Apply protocol restrictions if configured
348+
String[] allowedProtocols = tlsConfig.getAllowedProtocols();
349+
if (allowedProtocols != null && allowedProtocols.length > 0) {
350+
sslContextBuilder.protocols(allowedProtocols);
351+
LOGGER.debug("TLS client context restricted to protocols: {}", Arrays.toString(allowedProtocols));
352+
}
353+
329354
try {
330355
warnIfNonJdkProviderConfiguredInternal("netty.client", tlsConfig);
331356
return sslContextBuilder.build();
@@ -352,6 +377,14 @@ public static SslContext buildServerContext(TlsConfig tlsConfig) {
352377
if (tlsConfig.isClientAuthEnabled()) {
353378
sslContextBuilder.clientAuth(ClientAuth.REQUIRE);
354379
}
380+
381+
// Apply protocol restrictions if configured
382+
String[] allowedProtocols = tlsConfig.getAllowedProtocols();
383+
if (allowedProtocols != null && allowedProtocols.length > 0) {
384+
sslContextBuilder.protocols(allowedProtocols);
385+
LOGGER.debug("TLS server context restricted to protocols: {}", Arrays.toString(allowedProtocols));
386+
}
387+
355388
try {
356389
warnIfNonJdkProviderConfiguredInternal("netty.server", tlsConfig);
357390
return sslContextBuilder.build();
@@ -435,7 +468,7 @@ private static void logTlsDiagnosticsOnce(String contextName, SSLContext sslCont
435468
return;
436469
}
437470

438-
// Basic what are we actually using at runtime? visibility.
471+
// Basic "what are we actually using at runtime?" visibility.
439472
LOGGER.info(
440473
"TLS diagnostics ({}): SSLContext protocol='{}', provider='{}', configuredSslProvider='{}', insecure={}",
441474
contextName, protocol, providerName, configuredSslProvider, insecure);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pinot.common.config;
20+
21+
import org.testng.Assert;
22+
import org.testng.annotations.Test;
23+
24+
public class TlsConfigTest {
25+
@Test
26+
public void copyConstructorShouldCopyInsecureAndAllowedProtocols() {
27+
TlsConfig original = new TlsConfig();
28+
original.setInsecure(true);
29+
original.setAllowedProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
30+
31+
TlsConfig copy = new TlsConfig(original);
32+
Assert.assertTrue(copy.isInsecure());
33+
Assert.assertEquals(copy.getAllowedProtocols(), new String[]{"TLSv1.2", "TLSv1.3"});
34+
35+
// Ensure returned array is a defensive copy
36+
String[] protocols = copy.getAllowedProtocols();
37+
protocols[0] = "MUTATED";
38+
Assert.assertEquals(copy.getAllowedProtocols(), new String[]{"TLSv1.2", "TLSv1.3"});
39+
}
40+
41+
@Test
42+
public void equalsAndHashCodeShouldIncludeAllowedProtocols() {
43+
TlsConfig a = new TlsConfig();
44+
a.setInsecure(true);
45+
a.setAllowedProtocols(new String[]{"TLSv1.2"});
46+
47+
TlsConfig b = new TlsConfig();
48+
b.setInsecure(true);
49+
b.setAllowedProtocols(new String[]{"TLSv1.2"});
50+
51+
Assert.assertEquals(a, b);
52+
Assert.assertEquals(a.hashCode(), b.hashCode());
53+
54+
b.setAllowedProtocols(new String[]{"TLSv1.3"});
55+
Assert.assertNotEquals(a, b);
56+
}
57+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.pinot.common.utils.tls;
20+
21+
import java.lang.reflect.Field;
22+
import org.testng.Assert;
23+
import org.testng.annotations.AfterMethod;
24+
import org.testng.annotations.Test;
25+
26+
public class JvmDefaultSslContextTest {
27+
private static final String JVM_TRUST_STORE = "javax.net.ssl.trustStore";
28+
29+
@AfterMethod
30+
public void cleanup() throws Exception {
31+
System.clearProperty(JVM_TRUST_STORE);
32+
setInitialized(false);
33+
}
34+
35+
@Test
36+
public void initDefaultSslContextShouldNotThrowWhenInitializationFails() throws Exception {
37+
setInitialized(false);
38+
System.setProperty(JVM_TRUST_STORE, "/does/not/exist");
39+
40+
// Should not throw (used from PinotAdministrator static initializer)
41+
JvmDefaultSslContext.initDefaultSslContext();
42+
Assert.assertTrue(isInitialized());
43+
44+
// Subsequent calls should be short-circuited
45+
JvmDefaultSslContext.initDefaultSslContext();
46+
Assert.assertTrue(isInitialized());
47+
}
48+
49+
private static boolean isInitialized() throws Exception {
50+
Field f = JvmDefaultSslContext.class.getDeclaredField("_initialized");
51+
f.setAccessible(true);
52+
return (boolean) f.get(null);
53+
}
54+
55+
private static void setInitialized(boolean value) throws Exception {
56+
Field f = JvmDefaultSslContext.class.getDeclaredField("_initialized");
57+
f.setAccessible(true);
58+
f.set(null, value);
59+
}
60+
}

pinot-common/src/test/java/org/apache/pinot/common/utils/tls/TlsUtilsTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
*/
1919
package org.apache.pinot.common.utils.tls;
2020

21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import org.apache.pinot.common.config.TlsConfig;
24+
import org.apache.pinot.spi.env.PinotConfiguration;
25+
import org.testng.Assert;
2126
import org.testng.annotations.Test;
2227

2328

@@ -26,4 +31,22 @@ public class TlsUtilsTest {
2631
public void installDefaultSSLSocketFactoryWhenNoKeyOrTrustStore() {
2732
TlsUtils.installDefaultSSLSocketFactory(null, null, null, null, null, null);
2833
}
34+
35+
@Test
36+
public void extractTlsConfigShouldTrimProtocols() {
37+
Map<String, Object> props = new HashMap<>();
38+
props.put("tls.protocols", " TLSv1.2, TLSv1.3 ,, ");
39+
TlsConfig tlsConfig = TlsUtils.extractTlsConfig(new PinotConfiguration(props), "tls");
40+
Assert.assertEquals(tlsConfig.getAllowedProtocols(), new String[]{"TLSv1.2", "TLSv1.3"});
41+
}
42+
43+
@Test
44+
public void extractTlsConfigShouldFallbackToDefaultProtocolsWhenEmpty() {
45+
Map<String, Object> props = new HashMap<>();
46+
props.put("tls.protocols", " , ");
47+
TlsConfig defaultConfig = new TlsConfig();
48+
defaultConfig.setAllowedProtocols(new String[]{"TLSv1.2"});
49+
TlsConfig tlsConfig = TlsUtils.extractTlsConfig(new PinotConfiguration(props), "tls", defaultConfig);
50+
Assert.assertEquals(tlsConfig.getAllowedProtocols(), new String[]{"TLSv1.2"});
51+
}
2952
}

pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ private KafkaSSLUtils() {
5959
private static final String DEFAULT_KEY_ALGORITHM = "RSA";
6060
private static final String DEFAULT_KEYSTORE_TYPE = "PKCS12";
6161
private static final String DEFAULT_SECURITY_PROTOCOL = "SSL";
62-
private static final String DEFAULT_TRUSTSTORE_TYPE = "jks";
62+
// Follow the JVM default keystore type (typically "jks") unless explicitly configured.
63+
private static final String DEFAULT_TRUSTSTORE_TYPE = KeyStore.getDefaultType();
6364
private static final String DEFAULT_SERVER_ALIAS = "ServerAlias";
6465
private static final String DEFAULT_CLIENT_ALIAS = "ClientAlias";
6566
// Key constants

0 commit comments

Comments
 (0)