More than 70 Jenkins vulnerabilities were found during 2025, with most connected to plugins, while Shadowserver identified more than 45,000 internet-exposed Jenkins servers vulnerable to known exploits. That combination makes a neglected plugin list and a publicly reachable controller a far more urgent problem than another generic CI/CD security policy.

The useful response is not “update Jenkins someday.” It is a short remediation runbook with evidence at every stage: know what code is installed, decide what must change first, narrow who can reach the controller, and test the result without breaking the teams who ship through it.

1. Set a remediation boundary before touching the controller

Start by naming one Jenkins controller, one owner, and one maintenance window. Do not begin with “all CI servers,” because plugin dependencies, authentication arrangements, and agent connectivity are rarely identical across environments.

For example, a controller named jenkins-prod might run deployment jobs for production, accept inbound webhooks, and have 14 human administrators. That is a different remediation target from a development controller that runs pull-request tests and has no production credentials.

Create a change record containing these five facts before making any configuration change:

  • Controller URL, host name, Jenkins home directory, and operating system owner.
  • Whether the URL is reachable from the public internet, a corporate network, a VPN, or only a private subnet.
  • Authentication source: local Jenkins users, LDAP, SSO, or another identity provider.
  • Critical pipelines, deployment windows, and the person who can approve an emergency rollback.
  • A current backup or snapshot of JENKINS_HOME, plus a tested way to restore it.

The tradeoff people skip is that plugin cleanup changes operational ownership. Removing an abandoned plugin can break a job owned by a team that no longer watches Jenkins. Require each business-critical folder or pipeline to have a current owner before the maintenance window; otherwise, “unused” only means “nobody answered yet.”

2. Build a plugin inventory from the controller, not memory

A browser view of the Plugin Manager is useful, but export a machine-readable inventory as evidence. Jenkins exposes plugin data through its API, so an authenticated administrator can capture the installed version, enabled state, dependency list, and whether Jenkins reports an available update.

curl -sS -u "$JENKINS_USER:$JENKINS_API_TOKEN" \
  "https://jenkins.example.internal/pluginManager/api/json?depth=1" \
  -o plugins-before.json

Keep plugins-before.json with the change ticket. Do not put the API token in shell history or a shared script. Use an environment variable, a secret manager, or an interactive credential prompt according to your team’s operating practice.

Also record plugins directly from the Jenkins home directory. This catches the important operational detail that plugins are files on the controller, not just rows in a UI.

find "$JENKINS_HOME/plugins" -maxdepth 1 \
  -type f \( -name '*.jpi' -o -name '*.hpi' \) \
  -printf '%f\n' | sort > plugins-files-before.txt

For every plugin, add three fields that Jenkins cannot infer: a job or folder that uses it, a business owner, and a last-known reason for installation. If no owner can identify a live job after a defined review period, place it in the removal candidate set rather than treating it as permanently installed infrastructure.

3. Prioritize plugin work by reachability and blast radius

Do not sort the inventory only by “updates available.” A low-use plugin on a public controller can deserve attention before a higher-use plugin on a private controller, because internet reachability changes the attacker’s starting position.

Use a simple decision matrix during triage. It forces a decision rather than producing a spreadsheet that nobody acts on.

Condition Example Action
Known security update and internet-reachable controller Plugin is enabled on a controller accessible from the public internet Restrict network access immediately; schedule update or removal in the earliest approved window.
Plugin has no identified job owner Old reporting or notification integration Disable in a test window, validate jobs, then remove if no dependency appears.
Plugin supports production deployment or credentials use Deployment, cloud, SCM, or credentials-adjacent integration Review owner, permissions, update path, and rollback before changing it.
Core dependency used by many pipelines Plugin with broad dependency relationships Test update on a comparable non-production controller before production rollout.

The decision rule is practical: fix exposure first, then remove unowned code, then update remaining high-impact code. This avoids a common failure mode where a team spends a week resolving plugin compatibility while the same controller remains discoverable and reachable from the internet.

Jenkins plugins are not harmless UI add-ons. Each installed and enabled plugin expands the trusted server-side code footprint of the controller. The correct target is therefore not “every plugin at the newest version” but “the smallest supported plugin set, with a documented reason for every remaining plugin.”

4. Update and remove plugins with a rollback path

Separate plugin changes into small batches. Updating 35 plugins at once may appear efficient, but when a pipeline stops loading after restart, the investigation becomes dependency archaeology. A batch of one high-risk plugin and its required dependencies is easier to test, approve, and reverse.

Before a batch, capture the existing plugin files and configuration. At minimum, preserve the plugin directory and the inventory created earlier. Use your normal backup system where possible; a local copy on the same disk is not a recovery strategy for disk failure.

tar -C "$JENKINS_HOME" \
  -czf "jenkins-plugins-before-$(date +%F).tgz" plugins

For a removal candidate, disable it first through the Plugin Manager when your maintenance process permits. Run the identified jobs, watch controller logs, and confirm that no configuration page or pipeline fails because of a missing extension. Remove the plugin only after the validation period.

For an update, read the plugin’s published compatibility information and inspect dependency changes in Jenkins before selecting “restart.” A controller restart is often required to complete plugin changes, so put it inside a planned window and use a controlled restart method rather than killing the process from an arbitrary shell.

The maintenance cost is real: a minimal plugin set takes recurring ownership work. But it is cheaper than carrying integrations for retired ticketing systems, old chat services, or experiments whose credentials and permissions nobody reviews.

5. Remove public Jenkins exposure before perfecting the plugin list

