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

How to renew keystore (SSLContext) in Spring Data Geode connections without restarting?

上下文是我正在做一个 Kubernetes 项目,我们在其中使用 Geode 集群和 Spring Boot,还有 Spring Boot Data Geode (SBDG)。我们用它开发了一个应用程序,一个 ClientCache。我们还有一个专有的内部机制来生成集群内部证书,该机制根据最佳实践自动更新证书。我们将App代码中的PEM格式证书转为JKS格式,并配置Spring@EnableSSL注解获取。

所以问题是,当使用应用程序最初启动时使用的 JKS 文件创建连接时,第一个周期一切正常practice), Geode 连接失败抛出一堆Exception, 有时是SSLException (readHandshakeRecord), 很多时候是"Unable to connect to any locators in the list" (不过我调试过, 也是HandshakeException, 只是wrapper in a连接异常)。定位器和服务器已启动并且 运行(我用 GFSH 检查过),我认为只是应用程序尝试连接旧的 SSLContext 但在 SSL 握手中失败。

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

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

你们有没有办法做到这一点? 有没有办法:

我没有找到任何可能性。

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

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

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

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

我认为您正在寻找的内容与 Java 构建包在将应用程序部署到 CloudFoundry 时所做的非常相似。部署应用程序时,buildpack 会注入一个自定义安全提供程序,它会监视各种 key/trust 商店的更改。这允许更新证书而无需重新启动应用程序 (https://docs.cloudfoundry.org/buildpacks/java/)。

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

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

选项 1

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

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

HotSwappableX509ExtendedKeyManager

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);
    }

}

HotSwappableX509ExtendedTrustManager

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 实例缓存在您的客户端服务器中,您仍然可以换入和换出新的 keymanager 和 trustmanager。

此处提供代码片段:

Github - SSLContext Kickstart

选项 2

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

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

用法

SSLFactory sslFactory = SSLFactory.builder()
          .withSwappableIdentityMaterial()
          .withIdentityMaterial("identity.jks", "password".toCharArray())
          .withSwappableTrustMaterial()
          .withTrustMaterial("truststore.jks", "password".toCharArray())
          .build();

SSLContext sslContext = sslFactory.getSslContext();
          
// Give the sslContext instance to your server or client
// After some time change the KeyManager and TrustManager with the following snippet:

// swap identity and trust materials and reuse existing http client
KeyManagerUtils.swapKeyManager(sslFactory.getKeyManager().get(), anotherKeyManager);
TrustManagerUtils.swapTrustManager(sslFactory.getTrustManager().get(), anotherTrustManager);

// Cleanup old ssl sessions by invalidating them all. Forces to use new ssl sessions which will be created by the swapped KeyManager/TrustManager
SSLSessionUtils.invalidateCaches(sslContext);