Jenkins remains one of the most heavily targeted pieces of CI infrastructure in the world, and the reason is structural: the controller holds every credential your pipelines use, runs arbitrary Groovy, and typically sits behind a plugin ecosystem of thousands of components, many unmaintained. The clearest recent proof is CVE-2024-23897, disclosed in January 2024 — an arbitrary file read in the Jenkins CLI caused by the args4j library's expandAtFiles feature, which let an attacker with only Overall/Read permission read arbitrary files from the controller. Because those files include the binary keys Jenkins uses to encrypt stored credentials, the file-read bug chained into full credential decryption and, in several public exploit chains, remote code execution. Proof-of-concept exploits appeared within days, and internet-exposed controllers were compromised at scale. This guide walks the Jenkins attack surface and the hardening that contains it.
The attack surface
Jenkins concentrates risk in five areas. The controller itself runs pipeline Groovy and holds the credential store and the master encryption keys, so controller compromise is total compromise. The CLI and remoting endpoints have a long history of deserialization and file-read vulnerabilities, CVE-2024-23897 being the marquee example. Plugins are third-party code running with controller privileges; an unmaintained plugin with a known CVE is a standing backdoor. The Script Console at /script is unrestricted Groovy execution — effectively root on the controller for anyone who reaches it. Build agents that persist between jobs carry cached credentials and filesystem state forward, so one malicious build poisons every later one.
Hardening step 1: patch, then reduce the exposed surface
Keep Jenkins on a current LTS line — CVE-2024-23897 was fixed in 2.442 and LTS 2.426.3 — and subscribe to the Jenkins security advisories so plugin CVEs do not sit unpatched. Then shrink what is reachable. Disable the CLI over remoting if you do not use it, put the controller behind an authenticated reverse proxy or VPN rather than the public internet, and restrict the Script Console to a small set of named administrators through matrix-based authorization. Turn off anonymous read access; "anyone can read" is how an unauthenticated file-read bug becomes a breach.
Hardening step 2: keep builds off the controller
A pipeline that runs Groovy or shell on the controller shares its trust with the credential store. Force every stage onto an agent, and make agents ephemeral so they are destroyed after each build.
pipeline {
agent none // no work runs on the controller
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '30'))
}
stages {
stage('build') {
agent { kubernetes { yamlFile 'ci/pod.yaml' } } // fresh pod per build
steps {
sh 'set -euo pipefail; make build'
}
}
}
}
Ephemeral Kubernetes or cloud agents mean a compromised build cannot linger; the pod is gone before the next job starts.
Hardening step 3: control the plugin supply chain
Treat plugins as dependencies, because they are. Maintain an explicit, version-pinned plugin list under change control rather than clicking "update all" in the Update Center. Remove plugins you do not use — every installed plugin is attack surface whether or not a job references it. Review the security warnings the Update Center surfaces, and prioritize replacing or removing any plugin flagged with an unpatched advisory.
Credentials, secrets, and dropping static keys
Store secrets only in the Jenkins Credentials store or an external manager, and bind them into a narrow scope with withCredentials so they exist only for the commands that need them. Never interpolate a credential into a Groovy string with double quotes, because that value can leak into the build log; use single quotes so the shell expands it at runtime instead.
stage('deploy') {
agent { label 'ephemeral-linux' }
steps {
withCredentials([string(credentialsId: 'registry-token', variable: 'TOKEN')]) {
sh 'echo "$TOKEN" | docker login registry.example.com -u ci --password-stdin'
}
}
}
Better still, stop storing long-lived cloud keys. Run agents on infrastructure that carries its own identity — an EC2 instance profile, an EKS IRSA role, or GCP workload identity — so the pipeline assumes a short-lived, scoped role instead of holding an access key. For federated flows, the Jenkins OpenID Connect Provider plugin can issue signed id tokens that a cloud provider trusts, replacing static credentials entirely.
Adding Safeguard scanning to the pipeline
Controller hardening does not tell you whether the code you build is exploitable. Add a stage that runs the Safeguard CLI so software composition analysis runs on every build and can fail the pipeline before an artifact ships.
stage('security-scan') {
agent { label 'ephemeral-linux' }
steps {
withCredentials([string(credentialsId: 'safeguard-token', variable: 'SAFEGUARD_TOKEN')]) {
sh '''
set -euo pipefail
curl -sSfL https://get.safeguard.sh/install.sh | sh
safeguard scan --fail-on high --sbom cyclonedx
'''
}
}
}
Safeguard's SCA engine prioritizes by reachability, so the gate blocks the small set of exploitable findings rather than every CVE in the dependency tree, and Auto-Fix opens the upgrade pull request where a fix exists.
Hardening checklist
- Run a patched LTS; track Jenkins security advisories and patch plugin CVEs
- Disable the CLI over remoting if unused; keep the controller off the public internet
- Turn off anonymous read; restrict the Script Console to named admins
- Use
agent noneat the top level and run every stage on ephemeral agents - Pin plugins under change control and remove unused ones
- Bind secrets with
withCredentials; use single-quoted shell to avoid log leaks - Give agents cloud identity (instance profile, IRSA) instead of static keys
- Scan every build with reachability-aware SCA and fail on exploitable findings
Frequently Asked Questions
Why did an "arbitrary file read" bug like CVE-2024-23897 lead to full compromise?
Because of what those files are. Jenkins stores the master keys used to encrypt every credential on the controller's filesystem. Reading them lets an attacker decrypt the entire credential store, and from there pivot into the cloud accounts, registries, and repositories those credentials unlock. A read primitive against the right files is as good as code execution.
Is it safe to run pipeline steps on the built-in node?
Not for anything untrusted. The built-in node shares the controller's process and its access to the credential store and encryption keys. Set the built-in node's executor count to zero and run all work on ephemeral agents so a malicious Jenkinsfile cannot reach controller internals.
How do I manage plugin risk without breaking builds?
Pin plugin versions and update them deliberately in a staging controller before production, the same way you would any dependency. Remove plugins you do not use to shrink the surface, and let the Update Center's security warnings drive prioritization. An unmaintained plugin with an open advisory is the highest-value target on the box.
Where does Safeguard fit alongside Jenkins?
Safeguard runs as a CLI stage in any Jenkinsfile and via native SCM integration, ingesting build SBOMs and ranking findings by reachability so your gate is meaningful rather than noisy. See pricing for plan options.
To wire these controls into your pipeline, see Safeguard CLI, SCA, Auto-Fix, pricing, and full setup steps in the documentation at docs.safeguard.sh.