Overview

During a migration of our internal Jenkins controller from HTTP to HTTPS/TLS, several Jenkins pipelines running on Kubernetes agents began failing.

These pipelines use dynamically provisioned agents created by the Jenkins Kubernetes plugin, where each build runs inside a temporary pod.

After enabling TLS on the Jenkins controller, inbound agents were no longer able to establish a connection, preventing pipelines from starting.

This article explains:

  • the symptoms observed during the migration
  • why the Jenkins inbound agent (jnlp) failed to connect
  • how internal PKI affected TLS validation
  • the solution implemented using a custom Jenkins agent image

Environment Architecture

Our Jenkins pipelines run using dynamic Kubernetes agents.

Each pipeline execution creates a pod containing two containers:

The jnlp container is the actual Jenkins agent responsible for:

  • establishing the remoting connection with the Jenkins controller
  • managing the workspace
  • executing certain Jenkins operations such as checkout, stash, and unstash

Even if the build logic runs inside another container, the agent itself still runs in the jnlp container.


Symptoms

After migrating Jenkins to HTTPS, pipelines failed immediately when attempting to start Kubernetes agents.

The inbound agent logs showed errors similar to the following:

PKIX path building failed
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

The agent repeatedly attempted to connect but failed TLS validation, preventing Jenkins from establishing the remoting channel required to run the pipeline.

As a result, pipelines remained stuck during agent provisioning.


Root Cause

The issue was related to internal PKI infrastructure.

Our organization uses Step-CA as an internal certificate authority to issue TLS certificates for internal services.

When Jenkins was migrated to HTTPS, its TLS certificate was issued by this internal CA.

However, the default Jenkins inbound agent image: jenkins/inbound-agent

does not include the internal CA certificate.

Because of this, Java inside the agent container could not validate the TLS certificate chain presented by the Jenkins controller.

The TLS handshake therefore failed:


Solution: Custom Jenkins Inbound Agent Image

The solution was to build a custom Jenkins inbound agent image that includes the internal CA certificate.

This ensures that:

  • the Linux system trust store contains the internal CA
  • the Java trust store (cacerts) also contains the CA
  • TLS connections from the Jenkins agent succeed

Directory Structure

The custom image was built using the following structure:

jenkins-inbound-agent/
├─ Dockerfile
└─ roots.pem

The CA certificate was obtained from the internal Step-CA server:

curl -O https://ca.devops-db.internal/roots.pem

Dockerfile

The Dockerfile extends the official Jenkins inbound agent image and installs the internal CA certificate.

FROM jenkins/inbound-agent:latest-jdk17

USER root

COPY roots.pem /usr/local/share/ca-certificates/devops-db-root.crt

RUN update-ca-certificates && \
    keytool -importcert \
      -alias devops-db-root \
      -file /usr/local/share/ca-certificates/devops-db-root.crt \
      -cacerts \
      -storepass changeit \
      -noprompt

USER jenkins

This process performs two important actions:

  1. Adds the CA certificate to the system trust store
  2. Imports the certificate into the Java trust store

This is required because Jenkins remoting uses Java TLS.


Building the Image

The image can be built with:

docker build --platform linux/amd64 \
-t registry.devops-db.internal:5000/jenkins-inbound-agent:1.0.0 .

Tag the image as latest:

docker tag \
registry.devops-db.internal:5000/jenkins-inbound-agent:1.0.0 \
registry.devops-db.internal:5000/jenkins-inbound-agent:latest

Pushing the Image to the Registry

docker push registry.devops-db.internal:5000/jenkins-inbound-agent:1.0.0
docker push registry.devops-db.internal:5000/jenkins-inbound-agent:latest

Updating the Kubernetes Agent Configuration

After building the custom image, the Kubernetes pod template was updated to use it for the jnlp container. https://github.com/faustobranco/devops-db/tree/master/infrastructure/lib-utilities

Pod Template Function

def call(str_Build_id, str_image) {

  def obj_PodTemplate = """
        apiVersion: v1
        kind: Pod
        metadata:
          labels:
            some-label: "pod-template-${str_Build_id}"
        spec:
          securityContext:
            fsGroup: 1000
          containers:
          - name: jnlp
            image: registry.devops-db.internal:5000/jenkins-inbound-agent:latest
            imagePullPolicy: Always
          - name: container-1
            securityContext:
              runAsUser: 1000
              fsGroup: 1000
            image: "${str_image}"
            env:
            - name: CONTAINER_NAME
              value: "container-1"
            - name: ANSIBLE_HOST_KEY_CHECKING
              value: "False"
            volumeMounts:
            - name: shared-volume
              mountPath: /mnt
            command:
            - cat
            tty: true
          volumes:
          - name: shared-volume
            emptyDir: {}
"""
  return obj_PodTemplate
}

Example Pipeline Using the Updated Agent

The following pipeline runs using the updated pod template.

@Library('devopsdb-global-lib') _

import devopsdb.utilities.Utilities
def obj_Utilities = new Utilities(this)

pipeline {
    agent {
        kubernetes {
            yaml GeneratePodTemplate('1234-ABCD', 'registry.devops-db.internal:5000/img-jenkins-devopsdb:2.0')
            retries 2
        }
    }

    options {
        timestamps()
        skipDefaultCheckout(true)
    }

    stages {
        stage('Script') {
            steps {
                container('container-1') {
                    script {

                        def str_folder = "${env.WORKSPACE}/pipelines/python/log"
                        def str_folderCheckout = "/python-log"

                        obj_Utilities.CreateFolders(str_folder)

                        obj_Utilities.SparseCheckout(
                            'https://gitlab.devops-db.internal/infrastructure/pipelines/tests.git',
                            'master',
                            str_folderCheckout,
                            'usr-service-jenkins',
                            str_folder
                        )
                    }
                }
            }
        }

        stage('Cleanup') {
            steps {
                cleanWs deleteDirs: true, disableDeferredWipeout: true
            }
        }
    }
}

Validation

Before deploying the image, we verified that the CA certificate was correctly installed.

Run the container:

docker run --platform linux/amd64 -it \
registry.devops-db.internal:5000/jenkins-inbound-agent:1.0.0 bash

Verify the certificate in the Java trust store:

keytool -list -cacerts -storepass changeit | grep devops

Expected output:

devops-db-root, trustedCertEntry

TLS Connectivity Test

Connectivity to Jenkins can be tested using:

curl -v https://jenkins.devops-db.internal

Successful output includes:

SSL certificate verify ok

Result

After deploying the custom inbound agent image, Kubernetes agents were able to:

  • validate the Jenkins TLS certificate
  • establish the remoting connection
  • start pipelines successfully

Pipeline execution completed normally:

Finished: SUCCESS

Key Lessons

Jenkins agent images must trust internal PKI

If internal services use certificates issued by an internal CA, Jenkins agents must include that CA in their trust stores.


The JNLP container is critical in Kubernetes pipelines

Even if build steps run in another container, the jnlp container is still the Jenkins agent and must be able to establish secure connections.


HTTPS migrations impact CI/CD agents

Migrating infrastructure services such as Jenkins from HTTP to HTTPS often requires updating CI/CD agent images to trust the new certificate chain.