CoursesJenkins foundations, done rightPlugin hygiene & attack surface

Plugin hygiene & attack surface

Where the CVEs really live.

Beginner12 min · lesson 14 of 16

A brand-new Jenkins asks you one friendly question before you have built anything: install the suggested plugins? Click yes and roughly thirty pieces of third-party software land on your server. A plugin works the way a browser extension does. It is a packaged add-on, a file ending in .hpi or .jpi, that Jenkins loads at startup to give itself a new skill: checking out a Git repository, launching build agents (the separate worker machines that actually run your builds) over SSH, reporting JUnit test results. Even the Pipeline engine you write Jenkinsfiles for is a plugin. The update center is the online catalog Jenkins fetches them all from. Now the part that matters. Every one of those add-ons runs inside the controller, the Jenkins server holding your jobs, your configuration and your credentials, with that server's full privileges. Which is why the danger lives in plugins rather than in Jenkins core.

The advisory numbers say the same thing out loud. Most Jenkins security advisories, the official vulnerability notices published at jenkins.io/security, are about plugins and not about the core server. A CVE (Common Vulnerabilities and Exposures, the public ID number given to a known security bug, the way a product recall gets a reference number) in a plugin is a hole in code you chose to install and then handed the keys to. The nastiest ones are RCE (remote code execution, meaning an attacker gets to run their own commands on your machine), and plenty of those are pre-authentication: anyone who can load your login page can exploit them with no account at all. So your attack surface, the complete set of things an attacker can aim at, is roughly your plugin list.

The plugins that bite hardest

Some categories deserve more suspicion than others. script-security is the plugin that sandboxes Groovy, the language your pipelines are written in. It fences off what a Jenkinsfile (the pipeline definition checked into your repository) or a shared library is allowed to call, so untrusted pipeline code cannot reach in and read the controller's secrets. That fence is a security boundary, so a bug in it is a straight escape into running code, and script-security has a long history of exactly those CVEs. Credential plugins are sensitive for the obvious reason: a flaw there leaks the very secrets Jenkins carefully stars out of build logs. Anything that parses input arriving over the network (XML, YAML, HTTP callbacks) invites the classic parser bugs, including injection, XXE (XML external entity, where a crafted document makes the parser open files it should never touch) and SSRF (server-side request forgery, where you trick the server into making requests on your behalf). And because plugins run on the controller rather than on an isolated agent, one plugin RCE hands over every job and every credential Jenkins holds.

Start by reading your own plugin list

You cannot secure a list you have never read. jenkins-plugin-cli, the Plugin Installation Manager Tool that ships inside the official Jenkins image, reads the plugins directory and prints every plugin it finds along with its exact version. Point it at a running instance's JENKINS_HOME/plugins and you get ground truth instead of the list you remember installing. The flag that names that directory is -d, whose long form is --plugin-download-directory, and the tool treats whatever .jpi files sit in there as your installed set.

shell — audit what is installed
$ docker exec jenkins jenkins-plugin-cli \
-d /var/jenkins_home/plugins --list
Installed plugins:
configuration-as-code 1850.va_3a_f18f0e357
credentials 1371.vfee6b_095f0a_3
git 5.4.0
git-client 5.0.0
junit 1300.v03d9d8a_cf1fd
matrix-auth 3.2.2
script-security 1341.va_2819b_414686
ssh-slaves 3.1030.v5f5f2a_5a_ac73
workflow-aggregator 600.vb_57cdd26fdd7
workflow-cps 3968.vfffea_45a_ade6
Bundled plugins:
Resulting plugin list:
configuration-as-code 1850.va_3a_f18f0e357
credentials 1371.vfee6b_095f0a_3
git 5.4.0
... (37 plugins total)

Count how many of those you never picked. Install one plugin and Jenkins quietly installs its transitive dependencies, the other plugins it needs in order to work, and each of those is more code running with the controller's privileges. Blue Ocean, the pretty pipeline visualization UI, is a single checkbox that drags in over forty plugins. You can see that entire list before you commit to it, and we do exactly that further down.

