Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Changed `gremlin-go` Client `ReadBufferSize` and `WriteBufferSize` defaults to 1048576 (1MB) to align with DriverRemoteConnection.
* Fixed bug in `IndexStep` which prevented Java serialization due to non-serializable lambda usage by creating serializable function classes.
* Fixed bug in `Operator` which was caused only a single method parameter to be Collection type checked instead of all parameters.
* Support hot reloading of SSL certificates.

[[release-3-7-3]]
=== TinkerPop 3.7.3 (October 23, 2024)
Expand Down
10 changes: 10 additions & 0 deletions gremlin-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ limitations under the License.
<artifactId>logback-classic</artifactId>
<optional>true</optional>
</dependency>
<dependency>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This package is Apache 2 licensed with no provided NOTICE file. I see no concerns from a licensing standpoint.

<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-netty</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- METRICS -->
<dependency>
<groupId>com.codahale.metrics</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.IdleStateHandler;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.exception.GenericSecurityException;
import nl.altindag.ssl.netty.util.NettySslUtils;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.apache.tinkerpop.gremlin.server.util.SSLStoreFilesModificationWatcher;
import org.apache.tinkerpop.gremlin.util.MessageSerializer;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
Expand All @@ -44,19 +48,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -65,6 +62,7 @@
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

/**
Expand Down Expand Up @@ -148,8 +146,34 @@ public void init(final ServerGremlinExecutor serverGremlinExecutor) {
configureSerializers();

// configure ssl if present
sslContext = settings.optionalSsl().isPresent() && settings.ssl.enabled ?
Optional.ofNullable(createSSLContext(settings)) : Optional.empty();
if (settings.optionalSsl().isPresent() && settings.ssl.enabled) {
if (settings.ssl.getSslContext().isPresent()) {
logger.info("Using the SslContext override");
this.sslContext = settings.ssl.getSslContext();
} else {
final SSLFactory sslFactory = createSSLFactoryBuilder(settings).withSwappableTrustMaterial().withSwappableIdentityMaterial().build();
this.sslContext = Optional.of(createSSLContext(sslFactory));

if (settings.ssl.refreshInterval > 0) {
// At the scheduled refreshInterval, check whether the keyStore or trustStore has been modified. If they were,
// reload the SSLFactory which will reload the underlying KeyManager/TrustManager that Netty SSLHandler uses.
scheduledExecutorService.scheduleAtFixedRate(
new SSLStoreFilesModificationWatcher(settings.ssl.keyStore, settings.ssl.trustStore, () -> {
SSLFactory newSslFactory = createSSLFactoryBuilder(settings).build();
try {
SSLFactoryUtils.reload(sslFactory, newSslFactory);
} catch (RuntimeException e) {
logger.error("Failed to reload SSLFactory", e);
}
}),
settings.ssl.refreshInterval, settings.ssl.refreshInterval, TimeUnit.MILLISECONDS
);
}
}
} else {
this.sslContext = Optional.empty();
}

if (sslContext.isPresent()) logger.info("SSL enabled");

authenticator = createAuthenticator(settings.authentication);
Expand Down Expand Up @@ -307,74 +331,59 @@ private void configureSerializers() {
}
}

private SslContext createSSLContext(final Settings settings) {
private SSLFactory.Builder createSSLFactoryBuilder(final Settings settings) {
final Settings.SslSettings sslSettings = settings.ssl;

if (sslSettings.getSslContext().isPresent()) {
logger.info("Using the SslContext override");
return sslSettings.getSslContext().get();
}

final SslProvider provider = SslProvider.JDK;

final SslContextBuilder builder;

// Build JSSE SSLContext
final SSLFactory.Builder builder = SSLFactory.builder();
try {
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

// Load private key and signed cert
if (null != sslSettings.keyStore) {
final String keyStoreType = null == sslSettings.keyStoreType ? KeyStore.getDefaultType() : sslSettings.keyStoreType;
final KeyStore keystore = KeyStore.getInstance(keyStoreType);
final char[] password = null == sslSettings.keyStorePassword ? null : sslSettings.keyStorePassword.toCharArray();
try (final InputStream in = new FileInputStream(sslSettings.keyStore)) {
keystore.load(in, password);
builder.withIdentityMaterial(in, password, keyStoreType);
}
kmf.init(keystore, password);
} else {
throw new IllegalStateException("keyStore must be configured when SSL is enabled.");
}

builder = SslContextBuilder.forServer(kmf);

// Load custom truststore for client auth certs
if (null != sslSettings.trustStore) {
final String trustStoreType = null != sslSettings.trustStoreType ? sslSettings.trustStoreType
: sslSettings.keyStoreType != null ? sslSettings.keyStoreType : KeyStore.getDefaultType();

final KeyStore truststore = KeyStore.getInstance(trustStoreType);
: sslSettings.keyStoreType != null ? sslSettings.keyStoreType : KeyStore.getDefaultType();
final char[] password = null == sslSettings.trustStorePassword ? null : sslSettings.trustStorePassword.toCharArray();
try (final InputStream in = new FileInputStream(sslSettings.trustStore)) {
truststore.load(in, password);
builder.withTrustMaterial(in, password, trustStoreType);
}
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(truststore);
builder.trustManager(tmf);
}

} catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) {
} catch (GenericSecurityException | IOException e) {
logger.error(e.getMessage());
throw new RuntimeException("There was an error enabling SSL.", e);
}

if (null != sslSettings.sslCipherSuites && !sslSettings.sslCipherSuites.isEmpty()) {
builder.ciphers(sslSettings.sslCipherSuites);
builder.withCiphers(sslSettings.sslCipherSuites.toArray(new String[] {}));
}

if (null != sslSettings.sslEnabledProtocols && !sslSettings.sslEnabledProtocols.isEmpty()) {
builder.protocols(sslSettings.sslEnabledProtocols.toArray(new String[] {}));
builder.withProtocols(sslSettings.sslEnabledProtocols.toArray(new String[] {}));
}

if (null != sslSettings.needClientAuth && ClientAuth.OPTIONAL == sslSettings.needClientAuth) {
logger.warn("needClientAuth = OPTIONAL is not a secure configuration. Setting to REQUIRE.");
sslSettings.needClientAuth = ClientAuth.REQUIRE;
}

builder.clientAuth(sslSettings.needClientAuth).sslProvider(provider);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 SSLFactory does not expose an option to configure the SSL Provider. Instead, we configure it below (L. 381) on the Netty SslContextBuilder built from the SSLFactory.

if (sslSettings.needClientAuth == ClientAuth.REQUIRE) {
builder.withNeedClientAuthentication(true);
}

return builder;
}

private static SslContext createSSLContext(final SSLFactory sslFactory) {
try {
return builder.build();
final SslProvider provider = SslProvider.JDK;
return NettySslUtils.forServer(sslFactory).sslProvider(provider).build();
} catch (SSLException ssle) {
logger.error(ssle.getMessage());
throw new RuntimeException("There was an error enabling SSL.", ssle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@ public static class SslSettings {
*/
public ClientAuth needClientAuth = ClientAuth.NONE;

