Eureka双向SSL模式下无法自注册,报bad certificate异常
问题
我尝试通过双向SSL保障微服务间通信,服务A可正常注册到Eureka Server,但Eureka实例自注册或注册到其他Eureka实例时,始终出现bad certificate异常。
Eureka Server的application.yml配置
server: port: ${SERVER_SSL_PORT:${PORT:10100}} ssl: enabled: true client-auth: need key-alias: ${JKS_ALIAS} key-password: ${JKS_KEY_PASSWORD} key-store-type: ${JKS_TYPE} key-store-provider: ${JKS_PROVIDER:SUN} key-store: ${JKS_PATH} key-store-password: ${JKS_KEY_PASSWORD} trust-store: ${JKS_PATH} trust-store-password: ${JKS_KEY_PASSWORD} trust-store-provider: ${JKS_PROVIDER:SUN} trust-store-type: ${JKS_TYPE} ## EUREKA CONFIGURATION eureka: client: enabled: true register-with-eureka: true fetch-registry: false service-url: defaultZone: ${SVC_REGISTRY:https://localhost:14102/eureka} prefer-same-zone-eureka: true healthcheck: enabled: true tls: enabled: true key-store: ${JKS_PATH} key-password: ${JKS_KEY_PASSWORD} key-store-type: ${JKS_TYPE} key-store-password: ${JKS_KEYSTORE_PASSWORD} trust-store: ${JKS_PATH} trust-store-password: ${JKS_KEY_PASSWORD} trust-store-type: ${JKS_TYPE} instance: instance-id: ${SERVER_NAME:${spring.application.name}:${server.port}}@${eureka.instance.hostname} hostname: ${SERVER_HOST:localhost} home-page-url: https://${eureka.instance.hostname}:${server.port}${server.contextPath:} status-page-url: https://${eureka.instance.hostname}:${server.port}${server.contextPath:}${management.endpoints.web.base-path:}/info health-check-url: https://${eureka.instance.hostname}:${server.port}${server.contextPath:}${management.endpoints.web.base-path:}/health secure-health-check-url: https://${eureka.instance.hostname}:${server.port}${server.contextPath:}${management.endpoints.web.base-path:}/health lease-renewal-interval-in-seconds: 10 prefer-ip-address: true metadata-map: instanceId: ${eureka.instance.instance-id} zone: ${SERVER_ZONE:local} non-secure-port-enabled: false secure-port-enabled: true secure-port: ${server.port}
自定义JerseyClient配置
@Bean @SneakyThrows public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs2() { DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs(); EurekaJerseyClientImpl.EurekaJerseyClientBuilder clientBuilder = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder() .withClientName("DiscoveryClient-HTTPClient-Custom") .withUserAgent("Java-EurekaClient") .withConnectionTimeout(config.getEurekaServerConnectTimeoutSeconds() * 1000) .withReadTimeout(config.getEurekaServerReadTimeoutSeconds() * 1000) .withMaxConnectionsPerHost(config.getEurekaServerTotalConnectionsPerHost()) .withMaxTotalConnections(config.getEurekaServerTotalConnections()) .withConnectionIdleTimeout(config.getEurekaConnectionIdleTimeoutSeconds() * 1000) .withEncoderWrapper(CodecWrappers.getEncoder(config.getEncoderName())) .withDecoderWrapper(CodecWrappers.resolveDecoder(config.getDecoderName(), config.getClientDataAccept())) .withCustomSSL(sslContext()); EurekaJerseyClient jerseyClient = clientBuilder.build(); args.setEurekaJerseyClient(jerseyClient);//Provide custom EurekaJerseyClient to override default one return args; } @Bean public SSLContext sslContext() throws Exception { log.info("initialize ssl context bean with keystore {} ", env.getProperty("JKS_PATH")); char[] password = env.getProperty("JKS_KEY_PASSWORD").toCharArray(); return new SSLContextBuilder() .loadTrustMaterial(ResourceUtils.getFile(env.getProperty("JKS_PATH")), password).build(); }
错误日志
2022-09-01 18:44:01.522 INFO 26829 --- [tbeatExecutor-0]
c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error.
endpoint=DefaultEndpoint{
serviceUrl='https://XXXXXXXXXXXXXXXXXXXX:DDDD/eureka/},
exception=javax.net.ssl.SSLHandshakeException: Received fatal alert:
bad_certificate
stacktrace=com.sun.jersey.api.client.ClientHandlerException:
javax.net.ssl.SSLHandshakeException: Received fatal alert:
bad_certificate
at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:187)
at com.sun.jersey.api.client.Client.handle(Client.java:652)
at com.sun.jersey.api.client.WebResource.handle(WebResource.java:682)
at com.sun.jersey.api.client.WebResource.access$200(WebResource.java:74)
at com.sun.jersey.api.client.WebResource$Builder.put(WebResource.java:529)
at
该证书在服务A注册到注册中心时可正常使用,且在搭载有效证书的真实服务器上测试,仍出现此问题。
排查解决线索
- 校验Eureka TLS配置的密钥库密码一致性:
eureka.client.tls.key-store-password使用的是${JKS_KEYSTORE_PASSWORD},而server.ssl等其他配置用的是${JKS_KEY_PASSWORD},若两个变量值不一致,会导致Eureka客户端无法正确加载密钥库。建议统一密码变量,或确认JKS_KEYSTORE_PASSWORD的值是否匹配密钥库实际密码。 - 补充SSLContext的密钥库加载逻辑:当前自定义的
sslContext()仅加载了信任库,双向SSL要求客户端同时提供自身证书供服务端验证,需补充加载密钥库的代码:@Bean public SSLContext sslContext() throws Exception { log.info("initialize ssl context bean with keystore {} ", env.getProperty("JKS_PATH")); char[] password = env.getProperty("JKS_KEY_PASSWORD").toCharArray(); KeyStore keyStore = KeyStore.getInstance(env.getProperty("JKS_TYPE")); keyStore.load(new FileInputStream(ResourceUtils.getFile(env.getProperty("JKS_PATH"))), password); return new SSLContextBuilder() .loadKeyMaterial(keyStore, password) .loadTrustMaterial(ResourceUtils.getFile(env.getProperty("JKS_PATH")), password) .build(); } - 确认目标Eureka Server信任当前实例证书:检查目标Eureka Server的trust-store是否包含当前Eureka实例的证书公钥,或确认双方使用的JKS文件是否互相包含对方证书。
- 检查证书的有效期与SAN扩展:确认证书是否在有效期内,且证书的Subject Alternative Name(SAN)扩展包含当前Eureka实例的hostname或IP,地址不匹配会导致服务端拒绝证书。
- 验证自定义JerseyClient是否生效:通过日志或调试模式确认
discoveryClientOptionalArgs2Bean是否被正确加载,Eureka客户端实际使用的SSLContext是否为自定义实例。 - 确认Eureka Server的client-auth配置逻辑:当前
server.ssl.client-auth设为need,要求所有客户端必须提供有效证书,需确保Eureka实例作为客户端时,正确发送了自身证书,而非仅信任服务端证书。
内容的提问来源于stack exchange,提问作者Angelo