Patch on a calendar, not on a hunch

Half of plugin risk comes down to running yesterday's version after the fix has already shipped. A monthly patch pass closes the CVEs that automated scanners, and the attackers driving them, look for first. The update center compares what you have installed against the newest available release and flags anything behind, and it separately paints any plugin carrying a published security warning in red. You get the same comparison from the command line with the Jenkins CLI list-plugins command. A version in parentheses after a plugin name means a newer one is sitting there waiting.

shell — check for available updates
$ curl -sO http://localhost:8080/jnlpJars/jenkins-cli.jar
$ java -jar jenkins-cli.jar -s http://localhost:8080/ \
-auth admin:$API_TOKEN list-plugins | grep -iE 'script-security|^git |credentials'
credentials Credentials Plugin 1371.vfee6b_095f0a_3
git Git plugin 5.4.0
script-security Script Security 1341.va_2819b_414686 (1369.v9b_98a_4e95b_2d)
# the (1369...) tail = an update is available; script-security also
# carries a security warning in the Updates tab, so patch it first.

Treat a plugin carrying a security warning the way you would treat an unpatched public server. Patch it this week. If no fix exists yet, disable or remove the plugin and find another route rather than leaving a known hole open. Nobody hand-audits your instance. Attackers run scanners that fingerprint plugin versions against the public advisory list and go straight for whichever ones are exploitable. The gap between a fix shipping and you applying it is the exact window those scanners were built to catch, so the shorter you keep it, the less of your surface is ever live.

Two minutes of vetting before you click install

Adding a plugin is a security decision, so give it two minutes first. Open the plugin's page on plugins.jenkins.io and check three signals. Maintenance: is it still shipping releases, or is it flagged as up for adoption or deprecated? An abandoned plugin means the next CVE in it never gets fixed. Install count: a plugin running on a hundred thousand instances has had far more eyes on it than one running on two hundred. Open security warnings: both the plugin page and the Jenkins advisory list mark known-vulnerable versions. Prefer plugins that are actively maintained, widely used and warning-free. And ask, every single time, whether something you already have can do the job instead.

Vetting a plugin before install
A plugin you want to install
run this checklist before you click install
maintained?
Recent releases
Abandoned or 'up for adoption' means the next CVE never gets fixed. Walk away.
known CVEs?
Security warnings
Cross-check plugins.jenkins.io and jenkins.io/security. A warning on the current version is a no.
widely used?
Install count
Popular plugins get real scrutiny. Obscure ones can hide bugs nobody has looked for.
already covered?
Deduplicate
If a plugin you already run does the job, add nothing. Every plugin is surface.
All four have to pass. The cheapest, safest plugin is the one you decide not to install.

That vetting has to cover the dependencies too, because you inherit their risk in full. A plugin can look tiny on its own page and still pull in a dozen parsers, API shims and UI libraries you would never have chosen on purpose, and any one of them can carry its own CVE. Ask jenkins-plugin-cli to resolve a plugin without installing it and it prints the whole tree it would fetch, which is the real price of that one checkbox:

shell — reveal transitive dependencies
$ jenkins-plugin-cli --plugins blueocean:1.27.16 --list
All requested plugins:
blueocean 1.27.16
Plugins that will be downloaded:
blueocean 1.27.16
blueocean-commons 1.27.16
blueocean-config 1.27.16
blueocean-dashboard 1.27.16
blueocean-events 1.27.16
blueocean-rest 1.27.16
blueocean-web 1.27.16
handlebars 3.0.8
jackson2-api 2.17.0-379.v8ee1f76c5b_be
pubsub-light 1.18
sse-gateway 1.26
...
Resulting plugin list:
# 43 plugins pulled in by one install request

Rehearse the upgrade somewhere disposable

