A Jenkins controller usually holds the most powerful credentials in an engineering organisation, and anyone who can run code on it can take all of them. Jenkins is self-hosted, extended by plugins and driven by Groovy, so its security depends almost entirely on how it is configured: who can log in, where builds run, which plugins are installed and how secrets reach jobs. This guide covers the Jenkins security model for controllers, agents, plugins, the Credentials plugin, script approval, CSRF protection, the CLI and OIDC federation, with the lessons of CVE-2024-23897. It is written for platform engineers, DevSecOps teams and security architects who run or assess Jenkins.
Key takeaways
- Treat the controller as a tier 0 system. Overall/Administer means arbitrary code execution, and builds on the built-in node can read the whole Jenkins home directory, including encrypted credentials and the keys that decrypt them.
- Run zero executors on the controller. Move every build to agents, keep Agent to Controller Access Control on (always on since Jenkins 2.326) and isolate agents by trust level.
- Plugins are the main vulnerability surface. Fewer plugins, prompt updates and a review of every security warning matter more than any single setting.
- Credentials are only as safe as the jobs that can bind them. Scope them to folders, never interpolate them into Groovy strings and do not let untrusted Pipelines use trusted credentials.
- Replace stored cloud keys with OIDC. The OpenID Connect Provider plugin lets builds exchange short-lived signed tokens for AWS, Google Cloud or Vault access, removing long-lived secrets from the controller.
How identity and access work in Jenkins
Jenkins separates authentication from authorisation with two independent settings under Manage Jenkins » Security: the Security Realm (who the user is: Jenkins' own user database, LDAP, Active Directory, or SAML/OIDC through plugins) and the Authorization Strategy (what the user may do). Permissive strategies such as "Logged-in users can do anything" let any authenticated user create credentials and run arbitrary Pipelines.
Three kinds of identity
- Human users: authenticated through the Security Realm, authorised by the strategy (Matrix Authorization Strategy or Role-based Authorization Strategy plugins are the common fine-grained options).
- Build identities: by default every build runs as the internal
SYSTEMuser, which has full permissions to run on any node and can create, delete and trigger other jobs. Jenkins documents "Access Control for Builds" as the way to run builds as a less privileged identity, and notes that the Authorize Project plugin used for this is no longer actively maintained. - Machine clients: scripts and integrations authenticate with a username and API token (generated at
/me/configure), through the CLI, REST API or remote build triggers. Each token carries the full permissions of its user.
Controllers and agents
The controller serves the web UI, stores configuration, credentials and build records in JENKINS_HOME, and schedules work. Agents run the builds. Jenkins' controller isolation documentation is explicit: builds are often controlled by less trusted people (anyone with Job/Configure, or anyone who can change a Jenkinsfile or test code), and a build on the built-in node has the same file system access as the Jenkins process. Set the built-in node's number of executors to 0 under Manage Jenkins » Nodes » Built-In Node » Configure once agents or clouds exist. If no other machine is available, run the agent as a separate operating system user with no access to JENKINS_HOME and no sudo.
| Agent type | How it connects | Security notes |
|---|---|---|
| SSH agent | Controller connects out to the agent over SSH | No inbound port on the controller; the controller holds an SSH key per agent, store it in the System credential scope |
| Inbound agent (TCP) | Agent connects to the TCP agent listener | Use a fixed port so it can be firewalled; the official Docker image uses 50000; disable the listener if unused |
| Inbound agent (WebSocket) | Agent connects over the existing HTTP(S) port | No extra port; traffic goes through the reverse proxy and TLS |
| Cloud or Kubernetes agents | Provisioned per build and destroyed | Ephemeral agents limit persistence between builds; separate pools for trusted and untrusted jobs |
See the CI/CD Pipeline Identity Security Guide.
Admin roles and privileged access
The Jenkins permissions reference states that Overall/Administer grants "everything", including installing plugins that run arbitrary code and using the Script Console on the controller and agents, and that an instance where an untrusted user gained it should be considered fully compromised. Several permissions that look narrower are close to administrator equivalent.
| Permission | What it really allows | Who should hold it |
|---|---|---|
| Overall/Administer | Script Console, plugin installation and upload, update site management (Overall/RunScripts, UploadPlugins and ManageUpdateSites folded into it since 2.222) | Two or three named administrators, ideally through a separate admin account |
| Overall/Manage | A subset of admin settings without the most security critical options | Delegated platform operators |
| Job/Configure | Change any job definition; implies Run/Replay, which runs a Pipeline with an edited script | Job owners only, scoped by folder |
| Agent/Configure and Agent/Create | Point jobs at a machine the user controls as root and read build data and bound credentials | Platform administrators |
| Credentials/View, Create, Update | View or add stored credentials in the scopes the user can reach | Folder owners for their own folder |
| Overall/Read | Broad read access; with CVE-2024-23897 it meant reading entire files on the controller | Authenticated staff, never anonymous on an internet-facing instance |
- Separate admin accounts: give administrators a distinct account that is not used for daily job work, protected by SSO with MFA at the identity provider.
- Folders as boundaries: use folders with folder-level permissions and folder-scoped credentials so a team's credentials are reachable only by that team's jobs.
- Anonymous access: remove every permission from Anonymous, including Overall/Read, unless the instance is deliberately public. The Homebrew GitHub token exposure began with a browsable public Jenkins page that showed a token in a job's environment variables.
See the Identity Provider and SSO Security Guide.
Integrations, API tokens and service accounts
The Credentials plugin
The Credentials plugin stores Secret text, Username and password, Secret file, SSH Username with private key, Certificate (PKCS#12) and Docker Host Certificate credentials, encrypted on the controller. Pipelines refer to them by ID. Two scopes matter: System credentials are for the controller itself (agent connections, email) and are not exposed to jobs; Global credentials are available to items and their descendants. Credentials defined on a folder are available only to jobs in that folder.
Binding credentials safely
- Use withCredentials or an environment block: the Credentials Binding plugin injects the secret as an environment variable for the steps that need it.
- Never use Groovy string interpolation: the Jenkinsfile documentation says interpolation "should never be used with credentials". A double-quoted
sh "curl -H 'Authorization: Bearer ${TOKEN}' ..."expands the secret on the controller and exposes it in process arguments;sh 'curl -H "Authorization: Bearer $TOKEN" ...'lets the shell read it from the environment. The same rule stops injection through user-supplied build parameters. - Do not rely on masking: console masking replaces values with
****but the documentation says it only reduces accidental exposure, and a transformed value (encoded, split, or altered by Groovy) is printed in clear. Start shell steps withset +x. - Keep trusted credentials away from untrusted code: pull requests from forks and branches anyone can push must not run in jobs that can bind production credentials.
API tokens and the CLI
The CLI supports WebSocket (default), HTTP (-http) and SSH (-ssh, through the SSH Server plugin, disabled by default) modes. Authenticate with -auth username:apitoken, or load it from a file with -auth @/path/file, or set JENKINS_USER_ID and JENKINS_API_TOKEN. Scripted clients that use API tokens are exempt from CSRF crumbs, which makes tokens attractive to attackers as well as integrators. Create a dedicated service user per integration, grant it only the folder permissions it needs, name the token after the integration and revoke tokens with no recent use.
OIDC instead of stored cloud keys
The OpenID Connect Provider plugin makes Jenkins an OIDC issuer (by default https://YOUR_JENKINS_HOST/oidc) that signs short-lived id tokens for builds. Two credential types expose them: "OpenID Connect id token" and "OpenID Connect id token as file". The default sub claim is the job URL; claim templates under Configure Global Security » OpenID Connect can add fields. In AWS, a role trust policy allows sts:AssumeRoleWithWebIdentity with conditions on aud (for example sts.amazonaws.com) and sub; Google Cloud workload identity pools and HashiCorp Vault's jwt auth method work the same way. Pin trust to specific job URLs, never to the issuer alone, because anyone who can create a job on the controller can request a token. See the Workload Identity Federation Guide.
| Credential option | Lifetime | Main risk | Use when |
|---|---|---|---|
| Static secret in the Credentials plugin | Until rotated | Decryptable by anyone with controller code execution | Target supports nothing else; rotate on a schedule |
| Secret fetched from an external vault at build time | Short if the vault issues dynamic secrets | The vault authentication credential becomes the crown jewel | Many teams share one controller; central audit needed |
| OIDC id token federated to cloud or vault | Minutes | Loose sub conditions let other jobs assume the role | Cloud deployments and vault access; preferred default |
| User API token | Until revoked | Carries the user's full permissions | External systems calling Jenkins; one per integration |
See the Secrets Management Guide.
Plugins, scripts and the Groovy sandbox
Most Jenkins security advisories concern plugins, and every plugin runs with the controller's privileges. The plugin manager checks for updates every 24 hours by default and shows security warnings for installed versions; plugin pages carry a health score and list past advisories (the Credentials Binding plugin page, for example, lists several masking and path traversal fixes).
- Minimise: uninstall plugins nobody uses, then purge leftover configuration under Manage Old Data. Disable rather than delete a plugin that others depend on, because removing a dependency can stop the controller booting.
- Pin and test: manage the plugin list as code (for example a
plugins.txtin the controller image) and upgrade through a staging controller.
Script approval
The Script Security plugin runs Pipelines and other user scripts in a Groovy sandbox that allows only methods on an approved list. When a script calls something else it stops until an administrator approves the signature on Manage Jenkins » In-process Script Approval. Unchecking "Use Groovy Sandbox" requires approval of the whole script. Approvals are global: one approved signature is available to every job, Job DSL seed, Email Extension template and system Groovy step. Use "Approve assuming permissions check" where offered so the call is allowed only for users who already hold the permission, and treat approvals of methods such as reflection, file or process access as granting code execution on the controller.
Logging and detection
Jenkins core keeps build logs and a system log but no security audit trail. The Audit Trail plugin records who created, configured or deleted jobs and views, started or deleted builds, used credentials, ran potentially dangerous Groovy scripts and logged in or failed to log in, and ships to file, syslog or Elasticsearch. Set the reverse proxy to pass X-Forwarded-For so client IPs are correct. The Job Configuration History plugin adds diffs of job and system configuration.
| Signal | Source | Why it matters |
|---|---|---|
| Script Console use or system Groovy execution | Audit Trail | Direct code execution on the controller |
| New In-process Script Approval | Audit Trail, configuration history | May open a sandbox escape for every job |
| Plugin installed, uploaded or update site changed | Audit Trail, system log | Persistence through a malicious plugin (ATT&CK T1505 Server Software Component) |
| Credential bound by an unexpected job | Audit Trail credential usage | Theft of stored secrets (T1552 Unsecured Credentials) |
| New user, permission change or new API token | Audit Trail, identity provider logs | Persistence through valid accounts (T1078) |
| CLI requests from unusual sources, failed logins | Reverse proxy and Audit Trail | Exploitation of the web or CLI surface (T1190) |
| Job or Jenkinsfile change on a release branch | SCM audit log, configuration history | Pipeline tampering that leads to supply chain compromise (T1195.002) |
Metrics
- Executors on the controller: target 0.
- Plugin patch latency: days from a Jenkins security advisory to the fix being deployed; aim for days for critical issues.
- Installed plugins with open security warnings: target 0, and the total plugin count trending down.
- Static cloud credentials stored in Jenkins: count falling as OIDC replaces them.
- Holders of Overall/Administer: a short named list, reviewed quarterly.
- Script approvals reviewed: percentage of approved signatures with a recorded justification.
Hardening baseline
- Inventory first: list controllers, agents, plugins, credentials (by scope and folder), API tokens and holders of admin permissions. Note which instances are internet reachable.
- Close the exposure: put the controller behind SSO and a VPN or identity-aware proxy, remove anonymous permissions, terminate TLS at a reverse proxy and disable the TCP agent listener, SSH server and CLI if they are unused.
- Patch: move to a current LTS line and update all plugins on a staging controller first; subscribe to the Jenkins security advisory mailing list.
- Isolate builds: set controller executors to 0, move jobs to ephemeral agents, separate agent pools for untrusted (pull request) and trusted (release) work, and restrict which jobs use which labels.
- Restructure credentials: move Global credentials into folders, delete unused ones, replace cloud keys with OIDC and rotate anything that ever ran on the controller.
- Tighten authorisation: adopt matrix or role-based permissions, restrict Job/Configure and Agent/Configure, and configure build authorisation so builds do not run as SYSTEM where fine-grained permissions are used.
- Keep the defaults that protect you: CSRF protection (the documentation strongly recommends never setting
hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION), Agent to Controller Access Control, the Groovy sandbox and the safe markup formatter. - Test and roll back: make each change on a staging controller with copies of real jobs, back up
JENKINS_HOME, including thesecrets/directory, and protect the backup as strongly as the controller, and keep configuration as code (Configuration as Code plugin) so a bad change can be reverted.
Lessons from CVE-2024-23897
In January 2024 Jenkins disclosed CVE-2024-23897 (SECURITY-3314): the CLI parser args4j replaced an @ followed by a path with that file's contents, so users without Overall/Read (including anonymous attackers) could read the first lines of files on the controller and users with Overall/Read could read whole files, including binary keys that the advisory said could lead to remote code execution and decryption of stored secrets. Affected were weekly releases up to 2.441 and LTS up to 2.426.2; fixes shipped in 2.442, LTS 2.426.3 and 2.440.1. The same advisory fixed CVE-2024-23898, cross-site WebSocket hijacking of the CLI endpoint. CISA added CVE-2024-23897 to the Known Exploited Vulnerabilities catalog on 19 August 2024 and lists known ransomware use. The lessons: internet-exposed controllers are found and exploited, anonymous Overall/Read widens the blast radius, unused interfaces such as the CLI should be off (disabling CLI access was the official workaround), and a file read on a controller is a credential compromise, so rotate secrets after patching.
Common misconfigurations
- Builds on the built-in node: any job author can read
JENKINS_HOME, including the master key and every credential. - Public or anonymous-read instances: job pages, console logs and environment variables leak tokens, as in the Homebrew case above; scanners also find exposed controllers quickly after each critical CVE.
- Global credentials used by every job: a single malicious Jenkinsfile in any repository can bind production secrets. Compare the Codecov breach, where a tampered uploader script sent the environment variables of customer CI jobs, and the secrets in them, to an attacker server.
- Script approvals granted to unblock a build: approving a dangerous signature for one team quietly grants it to all.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | MITRE ATT&CK v20 |
|---|---|---|---|
| Least privilege for users, builds and tokens | AC-6, AC-2 | PR.AA-05 | T1078 Valid Accounts |
| Credential storage and rotation | IA-5 | PR.AA-01 | T1552 Unsecured Credentials, T1528 Steal Application Access Token |
| Plugin minimisation and secure configuration | CM-7, CM-6 | PR.PS-01 | T1505 Server Software Component |
| Patching controllers and plugins | SI-2 | PR.PS-02 | T1190 Exploit Public-Facing Application |
| Pipeline and build integrity | SA-15, SR-3, CM-3 | GV.SC-07 | T1195.002 Compromise Software Supply Chain |
| Audit logging and review | AU-2, AU-6 | DE.CM-03, DE.CM-09 | T1059 Command and Scripting Interpreter |
In CIS Controls v8.1 the same work falls mainly under Control 4 (Secure Configuration), Control 6 (Access Control Management) and Control 16 (Application Software Security); in ISO/IEC 27001:2022 Annex A under A.8.2 (privileged access rights), A.8.9 (configuration management) and A.8.25 (secure development life cycle). See the CI/CD Security Guide.
Practitioner checklist
- Set the built-in node to 0 executors and run all builds on ephemeral agents split by trust level.
- Remove anonymous permissions and put the controller behind SSO with MFA and a private network path.
- Confirm every controller is on a release later than 2.442 or LTS 2.426.3, and that CLI, SSH server and TCP agent listener are off unless needed.
- Restrict Overall/Administer, Job/Configure and Agent/Configure to named people, reviewed quarterly.
- Move credentials into folder scope, remove unused ones and rotate anything that has been exposed to builds on the controller.
- Replace stored cloud keys with OIDC id tokens and pin trust policies to specific job
subvalues. - Ban Groovy interpolation of secrets in Jenkinsfile reviews and lint for double-quoted
shsteps that reference credentials. - Install Audit Trail, forward it to the SIEM and alert on Script Console use, plugin changes and new admin grants.
- Manage plugins and configuration as code and test upgrades on a staging controller with a
JENKINS_HOMEbackup.
Standards and references
- Jenkins: Controller Isolation
- Jenkins: Security Advisory 2024-01-24 (CVE-2024-23897, CVE-2024-23898)
- Jenkins: Permissions
- Jenkins: In-process Script Approval
- Jenkins: Using a Jenkinsfile (Handling credentials)
- Jenkins Plugins: OpenID Connect Provider
- CISA: Known Exploited Vulnerabilities Catalog
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: CI/CD Security Guide · CI/CD Pipeline Identity Security Guide · GitHub and GitHub Actions Security Guide · Bitbucket Security Guide · Secrets Management Guide