Spring应用从GCP Secret Manager获取密钥遇SSL证书错误求助
问题:Spring应用访问GCP Secret Manager时出现SSL证书错误
我按照GCP官方文档实现从Secret Manager获取密钥,应用基于Spring框架,主方法先通过SpringApplication.run启动容器,但运行时出现SSL握手失败的错误,错误栈如下:
Exception in thread "main" com.google.api.gax.rpc.UnavailableException: io.grpc.StatusRuntimeException: UNAVAILABLE: Credentials failed to obtain metadata at com.google.api.gax.rpc.ApiExceptionFactory.createException(ApiExceptionFactory.java:67) at com.google.api.gax.grpc.GrpcApiExceptionFactory.create(GrpcApiExceptionFactory.java:72) at com.google.api.gax.grpc.GrpcApiExceptionFactory.create(GrpcApiExceptionFactory.java:60) at com.google.api.gax.grpc.GrpcExceptionCallable$ExceptionTransformingFuture.onFailure(GrpcExceptionCallable.java:97) at com.google.api.core.ApiFutures$1.onFailure(ApiFutures.java:67) at com.google.common.util.concurrent.Futures$CallbackListener.run(Futures.java:1132) at com.google.common.util.concurrent.DirectExecutor.execute(DirectExecutor.java:31) at com.google.common.util.concurrent.AbstractFuture.executeListener(AbstractFuture.java:1270) at com.google.common.util.concurrent.AbstractFuture.complete(AbstractFuture.java:1038) at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:808) at io.grpc.stub.ClientCalls$GrpcFuture.setException(ClientCalls.java:563) at io.grpc.stub.ClientCalls$UnaryStreamToFuture.onClose(ClientCalls.java:533) at io.grpc.PartialForwardingClientCallListener.onClose(PartialForwardingClientCallListener.java:39) at io.grpc.ForwardingClientCallListener.onClose(ForwardingClientCallListener.java:23) at io.grpc.ForwardingClientCallListener$SimpleForwardingClientCallListener.onClose(ForwardingClientCallListener.java:40) at com.google.api.gax.grpc.ChannelPool$ReleasingClientCall$1.onClose(ChannelPool.java:535) at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:562) at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:70) at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:743) at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:722) at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37) at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:133) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:858) Suppressed: com.google.api.gax.rpc.AsyncTaskException: Asynchronous task failed at com.google.api.gax.rpc.ApiExceptions.callAndTranslateApiException(ApiExceptions.java:57) at com.google.api.gax.rpc.UnaryCallable.call(UnaryCallable.java:112) at com.google.cloud.secretmanager.v1.SecretManagerServiceClient.accessSecretVersion(SecretManagerServiceClient.java:1279) at com.google.cloud.secretmanager.v1.SecretManagerServiceClient.accessSecretVersion(SecretManagerServiceClient.java:1215) at com.recontool.readSecrets.AccessSecretVersion.accessSecretVersion(AccessSecretVersion.java:39) at com.recontool.readSecrets.AccessSecretVersion.main(AccessSecretVersion.java:25) Caused by: io.grpc.StatusRuntimeException: UNAVAILABLE: Credentials failed to obtain metadata at io.grpc.StatusRuntimeException.fillInStackTrace(StatusRuntimeException.java:68) at io.grpc.StatusRuntimeException.<init>(StatusRuntimeException.java:58) at io.grpc.StatusRuntimeException.<init>(StatusRuntimeException.java:50) at io.grpc.Status.asRuntimeException(Status.java:535) ... 14 more Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:371) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:314) at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:309) at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1351) at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1226) at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1169) at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:396) at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:480) at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:458) at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:201) at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172) at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1505) at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1420) at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455) at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:426) at java.base/sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:578) at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:183) at java.base/sun.net.www.protocol.http.HttpURLConnection.getOutputStream0(HttpURLConnection.java:1430) at java.base/sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1401) at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:220) at com.google.api.client.http.javanet.NetHttpRequest.execute(NetHttpRequest.java:113) at com.google.api.client.http.javanet.NetHttpRequest.execute(NetHttpRequest.java:84) at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1012) at com.google.auth.oauth2.UserCredentials.doRefreshAccessToken(UserCredentials.java:277) at com.google.auth.oauth2.UserCredentials.refreshAccessToken(UserCredentials.java:191) at com.google.auth.oauth2.OAuth2Credentials$1.call(OAuth2Credentials.java:257) at com.google.auth.oauth2.OAuth2Credentials$1.call(OAuth2Credentials.java:254) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ... 3 more Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:388) at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:271) at java.base/sun.security.validator.Validator.validate(Validator.java:256) at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231) at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:132) at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1335) ... 27 more Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141) at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126) at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297) at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:383) ... 32 more
我的代码片段如下:
public static void main(String[] args) throws IOException { SpringApplication.run(AccessSecretVersion.class, args); String projectId = .. String secretId = .. String versionId = "2"; accessSecretVersion(projectId, secretId, versionId); } public static void accessSecretVersion(String projectId, String secretId, String versionId) throws IOException { // Initialize client that will be used to send requests. This client only needs to be created // once, and can be reused for multiple requests. After completing all of your requests, call // the "close" method on the client to safely clean up any remaining background resources. try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, versionId); // Access the secret version. AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName); // Verify checksum. The used library is available in Java 9+. // If using Java 8, you may use the following: // https://github.com/google/guava/blob/e62d6a0456420d295089a9c319b7593a3eae4a83/guava/src/com/google/common/hash/Hashing.java#L395 byte[] data = response.getPayload().getData().toByteArray(); Checksum checksum = new CRC32C(); checksum.update(data, 0, data.length); if (response.getPayload().getDataCrc32C() != checksum.getValue()) { System.out.printf("Data corruption detected."); return; } // Print the secret payload. // // WARNING: Do not print the secret in a production environment - this // snippet is showing how to access the secret material. String payload = response.getPayload().getData().toStringUtf8(); System.out.printf("Plaintext: %s\n", payload); } } }
请问我哪里操作有误,该如何解决这个问题?
解决方案
1. 核心问题:SSL证书信任缺失
错误栈最底层是SunCertPathBuilderException: unable to find valid certification path to requested target,说明JVM不信任GCP服务端的SSL证书,通常是因为:
- 本地环境没有导入GCP根证书
- 应用运行在代理环境下,代理证书未被JVM信任
2. Spring应用的代码写法问题
你在main方法启动Spring容器后直接调用静态方法accessSecretVersion,这种写法不符合Spring的生命周期管理,应该:
- 将Secret Manager客户端配置为Spring Bean,由Spring管理生命周期
- 在Spring的初始化回调(如
@PostConstruct)中执行密钥获取逻辑
修正后的代码示例:
@SpringBootApplication public class AccessSecretVersion { private final String projectId = "你的项目ID"; private final String secretId = "你的密钥ID"; private final String versionId = "2"; public static void main(String[] args) { SpringApplication.run(AccessSecretVersion.class, args); } @Bean public SecretManagerServiceClient secretManagerServiceClient() throws IOException { return SecretManagerServiceClient.create(); } @PostConstruct public void init() throws IOException { try (SecretManagerServiceClient client = secretManagerServiceClient()) { SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, versionId); AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName); byte[] data = response.getPayload().getData().toByteArray(); Checksum checksum = new CRC32C(); checksum.update(data, 0, data.length); if (response.getPayload().getDataCrc32C() != checksum.getValue()) { System.out.printf("Data corruption detected."); return; } String payload = response.getPayload().getData().toStringUtf8(); System.out.printf("Plaintext: %s\n", payload); } } }
3. 解决SSL证书问题的具体步骤
方法一:导入GCP根证书到JVM信任库
- 获取GCP服务端对应的根证书
- 使用
keytool命令导入证书到JVM的cacerts信任库:
keytool -importcert -file <证书文件路径> -alias gcp-root -keystore <JDK安装路径>/jre/lib/security/cacerts
默认信任库密码为changeit
方法二:配置JVM信任自定义证书
启动应用时添加JVM参数,指定包含GCP证书的自定义信任库:
java -Djavax.net.ssl.trustStore=<自定义信任库路径> -Djavax.net.ssl.trustStorePassword=<信任库密码> -jar 你的应用.jar
方法三:使用GCP SDK自动处理证书(推荐)
确保应用依赖最新版本的GCP Secret Manager SDK,它会自动处理证书信任逻辑。同时正确配置GCP认证:
- 本地开发时,设置环境变量
GOOGLE_APPLICATION_CREDENTIALS指向你的服务账号密钥文件 - 部署到GCP环境时,使用默认应用身份认证,无需额外配置
4. 认证配置检查
确保你的服务账号拥有roles/secretmanager.secretAccessor权限,并且:
- 本地开发时,
GOOGLE_APPLICATION_CREDENTIALS环境变量已正确设置 - 代码中没有覆盖默认的GCP认证配置
内容的提问来源于stack exchange,提问作者David Gordon
相关产品推荐
相关产品推荐

