如何在不重新启动的情况下续订Spring Data Geode连接中的密钥库(SSLContext)



上下文是我正在进行一个Kubernetes项目,在该项目中,我们使用了Geode集群和Spring Boot,以及Spring Boot Data Geode(SBDG)。我们用它开发了一个应用程序,ClientCache。此外,我们有一个专有的内部机制来生成集群内部证书,该机制根据最佳实践自动续订证书。我们将应用程序代码中的PEM格式证书转换为JKS,并使用@EnableSSL注释配置Spring来接受它们。

因此,问题是,在第一个周期,当连接是用应用程序最初启动时使用的JKS文件创建的时,一切都很好地工作,然而,如果证书续订,比如每小时续订一次(在云中,这是最佳实践),Geode无法连接到一堆异常,有时是SSLException(readHandshakeRecord),很多时候都带有";无法连接到列表"中的任何定位器;(但我进行了调试,它也是HandshakeException,只是连接异常中的包装器)。定位器和服务器已经启动并运行(我向GFSH核实了一下),只是我认为应用程序试图连接到旧的SSLContext,但在SSL握手中失败了。

到目前为止,我找到的唯一方法是完全重新启动应用程序,但我们需要这个系统是自动的,并且高度可用,所以这不应该是解决这个问题的唯一方法。

我认为这个问题影响了很多Spring/Java项目,因为我发现了这个问题(Kafka、PGSQL等)

你们有什么方法可以做到这一点吗?有没有办法:

  • 在不重新启动应用程序的情况下重新创建所有连接
  • 以某种方式使当前使用的连接无效,并强制ClientCache创建新的,重新读取JKS文件
  • 也许让客户端应用程序超时连接并销毁它们,然后使用刷新的SSLContext创建新的连接

我没有发现任何可能性。

编辑:让我添加一些代码,来展示我们是如何做事的,因为我们使用Spring,它非常简单:

@Configuration
@EnableGemfireRepositories(basePackages = "...")
@EnableEntityDefinedRegions(basePackages = "...")
@ClientCacheApplication
@EnableSsl(
truststore = "truststore.jks",
keystore = "keystore.jks",
truststorePassword = "pwd",
keystorePassword = "pwd"
)
public class GeodeTls {}

就是这样!然后,我们对@Regions和@repository使用普通注释,我们在@RestControllers中调用存储库方法,其中大多数只是空的,或者是默认的,因为我们使用OQL注释方法来处理Spring。由于Geode有一个基于属性的配置,我们从未设置KeyStores、TrustStores,我只是在调试过程中偶然在代码中看到它们。

第二版:多亏了以下评论,我终于解决了问题,正是这张Geode票帮了我很多忙(感谢Jen D):https://github.com/apache/geode/pull/2244,自Geode 1.8.0起可用。此外,下面的片段对Swappable KeyManager非常有用(感谢Hakan54),我最终完成了一个组合解决方案。不过,我必须小心,只设置一次默认的SSLContext,因为随后的设置无效,并且没有导致任何失败。现在该应用程序是稳定的,它似乎可以跨越证书更改。

我昨天遇到了你的问题,当时我正在开发一个原型。我认为在你的情况下这是可能的。然而,我只是用http客户端和服务器在本地进行了尝试,我可以在运行时更改证书,而无需重新启动这些应用程序或重新创建SSLContext。

选项1

从你的问题中,我可以理解你正在从某个地方读取PEM文件,并将其转换为其他文件,最后你使用的是SSLContext。在这种情况下,我假设您正在创建一个KeyManager和一个TrustManager。如果是这种情况,您需要创建KeyManager和TrustManager的自定义实现作为包装类,以将方法调用委托给包装类中的实际KeyManager和Trust Manager。还添加了一个setter方法,以便在证书更新时更改内部KeyManager和TrustManager。

在您的情况下,这将是一个文件观察器,当PEM文件发生更改时会触发它。在这种情况下,您只需要使用新证书重新生成KeyManager和TrustManager,并通过调用setter方法将其提供给封装的类。以下是您可以使用的示例代码片段:

热插拔X509扩展密钥管理器

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Objects;
public final class HotSwappableX509ExtendedKeyManager extends X509ExtendedKeyManager {
private X509ExtendedKeyManager keyManager;
public HotSwappableX509ExtendedKeyManager(X509ExtendedKeyManager keyManager) {
this.keyManager = Objects.requireNonNull(keyManager);
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return keyManager.chooseClientAlias(keyType, issuers, socket);
}
@Override
public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) {
return keyManager.chooseEngineClientAlias(keyTypes, issuers, sslEngine);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return keyManager.chooseServerAlias(keyType, issuers, socket);
}
@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) {
return keyManager.chooseEngineServerAlias(keyType, issuers, sslEngine);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return keyManager.getPrivateKey(alias);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return keyManager.getCertificateChain(alias);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return keyManager.getClientAliases(keyType, issuers);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return keyManager.getServerAliases(keyType, issuers);
}
public void setKeyManager(X509ExtendedKeyManager keyManager) {
this.keyManager = Objects.requireNonNull(keyManager);
}
}