Do not upgrade plugins on production Jenkins because you happened to notice a red badge. An upgrade can bump a shared library that another plugin depends on, or change an API that a pipeline step calls, and some updates only take effect after a full controller restart. The safe loop is declarative. Keep a pinned plugins.txt in git as the single source of truth, install exactly those versions into a throwaway staging Jenkins, run a representative pipeline against it, and promote the file to production only once it stays green. Because every version is pinned, staging and production end up with byte-identical plugin sets.

plugins.txt — pinned source of truth
# plugins.txt - exact versions, checked into git
configuration-as-code:1850.va_3a_f18f0e357
credentials:1371.vfee6b_095f0a_3
git:5.4.0
matrix-auth:3.2.2
script-security:1369.v9b_98a_4e95b_2d # bumped: closes the advisory above
shell — build & smoke-test in staging
$ jenkins-plugin-cli --plugin-file plugins.txt \
--plugin-download-directory ./plugins-staging
Downloading plugin script-security from url: https://updates.jenkins.io/download/plugins/script-security/1369.v9b_98a_4e95b_2d/script-security.hpi
script-security downloaded successfully
...
Done
$ docker run --rm -d --name jenkins-staging -p 8081:8080 \
-v $PWD/plugins-staging:/usr/share/jenkins/ref/plugins \
jenkins/jenkins:lts-jdk17
# run a representative pipeline on :8081; only if it stays green,
# promote plugins.txt to the production image and redeploy.
Deleting a plugin can break jobs you never touched
Plugins lean on each other, so you cannot pull a .jpi file out of the directory and call it pruning. Remove or disable a plugin that others depend on and Jenkins refuses to load those dependents at the next restart. The pipeline steps they provided vanish, jobs go red with 'No such DSL method', and nothing in the error points back at what you did. Before you prune anything, read the dependency graph under Manage Jenkins > Plugins and uninstall the dependents first. Snapshot JENKINS_HOME as well, so a bad plugin change is one restore away from undone.
Quick check
01A plugin you depend on picks up a fresh security advisory, and the maintainer has not shipped a fix yet. What is the safest move?
Incorrect — Firewalls fail and insiders exist. 'Internal only' is not a control you can lean on against a known pre-auth RCE.
Correct — An unfixed advisory is an unpatched server. Cut the surface now instead of waiting and hoping.
Incorrect — With no fix out yet, the newest release is vulnerable too. A version bump only helps once the advisory is marked resolved.
Incorrect — Plugin CVEs, pre-auth RCE above all, are scanned for constantly. Ignoring an advisory is how instances get taken over.
02Running the Jenkins CLI list-plugins prints: script-security Script Security 1341.va_2819b_414686 (1369.v9b_98a_4e95b_2d). What is the version in parentheses telling you?
Incorrect — A parenthesized version is not a safety floor. It is the newer release you could move up to.
Incorrect — The installed version is the first one, 1341..., and the parenthesized one is a different, newer release.
Correct — A version in parentheses after a plugin is the newer release you can update to.
Incorrect — That is a plugin version string, not a CVE ID. CVE identifiers look like CVE-YYYY-NNNNN.
03To trim attack surface you delete a plugin's .jpi file straight out of the plugins directory, because no job seems to use it. After the next restart, several unrelated pipelines go red with 'No such DSL method' errors. What happened?
Correct — Plugins carry transitive dependencies, so removing one that others need breaks the dependents and their steps become undefined.
Incorrect — Deleting the file does not corrupt the home directory. The failure is dependents losing a plugin they required.
Incorrect — Deleting the file removes the plugin. Jenkins does not quietly re-download and upgrade it on the next restart.
Incorrect — The timing points straight at the deletion. Steps from the removed plugin and its dependents are now missing.

That pinned plugins.txt is half of a Jenkins you can rebuild from scratch. The next lesson, Configuration as Code (JCasC), covers the other half by describing the controller's own settings in a single jenkins.yaml, so the whole instance, plugins included, can be torn down and rebuilt from files instead of from somebody's memory of which boxes they once ticked.

Try this

Work through “Rehearse the upgrade somewhere disposable” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: deleting a plugin can break jobs you never touched. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related