How to avoid SSL handshake errors in your Kafka client because of a self-signed cluster CA

You’re trying to connect a Kafka client to a development Apache Kafka cluster which has been quickly set up using a self-signed CA certificate. You don’t have a copy of that CA certificate, and (because it’s not signed by a well-known CA) your Kafka client is failing because of SSL handshake errors.

The error contains messages like
org.apache.kafka.common.errors.SslAuthenticationException: SSL handshake failed
and
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.

For example:

$ ./bin/kafka-console-consumer.sh \
 --bootstrap-server dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com:443 \
 --topic DALE.TOPIC \
 --group dalegrp \
 --consumer-property 'security.protocol=SASL_SSL' \
 --consumer-property 'sasl.mechanism=SCRAM-SHA-512' \
 --consumer-property 'sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="dale-user" password="pSRtfwTMKNlz";'

[2021-06-27 23:19:06,048] ERROR [Consumer clientId=consumer-dalegrp-1, groupId=dalegrp] Connection to node -1 (dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com/9.46.199.58:443) failed authentication due to: SSL handshake failed (org.apache.kafka.clients.NetworkClient)
[2021-06-27 23:19:06,049] WARN [Consumer clientId=consumer-dalegrp-1, groupId=dalegrp] Bootstrap broker dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com:443 (id: -1 rack: null) disconnected (org.apache.kafka.clients.NetworkClient)
[2021-06-27 23:19:06,069] ERROR Error processing message, terminating consumer process:  (kafka.tools.ConsoleConsumer$)
org.apache.kafka.common.errors.SslAuthenticationException: SSL handshake failed
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:326)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:269)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1339)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1214)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1157)
	at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1074)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1061)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:770)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1008)
	at org.apache.kafka.common.network.SslTransportLayer.runDelegatedTasks(SslTransportLayer.java:430)
	at org.apache.kafka.common.network.SslTransportLayer.handshakeUnwrap(SslTransportLayer.java:514)
	at org.apache.kafka.common.network.SslTransportLayer.doHandshake(SslTransportLayer.java:368)
	at org.apache.kafka.common.network.SslTransportLayer.handshake(SslTransportLayer.java:291)
	at org.apache.kafka.common.network.KafkaChannel.prepare(KafkaChannel.java:173)
	at org.apache.kafka.common.network.Selector.pollSelectionKeys(Selector.java:543)
	at org.apache.kafka.common.network.Selector.poll(Selector.java:481)
	at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:561)
	at org.apache.kafka.clients.consumer.internals.ConsumerNetworkClient.poll(ConsumerNetworkClient.java:265)
	at org.apache.kafka.clients.consumer.internals.ConsumerNetworkClient.poll(ConsumerNetworkClient.java:236)
	at org.apache.kafka.clients.consumer.internals.ConsumerNetworkClient.poll(ConsumerNetworkClient.java:215)
	at org.apache.kafka.clients.consumer.internals.AbstractCoordinator.ensureCoordinatorReady(AbstractCoordinator.java:244)
	at org.apache.kafka.clients.consumer.internals.ConsumerCoordinator.poll(ConsumerCoordinator.java:480)
	at org.apache.kafka.clients.consumer.KafkaConsumer.updateAssignmentMetadataIfNeeded(KafkaConsumer.java:1257)
	at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1226)
	at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1206)
	at kafka.tools.ConsoleConsumer$ConsumerWrapper.receive(ConsoleConsumer.scala:444)
	at kafka.tools.ConsoleConsumer$.process(ConsoleConsumer.scala:103)
	at kafka.tools.ConsoleConsumer$.run(ConsoleConsumer.scala:77)
	at kafka.tools.ConsoleConsumer$.main(ConsoleConsumer.scala:54)
	at kafka.tools.ConsoleConsumer.main(ConsoleConsumer.scala)
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:439)
	at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
	at java.base/sun.security.validator.Validator.validate(Validator.java:264)
	at java.base/sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:313)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:276)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:141)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1317)
	... 29 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:434)
	... 35 more
Processed a total of 0 messages

I’m assuming that this is just for development purposes, that you know it’s safe to trust the certificate that the Kafka cluster is presenting, and that you’d rather just workaround the error than ask the owner of the Kafka cluster for a copy of their CA.

If all that is true, here is how you can quickly workaround this error.

Step 1 – get a copy of the certificate that the Kafka cluster is presenting

$ openssl s_client \
  -connect YOUR-KAFKA-CLUSTER-BOOTSTRAP-ADDRESS:PORT \
  -servername YOUR-KAFKA-CLUSTER-BOOTSTRAP-ADDRESS

For example:

$ openssl s_client \
  -connect dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com:443 \
  -servername dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com