热插拔X509扩展的TrustManager

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;
import java.net.Socket;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Objects;
public class HotSwappableX509ExtendedTrustManager extends X509ExtendedTrustManager {
private X509ExtendedTrustManager trustManager;
public HotSwappableX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) {
this.trustManager = Objects.requireNonNull(trustManager);
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
trustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
trustManager.checkClientTrusted(chain, authType, socket);
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
trustManager.checkClientTrusted(chain, authType, sslEngine);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
trustManager.checkServerTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
trustManager.checkServerTrusted(chain, authType, socket);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
trustManager.checkServerTrusted(chain, authType, sslEngine);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers();
return Arrays.copyOf(acceptedIssuers, acceptedIssuers.length);
}
public void setTrustManager(X509ExtendedTrustManager trustManager) {
this.trustManager = Objects.requireNonNull(trustManager);
}
}

用法

// Your key and trust manager created from the pem files
X509ExtendedKeyManager aKeyManager = ...
X509ExtendedTrustManager aTrustManager = ...
// Wrapping it into your hot swappable key and trust manager
HotSwappableX509ExtendedKeyManager swappableKeyManager = new HotSwappableX509ExtendedKeyManager(aKeyManager);
HotSwappableX509ExtendedTrustManager swappableTrustManager = new HotSwappableX509ExtendedTrustManager(aTrustManager);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(new KeyManager[]{ swappableKeyManager }, new TrustManager[]{ swappableTrustManager })
// Give the sslContext instance to your server or client
// After some time change the KeyManager and TrustManager with the following snippet:
X509ExtendedKeyManager anotherKeyManager = ... // Created from the new pem files
X509ExtendedTrustManager anotherTrustManager = ... // Created from the new pem files
// Set your new key and trust manager into your swappable managers
swappableKeyManager.setKeyManager(anotherKeyManager)
swappableTrustManager.setTrustManager(anotherTrustManager)

因此,即使SSLContext实例缓存在客户端的服务器中,您仍然可以交换新的密钥管理器和信任管理器。

此处提供代码片段:

Github-SSLContext Kickstart

  • 热插拔X509扩展密钥管理器
  • 热插拔X509扩展的信任管理器

选项2

如果你不想将自定义(HotSwappableKeyManager和HotSwappaableTrustManager)代码添加到你的代码库中,你也可以使用我的库:

<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>7.4.5</version>
</dependency>

用法

SSLFactory baseSslFactory = SSLFactory.builder()
.withDummyIdentityMaterial()
.withDummyTrustMaterial()
.withSwappableIdentityMaterial()
.withSwappableTrustMaterial()
.build();
SSLContext sslContext = sslFactory.getSslContext();

Runnable sslUpdater = () -> {
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(Paths.get("/path/to/your/identity.jks"), "password".toCharArray())
.withTrustMaterial(Paths.get("/path/to/your/truststore.jks"), "password".toCharArray())
.build();

SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
};
// initial update of ssl material to replace the dummies
sslUpdater.run();

// update ssl material every hour    
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS);

更新#1-pem文件示例在评论中,有人请求了一个pem文件的示例,因此下面是用pem文件刷新ssl配置的示例:

首先确保您有以下库:

<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-pem</artifactId>
<version>7.4.5</version>
</dependency>

代码示例:

SSLFactory baseSslFactory = SSLFactory.builder()
.withDummyIdentityMaterial()
.withDummyTrustMaterial()
.withSwappableIdentityMaterial()
.withSwappableTrustMaterial()
.build();
SSLContext sslContext = sslFactory.getSslContext();

Runnable sslUpdater = () -> {
X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial(Paths.get("/path/to/your/certificate-chain.pem"), Paths.get("/path/to/your/"private-key.pem"));
X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial(Paths.get("/path/to/your/"some-trusted-certificate.pem"));
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(keyManager)
.withTrustMaterial(trustManager)
.build();

SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
};
// initial update of ssl material to replace the dummies
sslUpdater.run();

// update ssl material every hour    
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS);

我认为您所寻找的与Java构建包在将应用程序部署到CloudFoundry时所做的非常相似。部署应用程序时,构建包会注入一个自定义的安全提供程序,该提供程序会监视各种密钥/信任存储中的更改。这允许在无需重新启动应用程序的情况下更新证书(https://docs.cloudfoundry.org/buildpacks/java/)。

我不确定具体的实现细节,但安全提供程序的代码可以在这里找到https://github.com/cloudfoundry/java-buildpack-security-provider.希望这能给你一些关于如何根据自己的需求实现这一点的想法。

最新更新