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

问题描述

上下文是我正在处理一个 Kubernetes 项目,我们在其中使用 Geode 集群和 Spring Boot,以及 Spring Boot Data Geode (SBDG)。我们用它开发了一个应用程序,一个 ClientCache。此外,我们有一个专有的内部机制来生成集群内部证书,该机制根据最佳实践自动更新证书。我们将 App 代码中 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 和@Repositories 使用普通的注释,我们有我们的@RestControllers,我们在其中调用存储库方法,其中大多数只是空的,或者是默认的,因为我们使用OQL annotate 方法用Spring 做事。由于 Geode 有一个基于属性的配置,我们从不设置 KeyStores、TrustStores,我只是在调试过程中碰巧在代码中看到它们。

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

解决方法

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

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

,

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

选项 1

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

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

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,SSLEngine sslEngine) {
            return keyManager.chooseEngineClientAlias(keyTypes,sslEngine);
    }

    @Override
    public String chooseServerAlias(String keyType,Socket socket) {
            return keyManager.chooseServerAlias(keyType,socket);
    }

    @Override
    public String chooseEngineServerAlias(String keyType,SSLEngine sslEngine) {
            return keyManager.chooseEngineServerAlias(keyType,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,SSLEngine sslEngine) throws CertificateException {
        trustManager.checkClientTrusted(chain,sslEngine);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,String authType) throws CertificateException {
        trustManager.checkServerTrusted(chain,authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,Socket socket) throws CertificateException {
        trustManager.checkServerTrusted(chain,socket);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,SSLEngine sslEngine) throws CertificateException {
        trustManager.checkServerTrusted(chain,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

选项 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);

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...