SonarQube通过SMTP+STARTTLS发送测试通知失败的解决方法
问题背景
在SonarQube中配置SMTP协议结合STARTTLS发送测试通知时失败,但不使用STARTTLS的SMTP测试可以成功发送。
SonarQube配置信息
SMTP主机: 1X.XXX.XX.X1 SMTP端口: 587 安全连接类型: starttls
已正确填写目标邮箱地址,SonarQube运行在Debian 11系统,SMTP服务器为MS Exchange,使用自签名证书且已安装到系统truststore。
失败日志片段(web.log)
2022.10.24 09:36:57 INFO web[AYPp5oPhM9pKCPrzAA6Z][javax.mail] JavaMail version 1.6.2 2022.10.24 09:36:57 INFO web[AYPp5oPhM9pKCPrzAA6Z][javax.mail] successfully loaded resource: /META-INF/javamail.default.address.map 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][javax.activation] MailcapCommandMap: createDataContentHandler for text/plain 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][javax.activation] search DB #1 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][javax.activation] got content-handler 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][javax.activation] class com.sun.mail.handlers.text_plain 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][javax.mail] getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle] 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] useEhlo true, useAuth false 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] trying to connect to host "1X.XXX.XX.X1", port 587, isSSL false 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] connected to host "1X.XXX.XX.X1", port: 587 2022.10.24 09:36:57 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "SIZE", arg "26214400" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "PIPELINING", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "DSN", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "ENHANCEDSTATUSCODES", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "STARTTLS", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "AUTH", arg "NTLM" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "8BITMIME", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "BINARYMIME", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][com.sun.mail.smtp] Found extension "CHUNKING", arg "" 2022.10.24 09:36:58 DEBUG web[AYPp5oPhM9pKCPrzAA6Z][o.s.s.n.e.EmailNotificationChannel] Fail to send test email to xxxxxxx@xxxxx.xxx: {} org.apache.commons.mail.EmailException: Sending the email to the following server failed : 1X.XXX.XX.X1:587 ... Caused by: javax.mail.MessagingException: Could not convert socket to TLS ... Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target ... Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target ... Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
相关命令执行结果
- 直接连接SMTP端口:
$ echo | openssl s_client -connect 1X.XXX.XX.X1:587
返回:
CONNECTED(00000003) 140269928117568:error:1408F10B:SSL routines:ssl3_get_record:wrong version number:../ssl/record/ssl3_record.c:331: --- no peer certificate available --- No client certificate CA names sent --- SSL handshake has read 5 bytes and written 283 bytes Verification: OK --- New, (NONE), Cipher is (NONE) Secure Renegotiation IS NOT supported Compression: NONE Expansion: NONE No ALPN negotiated Early data was not sent Verify return code: 0 (ok) ---
- 使用STARTTLS连接SMTP端口:
$ echo | openssl s_client -connect 1X.XXX.XX.X1:587 -starttls smtp
返回:
CONNECTED(00000003) Can't use SSL_get_servername depth=2 CN = CA-ROOT verify return:1 depth=1 DC = local, DC = regulator, CN = CA-SUB verify return:1 depth=0 C = PL, ST = Aaa, L = Bbb, O = Ccc, OU = Ddd, CN = [DOMAIN.NAME] verify return:1 --- Certificate chain 0 s:C = PL, ST = Aaa, L = Bbb, O = Ccc, OU = Ddd, CN = [DOMAIN.NAME] i:DC = local, DC = regulator, CN = CA-SUB 1 s:DC = local, DC = regulator, CN = CA-SUB i:CN = CA-ROOT --- Server certificate -----BEGIN CERTIFICATE----- [...CERTIFICATE...] -----END CERTIFICATE----- subject=C = PL, ST = Aaa, L = Bbb, O = Ccc, OU = Ddd, CN = [DOMAIN.NAME] issuer=DC = local, DC = regulator, CN = CA-SUB --- No client certificate CA names sent Peer signing digest: SHA256 Peer signature type: RSA Server Temp Key: ECDH, P-256, 256 bits --- SSL handshake has read 3596 bytes and written 498 bytes Verification: OK --- New, TLSv1.2, Cipher is ECDHE-RSA-AES256-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-SHA384 Session-ID: [...SESSION ID...] Session-ID-ctx: Master-Key: [...MASTER ID...] PSK identity: None PSK identity hint: None SRP username: None Start Time: 1666767236 Timeout : 7200 (sec) Verify return code: 0 (ok) Extended master secret: yes --- 250 CHUNKING DONE
解决步骤
从日志中的PKIX path building failed错误可以看出,问题出在JVM没有信任Exchange的自签名证书链。虽然证书已经安装到系统truststore,但SonarQube使用的是自身JVM的truststore,而非系统的,因此需要将完整的证书链导入到SonarQube所用JVM的信任库中。
具体操作:
导出完整证书链
从openssl s_client -starttls smtp的输出中,复制完整的证书链(从-----BEGIN CERTIFICATE-----到-----END CERTIFICATE-----,包括所有层级的证书),保存为exchange-cert-chain.pem文件。找到SonarQube使用的JVM路径
执行以下命令查看SonarQube的JVM路径:sudo su - sonarqube -c "echo \$JAVA_HOME"如果没有设置
JAVA_HOME,可以查看SonarQube启动脚本中的JVM路径,或者使用which java找到系统默认JVM。导入证书到JVM truststore
使用keytool命令将证书链导入到JVM的cacerts信任库中:sudo keytool -import -alias exchange-smtp -keystore $JAVA_HOME/lib/security/cacerts -file exchange-cert-chain.pem默认的
cacerts密码是changeit,根据提示输入密码并确认信任该证书。重启SonarQube服务
sudo systemctl restart sonarqube额外配置(可选,如果仍有问题)
如果导入证书后还是失败,可以在SonarQube的配置文件sonar.properties中添加以下参数,强制指定使用的truststore:sonar.web.javaAdditionalOpts=-Djavax.net.ssl.trustStore=/path/to/your/cacerts -Djavax.net.ssl.trustStorePassword=changeit替换
/path/to/your/cacerts为实际的JVM truststore路径,然后再次重启SonarQube。
另外,注意日志中显示useAuth false,如果Exchange服务器需要NTLM认证,可能还需要在SonarQube配置中添加SMTP用户名和密码,确保认证信息正确。
内容的提问来源于stack exchange,提问作者KChyl