/**
* The interval, in milliseconds, at which the trustStore and keyStore files are checked for updates.
* The default interval is 60 seconds.
*/
public long refreshInterval = 60000L;

private SslContext sslContext;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.tinkerpop.gremlin.server.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

/**
* FileWatcher monitoring changes to SSL keyStore/trustStore files.
* If a keyStore/trustStore file is set to null, it will be ignored.
* If a keyStore/trustStore file is deleted, it will be considered not modified.
*/
public class SSLStoreFilesModificationWatcher implements Runnable {

private static final Logger logger = LoggerFactory.getLogger(SSLStoreFilesModificationWatcher.class);

private final Path keyStore;
private final Path trustStore;
private final Runnable onModificationRunnable;

private ZonedDateTime lastModifiedTimeKeyStore = null;
private ZonedDateTime lastModifiedTimeTrustStore = null;

/**
* Create a FileWatcher on keyStore/trustStore
*
* @param keyStore path to the keyStore file or null to ignore
* @param trustStore path to the trustStore file or null to ignore
* @param onModificationRunnable function to run when a modification to the keyStore or trustStore is detected
*/
public SSLStoreFilesModificationWatcher(final String keyStore, final String trustStore, final Runnable onModificationRunnable) {
// keyStore/trustStore can be null when not specified in gremlin-server Settings
this.keyStore = keyStore != null ? Paths.get(keyStore) : null;
this.trustStore = trustStore != null ? Paths.get(trustStore) : null;
this.onModificationRunnable = onModificationRunnable;

// Initialize lastModifiedTime
try {
if (this.keyStore != null) {
lastModifiedTimeKeyStore = getLastModifiedTime(this.keyStore);
}
if (this.trustStore != null) {
lastModifiedTimeTrustStore = getLastModifiedTime(this.trustStore);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
logger.info("Started listening to modifications to the KeyStore and TrustStore files");
}

@Override
public void run() {
try {
boolean keyStoreUpdated = false;
boolean trustStoreUpdated = false;
ZonedDateTime keyStoreModificationDateTime = null;
ZonedDateTime trustStoreModificationDateTime = null;

// Check if the keyStore file still exists and compare its last_modified_time
if (keyStore != null && Files.exists(keyStore)) {
keyStoreModificationDateTime = getLastModifiedTime(keyStore);
keyStoreUpdated = lastModifiedTimeKeyStore.isBefore(keyStoreModificationDateTime);
if (keyStoreUpdated) {
logger.info("KeyStore file has been modified.");
}
}

// Check if the trustStore file still exists and compare its last_modified_time
if (trustStore != null && Files.exists(trustStore)) {
trustStoreModificationDateTime = getLastModifiedTime(trustStore);
trustStoreUpdated = lastModifiedTimeTrustStore.isBefore(trustStoreModificationDateTime);
if (trustStoreUpdated) {
logger.info("TrustStore file has been modified.");
}
}

// If one of the files was updated, execute
if (keyStoreUpdated || trustStoreUpdated) {
onModificationRunnable.run();

if (keyStoreUpdated) {
lastModifiedTimeKeyStore = keyStoreModificationDateTime;
logger.info("Updated KeyStore configuration");
}
if (trustStoreUpdated) {
lastModifiedTimeTrustStore = trustStoreModificationDateTime;
logger.info("Updated TrustStore configuration");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static ZonedDateTime getLastModifiedTime(final Path filepath) throws IOException {
BasicFileAttributes attributes = Files.readAttributes(filepath, BasicFileAttributes.class);
return ZonedDateTime.ofInstant(attributes.lastModifiedTime().toInstant(), ZoneOffset.UTC);
}
}
Loading
Loading