Plugin hygiene & attack surface
Where the CVEs really live.
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.
$ docker exec jenkins jenkins-plugin-cli \-d /var/jenkins_home/plugins --listInstalled plugins:configuration-as-code 1850.va_3a_f18f0e357credentials 1371.vfee6b_095f0a_3git 5.4.0git-client 5.0.0junit 1300.v03d9d8a_cf1fdmatrix-auth 3.2.2script-security 1341.va_2819b_414686ssh-slaves 3.1030.v5f5f2a_5a_ac73workflow-aggregator 600.vb_57cdd26fdd7workflow-cps 3968.vfffea_45a_ade6Bundled plugins:Resulting plugin list:configuration-as-code 1850.va_3a_f18f0e357credentials 1371.vfee6b_095f0a_3git 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.
$ 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_3git Git plugin 5.4.0script-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.
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:
$ jenkins-plugin-cli --plugins blueocean:1.27.16 --listAll requested plugins:blueocean 1.27.16Plugins that will be downloaded:blueocean 1.27.16blueocean-commons 1.27.16blueocean-config 1.27.16blueocean-dashboard 1.27.16blueocean-events 1.27.16blueocean-rest 1.27.16blueocean-web 1.27.16handlebars 3.0.8jackson2-api 2.17.0-379.v8ee1f76c5b_bepubsub-light 1.18sse-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 - exact versions, checked into gitconfiguration-as-code:1850.va_3a_f18f0e357credentials:1371.vfee6b_095f0a_3git:5.4.0matrix-auth:3.2.2script-security:1369.v9b_98a_4e95b_2d # bumped: closes the advisory above
$ jenkins-plugin-cli --plugin-file plugins.txt \--plugin-download-directory ./plugins-stagingDownloading plugin script-security from url: https://updates.jenkins.io/download/plugins/script-security/1369.v9b_98a_4e95b_2d/script-security.hpiscript-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.
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.