TLS: allow protocol allowlist; harden JVM SSL init#17776
TLS: allow protocol allowlist; harden JVM SSL init#17776xiangfu0 merged 5 commits intoapache:masterfrom
Conversation
| return result; | ||
| } | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
There should be a newline at the file end
| private static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; | ||
| private static final String DEFAULT_SECURITY_PROTOCOL = "SSL"; | ||
| private static final String DEFAULT_TRUSTSTORE_TYPE = "jks"; | ||
| private static final String DEFAULT_TRUSTSTORE_TYPE = KeyStore.getDefaultType(); |
There was a problem hiding this comment.
Please add comments.
Would this introduce backward compatibility for existing users?
There was a problem hiding this comment.
On standard JDKs, KeyStore.getDefaultType() returns "jks" (the same value we had hardcoded), so existing users see identical behavior. The only difference is that JVMs configured with a different default keystore type (via java.security properties) will now get the correct type automatically instead of being forced to "jks". Users can still override this via explicit Kafka SSL configuration properties as before.
| // Read allowed TLS protocols from config (e.g., "TLSv1.2,TLSv1.3") | ||
| String protocolsConfig = pinotConfig.getProperty(key(namespace, PROTOCOLS)); | ||
| if (protocolsConfig != null && !protocolsConfig.isEmpty()) { | ||
| tlsConfig.setAllowedProtocols(protocolsConfig.split(",")); |
There was a problem hiding this comment.
Better trim the string after split?
| // HttpsURLConnection | ||
| HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); | ||
| setSslContext(sc); | ||
| logTlsDiagnosticsOnce("https.default", sc, null, false); |
There was a problem hiding this comment.
Pull request overview
This PR enhances TLS/SSL configuration in Apache Pinot by adding protocol allowlist support and improving JVM startup reliability. The changes prevent JVM startup failures when SSL context initialization fails (logging warnings instead), add configurable TLS protocol restrictions, and fix several bugs in the TlsConfig class.
Changes:
- Added configurable TLS protocol allowlist (
tls.<namespace>.protocols) that can restrict TLS versions - Prevented JVM startup failures by catching SSL initialization errors in JvmDefaultSslContext and logging warnings instead
- Fixed TlsConfig copy constructor to include the
_insecurefield and added_allowedProtocolsfield with proper equals/hashCode implementation - Changed Kafka truststore default from hardcoded "jks" to
KeyStore.getDefaultType()for better portability
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java | Changed default truststore type from hardcoded "jks" to KeyStore.getDefaultType() for better platform compatibility |
| pinot-common/src/main/java/org/apache/pinot/common/config/TlsConfig.java | Added allowedProtocols field with proper getters/setters, fixed copy constructor to include _insecure, and updated equals/hashCode methods |
| pinot-common/src/main/java/org/apache/pinot/common/utils/tls/TlsUtils.java | Added parsing of protocol allowlist from configuration and applied it to Netty client/server SSL contexts |
| pinot-common/src/main/java/org/apache/pinot/common/utils/tls/JvmDefaultSslContext.java | Wrapped SSL initialization in try-catch to prevent JVM startup failures, added detailed documentation, and fixed typo in log message |
| // Read allowed TLS protocols from config (e.g., "TLSv1.2,TLSv1.3") | ||
| String protocolsConfig = pinotConfig.getProperty(key(namespace, PROTOCOLS)); | ||
| if (protocolsConfig != null && !protocolsConfig.isEmpty()) { | ||
| tlsConfig.setAllowedProtocols(protocolsConfig.split(",")); |
There was a problem hiding this comment.
The protocol strings should be trimmed after splitting to handle whitespace in configuration (e.g., "TLSv1.2, TLSv1.3" vs "TLSv1.2,TLSv1.3"). Consider using a pattern like:
String[] protocols = Arrays.stream(protocolsConfig.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toArray(String[]::new);
tlsConfig.setAllowedProtocols(protocols);
This follows the established pattern in the codebase, as seen in AuditRequestProcessor.parseAllowedHeaders.
| tlsConfig.setAllowedProtocols(protocolsConfig.split(",")); | |
| String[] protocols = Arrays.stream(protocolsConfig.split(",")) | |
| .map(String::trim) | |
| .filter(s -> !s.isEmpty()) | |
| .toArray(String[]::new); | |
| tlsConfig.setAllowedProtocols(protocols); |
| @Nullable | ||
| public String[] getAllowedProtocols() { | ||
| if (_allowedProtocols != null) { | ||
| return Arrays.copyOf(_allowedProtocols, _allowedProtocols.length); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public void setAllowedProtocols(@Nullable String[] allowedProtocols) { | ||
| _allowedProtocols = allowedProtocols != null | ||
| ? Arrays.copyOf(allowedProtocols, allowedProtocols.length) | ||
| : null; | ||
| } |
There was a problem hiding this comment.
The new functionality for configuring allowed TLS protocols lacks test coverage. Consider adding tests that verify:
- Protocol allowlist is correctly parsed from configuration (e.g., "TLSv1.2,TLSv1.3")
- The allowlist is applied to both client and server SSL contexts
- Whitespace handling in protocol strings (e.g., "TLSv1.2, TLSv1.3")
- Edge cases like empty strings or null values
A test file such as pinot-common/src/test/java/org/apache/pinot/common/config/TlsConfigTest.java would be appropriate to verify the copy constructor, equals, hashCode, and the new allowedProtocols field.
| try { | ||
| initDefaultSslContextInternal(); | ||
| } catch (Exception e) { | ||
| // Do NOT rethrow — this runs in PinotAdministrator's static initializer. | ||
| // Rethrowing would cause ExceptionInInitializerError and prevent the JVM from starting. | ||
| // Pinot components re-initialize TLS individually during their own startup. | ||
| LOGGER.warn("Failed to initialize JVM default SSL context. This is non-fatal — Pinot components " | ||
| + "will configure TLS individually during startup. trustStoreType='{}', trustStore='{}'. Error: {}", | ||
| System.getProperty(JVM_TRUST_STORE_TYPE), System.getProperty(JVM_TRUST_STORE), e.getMessage()); | ||
| LOGGER.debug("Full stack trace for SSL context initialization failure:", e); | ||
| } | ||
| _initialized = true; | ||
| } |
There was a problem hiding this comment.
The new error handling logic that prevents JVM startup failure lacks test coverage. Consider adding a test that verifies:
- When SSL context initialization fails, a warning is logged but the method completes normally
- The
_initializedflag is still set to true even when initialization fails - Subsequent calls to
initDefaultSslContext()are properly short-circuited
This is a critical change for reliability, and automated tests would help ensure the behavior is maintained in future refactorings.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #17776 +/- ##
============================================
- Coverage 63.22% 63.20% -0.02%
Complexity 1454 1454
============================================
Files 3179 3183 +4
Lines 191310 191476 +166
Branches 29263 29285 +22
============================================
+ Hits 120950 121021 +71
- Misses 60918 60996 +78
- Partials 9442 9459 +17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| } | ||
| _initialized = true; | ||
| LOGGER.info("Successfully initialized mvm default SSL context"); | ||
| LOGGER.info("Successfully initialized JVM default SSL context"); | ||
| } |
There was a problem hiding this comment.
LOGGER.info("Successfully initialized JVM default SSL context") is logged unconditionally even when the method didn’t actually set a new default SSLContext (e.g., when neither javax.net.ssl.keyStore nor javax.net.ssl.trustStore is provided, or when non-file schemes are used). Consider logging success only when initialization was performed, and log a different message (or nothing) when the method no-ops.
| import org.testng.annotations.Test; | ||
|
|
||
| public class JvmDefaultSslContextTest { | ||
| private static final String JVM_TRUST_STORE = "javax.net.ssl.trustStore"; | ||
|
|
||
| @AfterMethod | ||
| public void cleanup() throws Exception { | ||
| System.clearProperty(JVM_TRUST_STORE); | ||
| setInitialized(false); |
There was a problem hiding this comment.
This test clears javax.net.ssl.trustStore and forces JvmDefaultSslContext._initialized=false after each method. If the test JVM was started with an existing trustStore system property (or if other tests in the same JVM rely on it), this can cause cross-test interference. Consider capturing the previous property value / initialized state in @BeforeMethod and restoring them in @AfterMethod instead of always clearing/resetting.
| import org.testng.annotations.Test; | |
| public class JvmDefaultSslContextTest { | |
| private static final String JVM_TRUST_STORE = "javax.net.ssl.trustStore"; | |
| @AfterMethod | |
| public void cleanup() throws Exception { | |
| System.clearProperty(JVM_TRUST_STORE); | |
| setInitialized(false); | |
| import org.testng.annotations.BeforeMethod; | |
| import org.testng.annotations.Test; | |
| public class JvmDefaultSslContextTest { | |
| private static final String JVM_TRUST_STORE = "javax.net.ssl.trustStore"; | |
| private String _originalTrustStore; | |
| private boolean _originalInitialized; | |
| @BeforeMethod | |
| public void setUp() throws Exception { | |
| _originalTrustStore = System.getProperty(JVM_TRUST_STORE); | |
| _originalInitialized = isInitialized(); | |
| } | |
| @AfterMethod | |
| public void cleanup() throws Exception { | |
| if (_originalTrustStore != null) { | |
| System.setProperty(JVM_TRUST_STORE, _originalTrustStore); | |
| } else { | |
| System.clearProperty(JVM_TRUST_STORE); | |
| } | |
| setInitialized(_originalInitialized); |
| private static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; | ||
| private static final String DEFAULT_SECURITY_PROTOCOL = "SSL"; | ||
| private static final String DEFAULT_TRUSTSTORE_TYPE = "jks"; | ||
| // Follow the JVM default keystore type (typically "jks") unless explicitly configured. |
There was a problem hiding this comment.
The comment says the JVM default keystore type is "typically "jks"", but on modern JDKs (including Java 11) KeyStore.getDefaultType() is commonly "pkcs12". To avoid misleading operators, consider rewording this comment to be version-agnostic (e.g., "depends on JVM 'keystore.type'"), or mention both common defaults.
| // Follow the JVM default keystore type (typically "jks") unless explicitly configured. | |
| // Follow the JVM default keystore type (from 'keystore.type', commonly "jks" or "pkcs12") unless explicitly configured. |
| .map(String::trim).filter(StringUtils::isNotBlank).orElse(null); | ||
| RenewableTlsUtils.enableAutoRenewalFromFileStoreForSSLFactory(jvmSslFactory, jvmKeystoreType, jvmKeyStorePath, | ||
| jvmKeystorePassword, jvmTrustStoreType, jvmTrustStorePath, jvmTrustStorePassword, null, null, () -> false); |
There was a problem hiding this comment.
The RenewableTlsUtils.enableAutoRenewalFromFileStoreForSSLFactory(...) call uses jvmKeystoreType, but jvmKeystoreType is currently derived from javax.net.ssl.trustStoreType (same property used for jvmTrustStoreType) instead of javax.net.ssl.keyStoreType. This can ignore an explicitly configured keystore type and cause SSLFactory identity material loading to fail when keystore/truststore types differ.
Prevent JVM startup failure when JvmDefaultSslContext initialization throws (log warning and continue; components configure TLS during their own startup).
Add configurable TLS protocol allowlist (tls..protocols) and apply it to Netty client/server SSL contexts.
Extend TlsConfig with allowedProtocols, fix copy-constructor to include _insecure, and update equals/hashCode.
Use KeyStore.getDefaultType() as Kafka truststore default instead of hardcoded "jks".