Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
74f9daa
feat: add custom root CA certificate import support
Piyush0049 Feb 13, 2026
b474ae5
fix: force LF line endings for scripts and Dockerfiles
Piyush0049 Feb 13, 2026
c161d52
fix: refine CA cert import and improve script robustness
Piyush0049 Feb 13, 2026
4bc1903
feat: add support for custom root CA certificates
Piyush0049 Feb 13, 2026
f050b58
fix: restore files and apply clean custom CA cert feature
Piyush0049 Feb 13, 2026
f6a80a6
fix: resolve JAVA_HOME inside container in CA cert test
Piyush0049 Feb 13, 2026
a6daba6
fix: use keytool instead of openssl for test cert generation
Piyush0049 Feb 13, 2026
15f288b
fix: run cert generation as root to avoid permission denied
Piyush0049 Feb 13, 2026
d0e9b6b
Merge remote-tracking branch 'upstream/master' into feature/custom-ca…
Piyush0049 Feb 14, 2026
d5aab3a
fix: address CI test failures with improved robustness and permissions
Piyush0049 Feb 14, 2026
bf2bc9d
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Feb 15, 2026
287486e
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Feb 15, 2026
96d58c0
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Feb 20, 2026
2368d53
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Mar 6, 2026
a9e5b71
refactor: remove CA cert import feature per reviewer feedback
Piyush0049 Mar 6, 2026
fa410e0
fix: simplify CA certificate init-container test
Piyush0049 Mar 6, 2026
9d373c7
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Mar 16, 2026
bf4eb34
Merge branch 'master' into feature/custom-ca-cert-import
Piyush0049 Mar 16, 2026
6b54858
Merge branch 'master' into feature/custom-ca-cert-import
lemeurherve May 22, 2026
0bb6014
final jenkins.io link
lemeurherve Jun 19, 2026
3d6655e
Merge branch 'master' into feature/custom-ca-cert-import
lemeurherve Jun 19, 2026
a9d359e
Merge branch 'master' into feature/custom-ca-cert-import
lemeurherve Jun 24, 2026
9c28382
Merge branch 'master' into feature/custom-ca-cert-import
lemeurherve Jul 7, 2026
ab2b064
Merge branch 'master' into feature/custom-ca-cert-import
lemeurherve Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ To potentially solve the issue, start the container specifying a dns server (for
docker run -p 8080:8080 -p 50000:50000 --restart=on-failure --dns 1.1.1.1 --dns 8.8.8.8 jenkins/jenkins:lts-jdk21
```

## Custom CA Certificates

You can add custom root CA certificates to the Jenkins Java keystore by volume-mounting `.crt` or `.pem` files into `/usr/share/jenkins/ref/certs/`. The certificates will be automatically imported at container startup.

```bash
docker run -p 8080:8080 -v /path/to/my-certs:/usr/share/jenkins/ref/certs:ro jenkins/jenkins:lts-jdk21
```

You can also specify a custom directory for certificates using the `JENKINS_CUSTOM_CERTS_DIR` environment variable:

```bash
docker run -p 8080:8080 -e JENKINS_CUSTOM_CERTS_DIR=/custom/path -v /path/to/my-certs:/custom/path:ro jenkins/jenkins:lts-jdk21
```

## Passing Jenkins launcher parameters

Arguments you pass to docker running the Jenkins image are passed to jenkins launcher, so for example you can run:
Expand Down
5 changes: 5 additions & 0 deletions alpine/hotspot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,15 @@ ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

# Allow the jenkins user to import custom CA certificates at runtime
RUN cp "${JAVA_HOME}/lib/security/cacerts" "${JAVA_HOME}/lib/security/cacerts.original" \
&& chown ${user}:${group} "${JAVA_HOME}/lib/security/cacerts"

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY import-custom-certs.sh /usr/local/bin/import-custom-certs.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.550
Expand Down
5 changes: 5 additions & 0 deletions debian/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,15 @@ ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

# Allow the jenkins user to import custom CA certificates at runtime
RUN cp "${JAVA_HOME}/lib/security/cacerts" "${JAVA_HOME}/lib/security/cacerts.original" \
&& chown ${user}:${group} "${JAVA_HOME}/lib/security/cacerts"

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY import-custom-certs.sh /usr/local/bin/import-custom-certs.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.550
Expand Down
61 changes: 61 additions & 0 deletions import-custom-certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash
# Script to import custom root CA certificates into the Java keystore.
# It handles .crt and .pem files mapped into the certs directory.

# Ensure JAVA_HOME is set, default to a common path if not
if [ -z "${JAVA_HOME}" ]; then
if [ -d "/opt/java/openjdk" ]; then
export JAVA_HOME="/opt/java/openjdk"
elif [ -d "/usr/lib/jvm/java-11-openjdk" ]; then
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
fi
fi

if [ -z "${JAVA_HOME}" ]; then
echo "ERROR: JAVA_HOME is not set and could not be determined." >&2
exit 0 # Don't crash the container
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded: we always set up JAVA_HOME in the Docker images. Feels like defensive programming or generated code. I suggest to simplify into jenkins.sh

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, this is unnecessary. Since JAVA_HOME is always set by the dockerfile, I will remove the entire detection block. I added it out of caution for general script portability,, but it doesnt belong in a controlled Docker image context.


: "${REF:="/usr/share/jenkins/ref"}"
: "${JENKINS_CUSTOM_CERTS_DIR:="${REF}/certs"}"

CACERTS_KEYSTORE="${JAVA_HOME}/lib/security/cacerts"
CACERTS_PASSWORD="${CACERTS_PASSWORD:-changeit}"

if [ ! -d "${JENKINS_CUSTOM_CERTS_DIR}" ]; then
exit 0
fi

echo "Scanning for custom certificates in ${JENKINS_CUSTOM_CERTS_DIR}..."

# Find certs and process them one by one
# Using a temp file for the list to avoid pipe subshell issues with while loop
cert_list=$(mktemp)
find "${JENKINS_CUSTOM_CERTS_DIR}" -maxdepth 1 -type f \( -name "*.crt" -o -name "*.pem" \) 2>/dev/null > "${cert_list}"

while read -r cert_file; do
if [ -z "${cert_file}" ]; then continue; fi

cert_name=$(basename "${cert_file}")
alias="custom-${cert_name%.*}"

echo "Checking: ${cert_name} (alias: ${alias})"

# Check if already exists
if keytool -list -keystore "${CACERTS_KEYSTORE}" -storepass "${CACERTS_PASSWORD}" -alias "${alias}" >/dev/null 2>&1; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keytool binary should be inferred from JAVA_HOME, same as the keystore (but for a binary)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, i will update the script to explicitly use ${JAVA_HOME}/bin/keytool to ensure we are using the binary associated with the current JVM.

echo " Certificate alias '${alias}' already exists, skipping."
continue
fi

echo " Importing: ${cert_name} ..."
if keytool -importcert -noprompt -keystore "${CACERTS_KEYSTORE}" -storepass "${CACERTS_PASSWORD}" -alias "${alias}" -file "${cert_file}" >/dev/null 2>&1; then
echo " Successfully imported ${cert_name}"
else
echo " WARNING: Failed to import ${cert_name}. Check file format and permissions." >&2
fi
done < "${cert_list}"

rm -f "${cert_list}"

echo "Custom CA certificate import process complete."
exit 0
5 changes: 5 additions & 0 deletions jenkins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ fi
: "${COPY_REFERENCE_FILE_LOG:="${JENKINS_HOME}/copy_reference_file.log"}"
: "${REF:="/usr/share/jenkins/ref"}"

# Import custom CA certificates if the script exists
if [ -f /usr/local/bin/import-custom-certs.sh ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this condition? Either the script is part of the image or it is not. I suggest to remove the condition which makes no sense.

Did you use an LLM for this (curious question, won't block your contribution but will help us to focus the review)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. Since the script is explicitly copied in the dockerfile, the file existence check is redundant.. I will remove it.
Regarding your question: yes, I used an LLM to help draft the implementation

bash /usr/local/bin/import-custom-certs.sh || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bash /usr/local/bin/import-custom-certs.sh || true
/usr/local/bin/import-custom-certs.sh
  • The hearth of a Docker image is that we finely control the environment: no need to invoke bash as the child shell script must have proper permissions to execute. On other contexts (than a Docker image) it would be a good practise though.
  • Why not failing if the certificate import fails? Looks like a major error which should prevent Jenkins to start no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid points, thanks.

  • Usage of bash: Agreed. Since we control the Docker environment and the file permissions, I will execute the script directly.
  • Failing on error: You are right. If a user provides custom certificates and the import fails, Jenkins should fail to start rather than silently ignoring the error and proceeding with potentially untrusted connections. I will remove the || true

fi

if ! [ -r "${JENKINS_HOME}" ] || ! [ -w "${JENKINS_HOME}" ]; then
echo "INSTALL WARNING: User: ${USER} missing rw permissions on JENKINS_HOME: ${JENKINS_HOME}"
fi
Expand Down
5 changes: 5 additions & 0 deletions rhel/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,15 @@ ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-and-war /javaruntime $JAVA_HOME
COPY --from=jre-and-war /war/jenkins.war /usr/share/jenkins/jenkins.war

# Allow the jenkins user to import custom CA certificates at runtime
RUN cp "${JAVA_HOME}/lib/security/cacerts" "${JAVA_HOME}/lib/security/cacerts.original" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intent of this instruction exactly? I don't see why duplicating certificates and changing permissions has any reason to be?

@Piyush0049 Piyush0049 Feb 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize the chown introduces a security concern, if Jenkins is compromised, the attacker gains write access to the truststore. A safer alternative would be to copy cacerts to JENKINS_HOME at startup and point the JVM at that copy via -Djavax.net.ssl.trustStore, keeping the system keystore root-owned. Would you prefer that approach?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I'm asking about the purpose of this line change:

  • Why making a backup of the certificates? What is the exact use case or reason? (at first sight, it makes no sense to me so an explanation is expected)
  • And then why even using the chown command? What problem does it solve exactly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’re using chown so the non-root jenkins user actually has permission to update the system truststore (cacerts) at runtime. Without this, the keytool command in our entrypoint just hits a permissions wall and fails. I’ve also added a cacerts.original backup as a safety net, just in case we need to roll back to the default upstream state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing such a backup is more a VM/bare metal behavior. In a container image, usually aimed at being immutable, it's better to use volumes. Your proposal above to copy the cert store at runtime makes better sense.

I'm more worried by the permissions change to jenkins. It's opening a security door which we might now want to open.

I wonder if, instead, documenting how to generate the new cacert would'nt be better instead? (either with docker run commands like your bats test and showing an example with a Dockerfile since such certificate are usually not updated often: baking them in user custom image could help them while keeping the cacert with proper root permissions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both approaches sound good to me. Once everyone align on the final scope, let me know and I'll get started on the changes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth a page in www.jenkins.io instead? WDYT @krisstern @timja @MarkEWaite ?

I like the idea of documentation, especially with the idea of Docker compose. There are other places on jenkins.io that would benefit from more use of Docker compose. This would be a good step towards wider use of Docker compose in the Jenkins documentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the alignment everyone! I'll:

  • Update this PR to include just the validation test (per @timja's suggestion)
  • Open a separate PR on jenkins.io with the full documentation page covering the Docker Compose and Kubernetes init-container approaches
  • Add a brief "Custom CA Certificates" section in the README here linking to the jenkins.io docs page

Would that work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the alignment everyone! I'll:

  • Update this PR to include just the validation test (per @timja's suggestion)
  • Open a separate PR on jenkins.io with the full documentation page covering the Docker Compose and Kubernetes init-container approaches
  • Add a brief "Custom CA Certificates" section in the README here linking to the jenkins.io docs page

Would that work?

@MarkEWaite @timja @krisstern @dduportal Hi everyone, just a gentle ping on this! Please let me know if this plan looks good to you all. I'd love to get started on these changes once we have alignment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks good to me, thanks!

&& chown ${user}:${group} "${JAVA_HOME}/lib/security/cacerts"

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY import-custom-certs.sh /usr/local/bin/import-custom-certs.sh
COPY jenkins-plugin-cli.sh /bin/jenkins-plugin-cli

ARG JENKINS_VERSION=2.550
Expand Down
29 changes: 29 additions & 0 deletions tests/runtime.bats
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,32 @@ runInScriptConsole() {
@test "[${SUT_DESCRIPTION}] ensure that 'ps' command is available" {
command -v ps # Check for binary presence in the current PATH
}

@test "[${SUT_DESCRIPTION}] custom CA certificate is imported into keystore" {
local container_name test_cert_dir
container_name="$(get_sut_container_name)"
cleanup "${container_name}"
test_cert_dir="$(mktemp -d)"

# Generate a self-signed test CA certificate using keytool from the SUT image
# (avoids dependency on openssl being installed on the CI host)
docker run --rm --user root -v "${test_cert_dir}:/certs" "${SUT_IMAGE}" \
bash -c 'keytool -genkeypair -alias testca -keyalg RSA -keysize 2048 \
-dname "CN=Test CA" -validity 1 -keypass changeit \
-keystore /tmp/test.jks -storepass changeit 2>/dev/null && \
keytool -exportcert -alias testca -rfc \
-keystore /tmp/test.jks -storepass changeit \
-file /certs/test-ca.crt 2>/dev/null && \
chmod -R 777 /certs'

# Start Jenkins with the test cert volume-mounted
docker run -d --name "${container_name}" \
-v "${test_cert_dir}:/usr/share/jenkins/ref/certs:ro" \
"${SUT_IMAGE}"

# Wait for import and verify the certificate was added to the keystore
retry 30 5 docker exec "${container_name}" \
keytool -list -cacerts -storepass changeit -alias custom-test-ca

rm -rf "${test_cert_dir}"
}