Connect Azure Devops to SonarQube with self-signed certificates

When you are running SonarQube on-premise, it is most likely that you want to use a self-signed certificate to keep the tool secured. Unfortunately, SonarQube is a Java based application, so getting it up and running is not as straight-forward as expected. In my case, after configuring SQ for HTTPS, the SonarQube extension for Azure Devops started complaining that it detected a self-signed certificate in the chain.

[error][SQ] API GET ‘/api/server/version’ failed, error was: {“code”:”SELF_SIGNED_CERT_IN_CHAIN”}

There are 3 basic steps that are performed in the Azure Devops pipeline. First step is to prepare the SonarQube analysis. The second step is to compile your code and run unit tests. The last step is to complete the SonarQube analysis.

SonarQube uses both NPM and Java to do its thing. This certificate error is caused by NPM. Solving the SELF_SIGNED_CERT_IN_CHAIN error requires a PEM file.
A PEM file is usually used to import several certificates at once. Creating the file is easy. You need to make an export from your entire certificate chain in Base-64 encoded X.509 format. Open each certificate with notepad and copy the entire content into your certificates.pem file.

It should look something like this:

(Your Primary SSL certificate: your_sonarqube.cer)
(Your Intermediate certificate: DigiCertCA.cer)
(Your Root certificate: TrustedRoot.cer)

Place your PEM file and add the following environment variable on your build server:

Value: C:\Certificates\Certificates.pem

Restart the build agents, and we have success!… euhmm, well, sort off…

After a closer look at the error log, the following line popped out:

Caused by: PKIX path building failed: unable to find valid certification path to requested target

Apparently, the second stage does not use the NPM settings we just did. We actually need to add the certificate chain we exported to create the PEM file into Java certificate store. This can be done by the following command:

Keytool.exe -import -cacerts -storepass changeit -alias "certificate name" -file "<Location>\Certificate.cer

Keytool can be found in the bin directory of your Java folder. In my case it is C:\Program Files\OpenJDK\jdk-15\bin\keytool.exe

Are we done? Not yet. SonarQube needs to know where the keystore is located. This can be done with an extra environment variable.

Value:"C:\Program Files\OpenJDK\jdk-15\lib\security\cacerts""C:\Program Files\OpenJDK\jdk-15\lib\security\cacerts"

And there we have it!

To make this all a bit easier for the future, I have written a PowerShell script for all these actions. All you need to run this script are the CERT files, the PEM file and OpenJDK installed on your build server.

Param (
    [string]$Location = 'C:\Certificates',
    [string]$StorePass = 'changeit',
    [string]$PemFile = "$Location\certificates.pem"

Write-Warning "The output of this script might contain errors. keytool.exe is using exit code 1 for a successful run :/ . `"NotSpecified: (Certificate was added to keystore`" error can safely be ignored."

#Find the keytool
Write-Host "Locating keytool.exe ..."
[string]$Keytool = (where.exe /r "$env:programfiles\OpenJDK" keytool.exe | Sort-Object -Descending)[0]
"Keytool: $Keytool"
$KeyStore = $KeyTool.Replace('bin\keytool.exe', 'lib\security\cacerts')

#Add certs
$Certificates = (Get-ChildItem $Location -Filter *.cer).baseName
Foreach ($Certificate in $Certificates) {
    if (-not (. "$keytool" -list -storepass "$StorePass" -alias "$Certificate" -cacerts | select-string "trustedCertEntry")) {       
        #Add key (dirty code, but default keytool -import throws an error when it succeeds)
        write-host "Adding $Certificate to the keystore ..."
        . "$keytool" -import -cacerts -storepass "$StorePass" -alias "$Certificate" -file "$Location\$Certificate.cer" -noprompt #add key
        #test if its there
        if (-not (. "$keytool" -list -storepass "$StorePass" -alias "$Certificate" -cacerts | select-string "trustedCertEntry")) {       
            Write-Host "##vso[task.logissue type=error]Importing $Certificate unsuccessful."
    } else {
        Write-Host "$Certificate is already registered."

#Add Environment Variable
Write-Host "Adding 'NODE_EXTRA_CA_CERTS' to the Machine System variable."
[System.Environment]::SetEnvironmentVariable('NODE_EXTRA_CA_CERTS', $PemFile, [System.EnvironmentVariableTarget]::Machine)

Write-Host "Adding 'SONAR_SCANNER_OPTS' to the Machine System variable."
[System.Environment]::SetEnvironmentVariable("SONAR_SCANNER_OPTS", "`"$KeyStore`"`"$KeyStore`"", [System.EnvironmentVariableTarget]::Machine)

Don’t forget to restart your Azure Devops Agents after running the script, and you are good to go!