If users reach Jenkins at a public URL, ask whether they truly need direct access from every internet address. Developers usually need access from a corporate network, VPN, zero-trust access proxy, or managed device network; Git hosting services may need webhook delivery. Those are separate requirements and should not result in an unrestricted controller UI.

Map every inbound path: DNS records, cloud load balancers, reverse proxies, firewall rules, Kubernetes ingress resources, and host-level listening ports. Then test from a network that should be denied. An internal DNS name does not prove private access, and an external DNS name does not prove that a firewall blocks it.

A safer target architecture commonly looks like this:

  • Jenkins controller listens on a private network or localhost behind a managed reverse proxy.
  • Human access requires VPN, an access proxy, or a restricted corporate network path.
  • Administrative routes receive tighter access controls than ordinary authenticated user access.
  • Webhook traffic is limited to the required endpoint and validated through the SCM integration’s configured secret or authentication mechanism.
  • Agent connections use explicitly configured controller-agent connectivity rather than an accidental public port.

Do not solve webhook delivery by reopening the entire UI to the internet. If a third-party service must call Jenkins, document the exact endpoint, source controls available from that service, and the authentication or signature validation used. OWASP identifies ungoverned third-party services as a CI/CD security risk; a webhook exception without an owner is exactly that kind of ungoverned integration.

6. Make Jenkins administration a separate, smaller access tier

Authenticated does not mean authorized to administer CI infrastructure. A Jenkins administrator can alter jobs, credentials usage, nodes, plugins, and global configuration, so the administrative group should be much smaller than the population allowed to view builds or trigger approved jobs.

Review the current security realm and authorization strategy in Jenkins global security settings. Prefer centrally managed identities where your organization supports them, disable accounts belonging to departed users, and avoid shared administrator credentials. Every administrator should have an individual identity so an audit event can be tied to a person rather than a team password.

Use role or folder-based access design where it fits the controller:

  • Developers can read build logs and trigger the jobs they own.
  • Release operators can approve or run production deployment jobs without receiving global administration.
  • Platform administrators can change global configuration and plugins.
  • Service accounts have only the API permissions required by their integration.

Restrict the most powerful paths at the network layer as well as inside Jenkins. If possible, require administrative access through a dedicated VPN group or access-proxy policy. This creates a second control: a stolen ordinary developer session should not automatically reach the controller’s global configuration screens from any network.

7. Review external services and artifact paths during the same change

Plugin inventory is also an integration inventory. A plugin for source control, chat notifications, cloud provisioning, artifact storage, or deployment tooling creates a trust relationship that must have an owner and an explicit credential scope.

For each remaining external integration, answer four questions: What data does Jenkins send? What identity does it use? Which jobs can invoke it? Who approves changes to it? If the answer to any question is “the old platform team,” the integration is a removal or redesign candidate.

Include artifact handling in the review. OWASP lists improper artifact integrity validation as a CI/CD risk, and this matters when a Jenkins pipeline downloads a binary, container, package, or build output from another system before deployment. Record where artifacts come from, how the pipeline identifies the intended artifact, and what verification the deployment step performs.

A concrete decision rule is: production deployment jobs should consume artifacts from an approved source with an identifiable version or digest, not simply “the latest successful build” from an ambiguous location. The point is traceability during an incident: operators need to establish what was deployed and where it originated without reconstructing a chain of chat messages and mutable links.

8. Validate the hardened state from outside and inside

A completed restart is not validation. Repeat the original evidence collection and compare it with the approved target state. Store the after-state beside the before-state so the next administrator does not have to infer whether a firewall rule or plugin was intentionally changed.

Run this validation checklist after each batch:

  1. Export plugins-after.json from the Plugin Manager API and compare installed, enabled, and updated plugins with the approved change list.
  2. Confirm removed plugins are absent from the plugin directory and no longer enabled in Jenkins.
  3. From an unauthorized network, verify the controller URL and administrative access path are denied as designed.
  4. From an authorized network, sign in as a normal developer and confirm they cannot reach global administration.
  5. Run one representative build, one artifact-producing build, and one approved deployment path if the maintenance window allows it.
  6. Confirm required webhooks still arrive and that rejected or failed requests are visible in the relevant logs.
  7. Review Jenkins logs and reverse-proxy or access-proxy logs for unexpected authentication failures, routing errors, or plugin load failures.

Test with least-privileged accounts, not only an administrator account. Administrators bypass the exact controls you are trying to prove. A developer account and a release-operator account reveal whether authorization rules match the intended day-to-day workflow.

9. What to complete this week

Start with a single production-relevant controller and finish the full loop rather than opening a large multi-quarter hardening program. The tangible output should be a before-and-after plugin inventory, a short list of deleted or updated plugins, an access diagram, and validation results.

  • Day 1: identify the controller owner, export the plugin inventory, list internet-facing paths, and take a recoverable backup.
  • Day 2: assign owners to plugins and integrations; mark unowned plugins and public entry points as remediation candidates.
  • Day 3: restrict unnecessary public access, reduce administrator membership, and schedule the first plugin batch.
  • Day 4: update or disable the first batch, then run the validation checklist using non-admin accounts.
  • Day 5: document exceptions with an owner, expiry date, and compensating control such as a network restriction or access-proxy rule.

Repeat the runbook after major Jenkins upgrades and whenever a new plugin or third-party integration is proposed. The approval question should be specific: “Which job needs this, who owns it, what permissions does it add, and when will we review it?” That question reduces both plugin risk and the operational clutter that makes Jenkins hard to secure later.