Importing internal CA to Jenkins



I’m trying to use a Jenkins job (pipeline) to read some information from a json endpoint and do something based on that information then.

The endpoint it is reading from is an internal one and the application is reachable via https with a certificate that is self-signed by our internal CA.

Here’s the example code, that is run by the pipeline to parse the json:

new JsonSlurper().parse(new URL('https://my.internal.url/info'))?.application?.git?.commit

To make it a little more complex, I’m using a Java binary from the Global Tool Configuration in the pipeline as well.

When I run the pipeline, I get the follwing error:

sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:392)
Caused: sun.security.validator.ValidatorException: PKIX path building failed
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:397)
    at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:302)
    at sun.security.validator.Validator.validate(Validator.java:260)
    at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1496)
Caused: javax.net.ssl.SSLHandshakeException
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1959)
    ...
Caused: groovy.json.JsonException: Unable to process url: https://my.internal.url/info
    at groovy.json.JsonSlurper.parseURL(JsonSlurper.java:416)
    at groovy.json.JsonSlurper.parse(JsonSlurper.java:379)
    ...

So now I’m trying to make this CA known to the JVM, that runs this code, but I cannot figure out how to do that.

I tried to download the pem-file for the root CA, add it to the system certs and then import this to the system-default cacerts file for java (/etc/ssl/certs/java/cacerts) by doing the following:

curl -Lso /etc/ssl/certs/rootca1.pem "<DOWNLOAD LINK>" 
    && chmod 777 /etc/ssl/certs/rootca1.pem 
    && mkdir -p /usr/share/ca-certificates/projectname 
    && cp /etc/ssl/certs/rootca1.pem /usr/share/ca-certificates/projectname/rootca1.crt 
    && echo "projectname/rootca1.crt" >> /etc/ca-certificates.conf 
    && update-ca-certificates -f

Afterwards, when I use a class to test ssl connections in java (like https://gist.github.com/4ndrej/4547029) and run java SSLPoke https://my.internal.url/info 443 I can successfully connect. The Jenkins pipeline still fails with the same error.

Then I figured, maybe the pipeline uses the java binary that is copied by the global tool configuration (even though this is not yet done in the Jenkinsfile) to the workspace, so I added the CA to the cacerts keystore of that tool, that is copied into the workspace ($WORKSPACE/tools/hudson.model.JDK/Java_8/jre/lib/security/cacerts).

And again afterwards I can use that binary with the SSLPoke class to successfully connect to the URL, but the pipeline still fails…

So I am out of ideas now… Has anyone experienced similar problems and managed to fix them? Without moving to officially signed certificates (which are not an option for the internal urls for various reasons, not even Let’s Encrypt).

Thanks in advance!

Answer

I did not manage to fix this but we have a workaround in place. We put the certificate into the slave container and regularl install it in all our Java installations on all our nodes:

def jdksJava8 = ['Java 8', 'Java 8 Oracle']
def jdksJava11 = ['Java 11']

def nodeNames = env.'NODE_NAME' ? [env.'NODE_NAME'] : getNodeNames().sort().findAll { it.startsWith('my-node') }

stage 'Install myRootCA', {

    currentBuild.displayName = "${env.'BUILD_NUMBER'}: ${env.'NODE_NAME' ?: 'all nodes'}"

    parallel nodeNames.collectEntries { nodeName ->
        [
            (nodeName): {
                node nodeName, {
                    jdksJava8.each { jdkName ->
                        withEnv(["JAVA_HOME=${tool jdkName}"]) {
                            sh '$JAVA_HOME/bin/java -version'
                            sh script: '$JAVA_HOME/bin/keytool -delete -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -alias myRootCA', returnStatus: true
                            sh script: '$JAVA_HOME/bin/keytool -import -trustcacerts -noprompt -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -alias myRootCA -file /usr/share/ca-certificates/myCompany/myRootCA.crt', returnStatus: true
                        }
                    }

                    jdksJava11.each { jdkName ->
                        withEnv(["JAVA_HOME=${tool jdkName}"]) {
                            sh script: '$JAVA_HOME/bin/keytool -delete -cacerts -storepass changeit -noprompt -alias myRootCA', returnStatus: true
                            sh script: '$JAVA_HOME/bin/keytool -importcert -cacerts -storepass changeit -noprompt -alias myRootCA -file /usr/share/ca-certificates/myCompany/myRootCA.crt', returnStatus: true
                        }
                    }
                }
            }
        ]
    }

}

@NonCPS
def getNodeNames() {
    jenkins.model.Jenkins.instance.nodes.collect { node -> node.name }
}

A job with this script now runs every night. And in case we have to restart a node we run it manually afterwards to install the certificate back.



Source: stackoverflow