Jenkins · lesson 7 of 7
Jenkins Plugin Pinning and Upgrades
Pin Jenkins plugin versions, resolve dependency conflicts, check security warnings and available updates, and test a core bump before rolling it out.
By SkillByExample · Updated
plugins.txt and jenkins-plugin-cli from the previous lesson pin four plugins by exact
version. This lesson is about everything that pin doesn’t cover: the plugins those four
depend on, what breaks when a pin outgrows the Jenkins core underneath it, and how to
check both before an upgrade reaches a real controller.
1) A pin doesn’t freeze the whole dependency tree
plugins.txt names four plugins:
configuration-as-code:2136.veb_3a_554550b_f
job-dsl:3732.v9a_c49a_61a_313
credentials:1514.vb_01eb_014e7d4
plain-credentials:199.v9f8e1f741799
The controller built from it has 25:
$ docker exec jcasc-demo jenkins-plugin-cli --list
...
Resulting plugin list:
antisamy-markup-formatter 173.v680e3a_b_69ff3
asm-api 9.11.1-226.vff833a_e4c58e
bootstrap5-api 5.3.8-1048.va_c299057e35c
bouncycastle-api 2.30.1.86.2-310.v4655f6742752
branch-api 2.1303.v9f3b_95dc329d
...
workflow-support 1028.vc22910142650
Done
The other 21 are transitive dependencies, each resolved to whatever the update center’s
current metadata says is compatible — jenkins-plugin-cli’s --latest flag defaults to
true, meaning “use the newest version of every dependency, even for a pinned
top-level plugin.” Building the same plugins.txt again in six months can install
different dependency versions than it did today, with the four pins unchanged. Pass
--latest false to pin the whole tree instead of just the top level — section 6 shows why
that trade-off isn’t free either.
A pinned plugin also brings its own minimum version of a shared dependency, and that
minimum can be lower than what’s already pinned at the top level. Pinning
job-dsl:3732.v9a_c49a_61a_313 alongside configuration-as-code:2136.veb_3a_554550b_f
hits exactly that, because job-dsl only requires an older configuration-as-code as its
floor:
$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.580.1 --plugin-file /work/plugins-conflict-demo.txt --verbose
...
job-dsl depends on:
commons-lang3-api 3.18.0-98.v3a_674c06072d
commons-text-api 1.13.1-176.v74d88f22034b_
configuration-as-code 1963.v24e046127a_3f
...
Skipping dependency configuration-as-code:1963.v24e046127a_3f and its sub-dependencies, because there is a higher version defined on the top level - configuration-as-code:2136.veb_3a_554550b_f
jenkins-plugin-cli resolves this itself — the higher of the two versions wins, and only
one copy of configuration-as-code gets installed, satisfying job-dsl’s floor with
room to spare. Section 5 shows the case where that kind of resolution isn’t possible: not
a minimum a higher pin can satisfy, but a plugin that needs a Jenkins core newer than the
one actually running.
2) Listing what’s actually on a controller
Two more ways to answer “what’s installed” beyond jenkins-plugin-cli --list at build
time (section 1): jenkins-cli.jar against a live instance, and the REST API.
$ docker run --rm -v "$(pwd):/work" eclipse-temurin:21 java -jar /work/jenkins-cli.jar -s http://host.docker.internal:8080/ -auth admin:s3cr3t-admin-pw list-plugins | grep -E "^(configuration-as-code|credentials|job-dsl|plain-credentials) "
configuration-as-code Configuration as Code Plugin 2136.veb_3a_554550b_f
credentials Credentials Plugin 1514.vb_01eb_014e7d4
job-dsl Job DSL 3732.v9a_c49a_61a_313
plain-credentials Plain Credentials Plugin 199.v9f8e1f741799
$ curl -s -u admin:s3cr3t-admin-pw "http://localhost:8080/pluginManager/api/json?pretty=true&tree=plugins%5BshortName,version%5D" | head -11
{
"_class" : "hudson.LocalPluginManager",
"plugins" : [
{
"shortName" : "antisamy-markup-formatter",
"version" : "173.v680e3a_b_69ff3"
},
{
"shortName" : "asm-api",
"version" : "9.11.1-226.vff833a_e4c58e"
},
jenkins-cli.jar needs a Java 21 runtime — it’s compiled for the same class file version
as the controller, and JDKs older than 21 can’t load it, so the command above runs it
inside a throwaway eclipse-temurin:21 container instead of directly, reaching the
published port through host.docker.internal.
3) Checking for known vulnerabilities before you pin
jenkins-plugin-cli carries its own security-warning feed, independent of the update
check in section 4.
$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugin-file /work/plugins.txt
Done
Nothing printed beyond Done — none of the four pinned plugins currently carry a known
advisory. --no-download keeps this a read-only query instead of actually writing
plugin files, but it isn’t what determines whether a warning prints: that depends on
whether the queried plugin:version is already sitting on disk wherever you run this. A
controller that already has the exact pinned versions installed has nothing left to
resolve, so the check silently reports nothing either way, warnings or not — run it
against a clean pull of the base image, not the controller you already built (Practice 3
shows this exact gap with the vulnerable versions from section 6). --view-all-security-warnings
ignores plugins.txt and lists every plugin in the entire update center with an open
advisory, regardless of whether you use it:
$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-all-security-warnings | wc -l
1513
$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-all-security-warnings | head -5
AdaptivePlugin - Arbitrary code execution vulnerability
AnchorChain - Stored XSS vulnerability
ApicaLoadtest - Tokens stored and displayed in plain text
BlazeMeterJenkinsPlugin - Missing permission check allows enumerating credentials IDs
DotCi - RCE vulnerability
Run the scoped form against your own plugins.txt before every upgrade; the unscoped
form is a catalog, not a check.
4) Checking for available updates
--available-updates compares named plugin:version pins against the current update
center, not just whatever’s installed:
$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.580.1 --available-updates --plugins job-dsl:1.90 configuration-as-code:1700.v09b_045ee9299
Available updates:
job-dsl (1.90) has an available update: 3732.v9a_c49a_61a_313
configuration-as-code (1700.v09b_045ee9299) has an available update: 2136.veb_3a_554550b_f
Run it against the plugins this lesson already pins to the current latest, and there’s nothing to report:
$ docker run --rm -v "$(pwd):/work" --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --available-updates --plugin-file /work/plugins.txt
No available updates
5) When a pin is too new for the core
Jenkins core itself has a required-version floor per plugin release. Try installing the
current configuration-as-code pin onto an older core —
jenkins/jenkins:2.480-jdk21 instead of the 2.580.1 this topic otherwise uses:
# Dockerfile.oldcore
FROM jenkins/jenkins:2.480-jdk21
COPY plugins-cas-only.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
$ docker build -f Dockerfile.oldcore -t jenkins-oldcore-fail . > build.log 2>&1; echo "exit=$?"
exit=1
$ grep -E "requires a greater version|ERROR: process" build.log
#7 14.05 font-awesome-api (7.2.0-965.ve3840b_696418) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 json-api (20260814-226.v20f9685d642c) requires a greater version of Jenkins (2.492.1) than 2.480,
#7 14.05 asm-api (9.11.1-226.vff833a_e4c58e) requires a greater version of Jenkins (2.492.3) than 2.480,
#7 14.05 configuration-as-code (2136.veb_3a_554550b_f) requires a greater version of Jenkins (2.541.1) than 2.480,
#7 14.05 workflow-support (1015.v785e5a_b_b_8b_22) requires a greater version of Jenkins (2.504.3) than 2.480,
#7 14.05 bootstrap5-api (5.3.8-895.v4d0d8e47fea_d) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 prism-api (1.30.0-717.vb_f8360844b_53) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 plugin-util-api (6.1192.v30fe6e2837ff) requires a greater version of Jenkins (2.516.3) than 2.480,
#7 14.05 scm-api (728.vc30dcf7a_0df5) requires a greater version of Jenkins (2.504.3) than 2.480,
#7 14.05 workflow-api (1384.vdc05a_48f535f) requires a greater version of Jenkins (2.504.1) than 2.480
#7 ERROR: process "/bin/sh -c jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt" did not complete successfully: exit code: 1
...
$ grep -oE "[a-zA-Z0-9-]+ \([^)]+\) requires a greater version" build.log | awk '{print $1}' | sort -u | wc -l
10
The build fails before the image even has a chance to run (exit=1) — ten distinct
plugins, by that last count, all name the exact core version they need, every one of
them higher than 2.480. This is
the same category of problem as last lesson’s numExecutor typo (section 4 there): JCasC
and jenkins-plugin-cli would both rather fail your build than let you ship a controller
that can’t start.
6) Downgrading the plugin instead of the core isn’t automatically safer
The other direction — keep the old core, pin an older, compatible plugin release — installs cleanly, but isn’t free:
# Dockerfile.oldcore-pinned
FROM jenkins/jenkins:2.480-jdk21
COPY plugins-cas-old.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --latest false --plugin-file /usr/share/jenkins/ref/plugins.txt
plugins-cas-old.txt pins configuration-as-code:1810.v9b_c30a_249a_4c, a release
contemporaneous with core 2.480:
$ docker build -f Dockerfile.oldcore-pinned -t jenkins-oldcore-pinned . > build2.log 2>&1; echo "exit=$?"
exit=0
$ grep -E "Security warnings|SECURITY-" build2.log | head -3
#7 7.449 Security warnings:
#7 7.449 structs (325.vcb_307d2a_2782): SECURITY-3371 Exposure of secrets through system log https://www.jenkins.io/security/advisory/2024-06-26/#SECURITY-3371
#7 7.450 script-security (1251.vfe552ed55f8d): SECURITY-3341 Multiple sandbox bypass vulnerabilities https://www.jenkins.io/security/advisory/2024-05-02/#SECURITY-3341
$ grep -E "^#7 1[01]\.|^#7 DONE" build2.log
#7 11.47 Done
#7 DONE 11.6s
$ grep -c "SECURITY-" build2.log
15
The build succeeds (exit=0), jenkins-plugin-cli installs it, and the warnings print
unprompted during the install — fifteen by that grep -c count — against the old
structs and script-security releases that configuration-as-code:1810 pulled in with
--latest false. Compare that to section 3’s Done-with-nothing on the current pins
against the current core: the plugin downgrade traded a loud build failure for a quiet
set of known vulnerabilities. Bumping the core and keeping the plugin current — the
previous lesson’s image — is the version of this upgrade that passes both checks.
Practice
These ran against the skillbyexample-jenkins-jcasc:good image, jcasc-demo container,
jenkins-cli.jar, and plugins.txt from this lesson and the previous one (Jenkins
2.580.1).
-
Diff
plugins.txt(4 lines) againstjenkins-plugin-cli --list’s “Resulting plugin list” (25 lines) run inside the image. Which plugins appear in the second list but not the first, and why are they there if nothing inplugins.txtnames them?Solution
$ docker exec jcasc-demo jenkins-plugin-cli --list 2>&1 | sed -n '/Resulting plugin list/,$p' | tail -n +2 | grep -v '^Done$' | grep -v '^$' | awk '{print $1}' > installed.txt $ awk -F: '{print $1}' plugins.txt > pinned.txt $ comm -23 <(sort installed.txt) <(sort pinned.txt) antisamy-markup-formatter asm-api bootstrap5-api bouncycastle-api branch-api caffeine-api cloudbees-folder commons-lang3-api commons-text-api font-awesome-api ionicons-api json-api plugin-util-api prism-api scm-api script-security snakeyaml-api structs workflow-api workflow-step-api workflow-support21 plugins, none named in
plugins.txt. Each is a dependency of one of the four pins —configuration-as-codealone needscaffeine-api,commons-lang3-api,commons-text-api,json-api,prism-api,snakeyaml-api, and the entireworkflow-*family (workflow-api,workflow-step-api,workflow-support) just to load, andjob-dslseparately pulls inbranch-apiandcloudbees-folder. None of them are optional;jenkins-plugin-cliresolved and installed them automatically from each pinned plugin’s own manifest. -
Run
--view-all-security-warningsand intersect it by name only against all 25 installed plugins from task 1. Does matching by name agree with section 3’s--view-security-warnings --plugin-file plugins.txtresult?Solution
$ docker exec jcasc-demo jenkins-plugin-cli --view-all-security-warnings | awk -F' - ' '{print $1}' | sort -u > all-warned.txt $ comm -12 <(sort installed.txt) all-warned.txt cloudbees-folder configuration-as-code credentials job-dsl plain-credentials script-security structs workflow-supportNo, and that’s the trap this task is pointing at: 8 of the 25 installed plugin names have an advisory somewhere in their history, which looks alarming next to section 3’s clean
Done. But--view-all-security-warningslists a plugin the moment any past release of it had a CVE — it says nothing about which release. Checking the exact installedname:versionpairs against a clean pull of the base image (not the controller that already has them installed — section 3 explained why that matters) still reports nothing:$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:362.va_b_695ef4fdf9 script-security:1436.v1082791135e4 cloudbees-folder:6.1106.v3a_d9a_6d2465e workflow-support:1028.vc22910142650 configuration-as-code:2136.veb_3a_554550b_f credentials:1514.vb_01eb_014e7d4 job-dsl:3732.v9a_c49a_61a_313 plain-credentials:199.v9f8e1f741799 DoneThe 8 name matches are all plugins whose older releases carried a CVE that the versions actually installed here have long since fixed — the same
structsandscript-securityreleases that section 6’s downgrade resurrects. Checking by name alone overstates risk; the scoped, versioned check against a clean image is the one that answers “is what I actually have installed vulnerable.” -
Run
--view-security-warningsfor exactlystructs:325.vcb_307d2a_2782andscript-security:1251.vfe552ed55f8d— the two versionsDockerfile.oldcore-pinnedresolved in section 6 — against a fresh pull ofjenkins/jenkins:2.480-jdk21, the same way task 2 queried the current versions against a freshlts-jdk21.Solution
$ docker run --rm --entrypoint="" jenkins/jenkins:2.480-jdk21 jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:325.vcb_307d2a_2782 script-security:1251.vfe552ed55f8d Security warnings: structs (325.vcb_307d2a_2782): SECURITY-3371 Exposure of secrets through system log https://www.jenkins.io/security/advisory/2024-06-26/#SECURITY-3371 script-security (1251.vfe552ed55f8d): SECURITY-3341 Multiple sandbox bypass vulnerabilities https://www.jenkins.io/security/advisory/2024-05-02/#SECURITY-3341 ... DoneThis reproduces the same warnings
Dockerfile.oldcore-pinned’s build already printed — confirming they come from those specific old releases, not from the newerjob-dsl/configuration-as-codepins this topic otherwise uses. Running the exact same query againstjenkins-oldcore-pinned— the image already built with these plugins on disk — instead of a fresh2.480-jdk21pull:$ docker run --rm --entrypoint="" jenkins-oldcore-pinned jenkins-plugin-cli --view-security-warnings --no-download --plugins structs:325.vcb_307d2a_2782 script-security:1251.vfe552ed55f8d DoneDone, no warnings at all — the same already-satisfied short-circuit from section 3, now hiding a real finding instead of confirming a clean one. Always run this check against a clean image, never the controller you already built from it. -
Pin
configuration-as-codeto a version between the two already tested —1971.vf9280461ea_89(requiredCore: 2.504.1) — against the2.480core. Does it succeed, fail outright, or fail with a different message than section 5’s?Solution
$ docker run --rm --entrypoint="" jenkins/jenkins:lts-jdk21 jenkins-plugin-cli --jenkins-version 2.480 --no-download --plugins configuration-as-code:1971.vf9280461ea_89 Multiple plugin prerequisites not met: ... configuration-as-code (1971.vf9280461ea_89) requires a greater version of Jenkins (2.504.1) than 2.480, ...Same failure mode as section 5, just a smaller required-core gap (
2.504.1vs.2.480, instead of2.541.1).jenkins-plugin-clidoesn’t have a “close enough” threshold — anyrequiredCoreabove the running core fails the same way, whether the gap is one release or twenty.