CONNECTED(00000005)
depth=1 O = io.strimzi, CN = cluster-ca v0
verify error:num=19:self signed certificate in certificate chain
verify return:0
---
Certificate chain
 0 s:/O=io.strimzi/CN=dale-kafka
   i:/O=io.strimzi/CN=cluster-ca v0
 1 s:/O=io.strimzi/CN=cluster-ca v0
   i:/O=io.strimzi/CN=cluster-ca v0
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIE7zCCA9egAwIBAgIJAMakmFsl67WbMA0GCSqGSIb3DQEBCwUAMC0xEzARBgNV
BAoMCmlvLnN0cmltemkxFjAUBgNVBAMMDWNsdXN0ZXItY2EgdjAwHhcNMjEwNjI3
MjIwNDEwWhcNMjIwNjI3MjIwNDEwWjAqMRMwEQYDVQQKDAppby5zdHJpbXppMRMw
EQYDVQQDDApkYWxlLWthZmthMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEApeiV0pelGA3TBVYcRMR3bGwbNeHt8tvRErblZapzuDtDLSjWwZpIh26hpMWC
x4BNGmiT44IohnVqB87DADjpLlEs960sxgTdFcIn5OQAUsUODPHiXjtPL3nVyT4p
4WqsElHEUFnNTt0WdlYxN0c73eyUMA3hb87ayLKP4TVyGLJqYsWy8IEdv42UUv51
2/L7t44z0fva39HsVUAOCCxTmb+CyCrC0suXeVuDfqGpHPXqXVHfg01f2l1Z85au
l8BZUwyWmajTKSobwVEoPkw4oAR61HA5EUTYX4s0yWalN0O+k4GX9ck70vuj9Hi4
bMHGZm5Spo+WdsNk4T68c7MzLwIDAQABo4ICEzCCAg8wggILBgNVHREEggICMIIB
/oIeZGFsZS1rYWZrYS1icm9rZXJzLnN0cmltemkuc3ZjgixkYWxlLWthZmthLWJy
b2tlcnMuc3RyaW16aS5zdmMuY2x1c3Rlci5sb2NhbIISZGFsZS1rYWZrYS1icm9r
ZXJzghpkYWxlLWthZmthLWJyb2tlcnMuc3RyaW16aYI5ZGFsZS1rYWZrYS0wLmRh
bGUta2Fma2EtYnJva2Vycy5zdHJpbXppLnN2Yy5jbHVzdGVyLmxvY2FsgiBkYWxl
LWthZmthLWJvb3RzdHJhcC5zdHJpbXppLnN2Y4IuZGFsZS1rYWZrYS1ib290c3Ry
YXAuc3RyaW16aS5zdmMuY2x1c3Rlci5sb2NhbIJDZGFsZS1rYWZrYS1zYXNsc2Ny
YW0tMC1zdHJpbXppLmFwcHMuZWVtLXRlc3QtZmVzdC02LmNwLmZ5cmUuaWJtLmNv
bYIUZGFsZS1rYWZrYS1ib290c3RyYXCCS2RhbGUta2Fma2Etc2FzbHNjcmFtLWJv
b3RzdHJhcC1zdHJpbXppLmFwcHMuZWVtLXRlc3QtZmVzdC02LmNwLmZ5cmUuaWJt
LmNvbYIcZGFsZS1rYWZrYS1ib290c3RyYXAuc3RyaW16aYIrZGFsZS1rYWZrYS0w
LmRhbGUta2Fma2EtYnJva2Vycy5zdHJpbXppLnN2YzANBgkqhkiG9w0BAQsFAAOC
AQEAsOZZZ1+caVBj9qy6qg/TNdGaym8VOmQny0qe2hRoVAC0ljQvoKUoC0HusO25
WVerYTxLroknH88NgwsugcKdLgTCd8hhG+kB6XWpGtU2uo7P4wh96yZi3eUkyxrj
CnIJkKZvkRkstqt62iGaivamAsCLlU+XNxv9l+RQ++cy76JIuWMTenED4ZdZgcuq
LooPbukLBvDD3Iw41fG0wMEJRfn8H7HPamBiF5fezQQZUeiYLf7PBe7rticY1ST/
VGm/uimPqWtUig3MyJE9XawsRA/vKF4PcV9MH6wPvBufAVpo6tt0+EZeecSzHWET
9Dl+4TT0eBZNVZEf/PXohChyUw==
-----END CERTIFICATE-----
subject=/O=io.strimzi/CN=dale-kafka
issuer=/O=io.strimzi/CN=cluster-ca v0
---
No client certificate CA names sent
Server Temp Key: ECDH, X25519, 253 bits
---
SSL handshake has read 2538 bytes and written 373 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: 91C40A5999CD12938B5C199321C925750261D9212976FD95C813F08DD05BE166
    Session-ID-ctx:
    Master-Key: 0F2D6AE8BBA0C7E88A5805BB884415D7906315E6D6B12E29643C455EBD16ABA2C4A6F52A2A7D999E0998EABFE79AC67F
    Start Time: 1624832427
    Timeout   : 7200 (sec)
    Verify return code: 19 (self signed certificate in certificate chain)
---

Get a copy of the certificate from that output (between the BEGIN CERTIFICATE and END CERTIFICATE lines) and put it in a file called ca.pem.

For example:

$ cat ca.pem
-----BEGIN CERTIFICATE-----
MIIE7zCCA9egAwIBAgIJAMakmFsl67WbMA0GCSqGSIb3DQEBCwUAMC0xEzARBgNV
BAoMCmlvLnN0cmltemkxFjAUBgNVBAMMDWNsdXN0ZXItY2EgdjAwHhcNMjEwNjI3
MjIwNDEwWhcNMjIwNjI3MjIwNDEwWjAqMRMwEQYDVQQKDAppby5zdHJpbXppMRMw
EQYDVQQDDApkYWxlLWthZmthMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEApeiV0pelGA3TBVYcRMR3bGwbNeHt8tvRErblZapzuDtDLSjWwZpIh26hpMWC
x4BNGmiT44IohnVqB87DADjpLlEs960sxgTdFcIn5OQAUsUODPHiXjtPL3nVyT4p
4WqsElHEUFnNTt0WdlYxN0c73eyUMA3hb87ayLKP4TVyGLJqYsWy8IEdv42UUv51
2/L7t44z0fva39HsVUAOCCxTmb+CyCrC0suXeVuDfqGpHPXqXVHfg01f2l1Z85au
l8BZUwyWmajTKSobwVEoPkw4oAR61HA5EUTYX4s0yWalN0O+k4GX9ck70vuj9Hi4
bMHGZm5Spo+WdsNk4T68c7MzLwIDAQABo4ICEzCCAg8wggILBgNVHREEggICMIIB
/oIeZGFsZS1rYWZrYS1icm9rZXJzLnN0cmltemkuc3ZjgixkYWxlLWthZmthLWJy
b2tlcnMuc3RyaW16aS5zdmMuY2x1c3Rlci5sb2NhbIISZGFsZS1rYWZrYS1icm9r
ZXJzghpkYWxlLWthZmthLWJyb2tlcnMuc3RyaW16aYI5ZGFsZS1rYWZrYS0wLmRh
bGUta2Fma2EtYnJva2Vycy5zdHJpbXppLnN2Yy5jbHVzdGVyLmxvY2FsgiBkYWxl
LWthZmthLWJvb3RzdHJhcC5zdHJpbXppLnN2Y4IuZGFsZS1rYWZrYS1ib290c3Ry
YXAuc3RyaW16aS5zdmMuY2x1c3Rlci5sb2NhbIJDZGFsZS1rYWZrYS1zYXNsc2Ny
YW0tMC1zdHJpbXppLmFwcHMuZWVtLXRlc3QtZmVzdC02LmNwLmZ5cmUuaWJtLmNv
bYIUZGFsZS1rYWZrYS1ib290c3RyYXCCS2RhbGUta2Fma2Etc2FzbHNjcmFtLWJv
b3RzdHJhcC1zdHJpbXppLmFwcHMuZWVtLXRlc3QtZmVzdC02LmNwLmZ5cmUuaWJt
LmNvbYIcZGFsZS1rYWZrYS1ib290c3RyYXAuc3RyaW16aYIrZGFsZS1rYWZrYS0w
LmRhbGUta2Fma2EtYnJva2Vycy5zdHJpbXppLnN2YzANBgkqhkiG9w0BAQsFAAOC
AQEAsOZZZ1+caVBj9qy6qg/TNdGaym8VOmQny0qe2hRoVAC0ljQvoKUoC0HusO25
WVerYTxLroknH88NgwsugcKdLgTCd8hhG+kB6XWpGtU2uo7P4wh96yZi3eUkyxrj
CnIJkKZvkRkstqt62iGaivamAsCLlU+XNxv9l+RQ++cy76JIuWMTenED4ZdZgcuq
LooPbukLBvDD3Iw41fG0wMEJRfn8H7HPamBiF5fezQQZUeiYLf7PBe7rticY1ST/
VGm/uimPqWtUig3MyJE9XawsRA/vKF4PcV9MH6wPvBufAVpo6tt0+EZeecSzHWET
9Dl+4TT0eBZNVZEf/PXohChyUw==
-----END CERTIFICATE-----

Step 2 – put that in a new truststore

$ openssl x509 -outform der -in ca.pem -out ca.der

$ keytool -import -noprompt \
	-alias ca \
	-file ca.der \
	-keystore ca.p12 -storepass STOREPASSW0RD

Step 3 – use your new truststore with your Kafka client

For example:

./bin/kafka-console-consumer.sh \
 --bootstrap-server dale-kafka-saslscram-bootstrap-strimzi.apps.eem-test-fest-6.cp.fyre.ibm.com:443 \
 --topic DALE.TOPIC \
 --group dalegrp \
 --consumer-property 'security.protocol=SASL_SSL' \
 --consumer-property 'sasl.mechanism=SCRAM-SHA-512' \
 --consumer-property 'sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="dale-user" password="pSRtfwTMKNlz";' \
 --consumer-property 'ssl.truststore.location=ca.p12' \
 --consumer-property 'ssl.truststore.type=PKCS12' \
 --consumer-property 'ssl.truststore.password=STOREPASSW0RD'

Tags: ,

One Response to “How to avoid SSL handshake errors in your Kafka client because of a self-signed cluster CA”

  1. Srinu B says:

    Very good article clearly explaining problem and solution. Thank you.