Jenkins deployments often accumulate misconfigurations over time: anonymous read access, outdated plugins with known CVEs, credentials in plain-text logs, overly permissive pipeline scripts. A single misconfiguration can leak AWS keys or provide a foothold for lateral movement. This Jenkins security hardening checklist walks through the audit steps that catch the most common and damaging issues: authentication gaps, agent trust boundaries, credential leakage, plugin vulnerabilities, pipeline sandboxing failures, and network exposure.
Each section shows the exact config path, CLI command, or API endpoint to check, plus the flags and settings that matter. You will leave with a concrete list of fixes ranked by risk. If you run Jenkins at scale, also review general Linux server hardening steps for the underlying host.
Audit Authentication and Anonymous Access
Check Jenkins Security Realm and Authorization Strategy
Navigate to Manage Jenkins โ Configure Global Security. The Security Realm controls how users authenticate (Jenkins' own database, LDAP, SAML, Active Directory). The Authorization section controls who can do what. For a quick audit, pull the current config with the Jenkins CLI:
java -jar jenkins-cli.jar -s http://localhost:8080/ -auth admin:token \
get-job config.xml | grep -A10 'authorizationStrategy\|securityRealm'
Or read $JENKINS_HOME/config.xml directly. Look for <authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/> or <useSecurity>false</useSecurity>. Both mean anyone can do anything. For production, switch to Matrix-based security or install the Role-based Authorization Strategy plugin. Matrix-based lets you grant per-user or per-group permissions (Overall/Read, Job/Build, Credentials/View) in a table. Role-based is cleaner for larger teams: define roles like developer, viewer, admin, then assign users to roles.
Disable or Lock Down Anonymous Read Access
By default, many Jenkins installations grant anonymous users Overall/Read. This leaks job names, build history, parameters, and sometimes credentials if a build step prints them. An attacker enumerating /api/json can map your CI/CD topology. Uncheck Overall/Read for the Anonymous user in the authorization matrix. If you must allow unauthenticated access (for build badges or webhooks), grant only specific permissions like Job/Discover and lock down everything else.
A common mistake: the web UI requires login, but the CLI and JNLP agent ports remain open. Check Configure Global Security โ TCP port for inbound agents. If set to a fixed port or random, ensure firewall rules restrict it to known agent IPs. For the CLI, disable it entirely if unused or require the same authentication as the web UI.
Enforce Two-Factor Authentication
Install the otp-credentials plugin or integrate SAML with an identity provider that enforces 2FA (Okta, Azure AD, Google Workspace). For otp-credentials, each user configures TOTP in their profile. To enforce it globally, combine with the Strict TOTP option under Configure Global Security โ OTP, which blocks login until a TOTP secret is registered. For SAML, the IdP enforces 2FA; Jenkins just validates the assertion. Audit who has 2FA enabled via the /user/<username>/configure page or the users.xml files in $JENKINS_HOME/users/. Look for <totpSecret> or SAML attributes in the session.
Lock Down Build Agent Configuration
Review Agent-to-Controller Security Subsystem
Since Jenkins 2.204, the Agent โ Controller Security subsystem restricts what agents can ask the controller to do. Navigate to Manage Jenkins โ Configure Global Security โ Agents. You will see checkboxes for Enable Agent โ Controller Access Control and a list of whitelisted file path rules and callable rules. If this is disabled, a compromised agent can read arbitrary files on the controller (/etc/shadow, $JENKINS_HOME/secrets/master.key) or execute code via Java deserialization.
Enable it. The defaults whitelist common build tool paths (/usr/bin/git, /var/lib/jenkins/workspace/) and safe callables. If builds fail with Rejected: No entry currently matches, add the specific path or callable to the whitelist. Never disable the entire subsystem. Review the file path rules for overly broad patterns like read / or read /home/. Restrict to workspace directories and known tool locations.
The script-security plugin sandboxes Groovy scripts in pipelines. Agents can submit scripts, so ensure Manage Jenkins โ In-process Script Approval shows no unapproved signatures from untrusted agents. A rogue agent submitting new File('/etc/passwd').text will be blocked unless you approve it.
Restrict Agent Executors and Tool Installations
List all agents with the CLI or API:
curl -u admin:token http://localhost:8080/computer/api/json?pretty=true | jq '.computer[] | {name, offline}'
Check each agent's launch method under Manage Jenkins โ Manage Nodes and Clouds โ (agent name) โ Configure. JNLP (Java Web Start) without a secret is trivially hijackable; always set a Secret in the agent config. SSH launch with password authentication is weak; use SSH keys and disable password auth in sshd_config. Docker-based agents are safer if you use ephemeral containers, but ensure the Docker socket is not mounted as -v /var/run/docker.sock without restrictions.
Under Manage Jenkins โ Global Tool Configuration, review JDK, Maven, and other auto-installers. If set to Install automatically from oracle.com or maven.apache.org, an attacker who compromises those sites can inject a backdoored JDK. Pin to specific versions stored in your artifact repository or disable auto-install and bake tools into agent images. Similar principles apply to the underlying host; see Linux Security Hardening for baseline OS lockdown.
Audit Credentials and Secrets Management
Enumerate All Stored Credentials
Navigate to Manage Jenkins โ Manage Credentials. Each credential lives in a domain (usually global) and has a type: username/password, SSH private key, secret text, or certificate. Click through each domain to see the list. For a programmatic audit, query the API:
curl -u admin:token http://localhost:8080/credentials/store/system/domain/_/api/json?pretty=true
This returns credential IDs and types but not the secrets themselves (they are encrypted in $JENKINS_HOME/credentials.xml with master.key and hudson.util.Secret). Look for credentials with vague IDs like cred123 or duplicates. Remove any that are no longer referenced in jobs. Check the Usage column in the UI or grep $JENKINS_HOME/jobs/*/config.xml for <credentialsId>.
Check for Secrets in Build Logs and Environment Variables
Even with credentials stored securely, they leak if a build step prints them. Search archived logs for common patterns:
zgrep -rE 'aws_secret_access_key|AKIA[0-9A-Z]{16}|password|BEGIN RSA PRIVATE KEY' \
/var/lib/jenkins/jobs/*/builds/*/log
In pipelines, the withCredentials step binds secrets to environment variables and masks them in logs, but only if you do not explicitly echo them:
withCredentials([string(credentialsId: 'api-key', variable: 'KEY')]) {
sh 'curl -H "Authorization: Bearer $KEY" https://api.example.com' // masked
// sh 'echo $KEY' // LEAKED in console output
}
The Credentials Binding plugin masks the literal value, but printenv, set, or echoing the variable defeats it. Audit Jenkinsfiles for echo, println, or shell commands that dump the environment. The mask-passwords option in older plugins is regex-based and misses base64-encoded or URL-encoded secrets.
Integrate External Secrets Managers
For production, store secrets in HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault and retrieve them at build time. The HashiCorp Vault Plugin reads secrets via AppRole or Kubernetes auth. A minimal Jenkinsfile snippet:
pipeline {
agent any
stages {
stage('Deploy') {
steps {
withVault([vaultSecrets: [[path: 'secret/data/myapp', secretValues: [[envVar: 'DB_PASS', vaultKey: 'password']]]]]) {
sh 'deploy.sh'
}
}
}
}
}
Configure the plugin under Manage Jenkins โ Configure System โ Vault. Set the Vault URL, auth method, and namespace. The plugin injects DB_PASS at runtime; it never appears in credentials.xml. For AWS Secrets Manager, use the AWS Secrets Manager Credentials Provider plugin, which pulls secrets by ARN and caches them per build. Rotate secrets in the external store; Jenkins fetches the latest on each run. The official Jenkins secrets documentation covers encryption details and plugin integration patterns.
Scan Plugins for Known Vulnerabilities
Check Plugin Update Status and CVE Advisories
Jenkins plugins are the primary attack vector: a vulnerable plugin can bypass all authentication and authorization checks. Navigate to Manage Jenkins โ Manage Plugins โ Updates. Any plugin with a security warning shows a red shield icon. Click it to see the CVE and affected versions. Cross-reference against the Jenkins security advisories page, which lists every disclosed vulnerability, the fixed version, and severity score. For a programmatic audit, list installed plugins with versions:
java -jar jenkins-cli.jar -s http://localhost:8080/ -auth admin:token list-plugins
The output shows plugin-name (version) and a (pinned) or (update available) flag. Pipe it to a script that queries the National Vulnerability Database or Sonatype OSS Index. For example, extract the plugin ID and version, then check NVD's JSON feed or use the OSS Index API:
curl -X POST https://ossindex.sonatype.org/api/v3/component-report \
-H "Content-Type: application/json" \
-d '[{"coordinates":"pkg:maven/org.jenkins-ci.plugins/<plugin-id>@<version>"}]'
Match the returned vulnerabilities against your installed versions. Prioritize CVSS 7.0+ and any RCE or authentication bypass. Update plugins via the UI or CLI (install-plugin <name>) and restart Jenkins. If a critical plugin has no fix, check the advisory for workarounds. Often you can disable a specific feature or restrict access to a subset of users.
Disable or Remove Unused Plugins
Every installed plugin expands the attack surface. Disabling is safer than uninstalling: it preserves configuration and prevents cascading failures if another plugin depends on it. To disable a plugin, navigate to Manage Jenkins โ Manage Plugins โ Installed, uncheck it, and click Apply. No restart required. Alternatively, edit $JENKINS_HOME/plugins/<plugin-name>.jpi.disabled (create an empty file with that name) or use the CLI:
java -jar jenkins-cli.jar -s http://localhost:8080/ -auth admin:token \
disable-plugin <plugin-name>
To remove entirely, delete the .jpi and .jpi.pinned files, plus the unpacked directory under plugins/<plugin-name>/, then restart. Before upgrading or disabling a widely used plugin, test in a staging environment. The plugin-compatibility-tester (PCT) tool runs plugin tests against a specific Jenkins core version. Clone the plugin repo and run:
mvn hpi:run -Djenkins.version=2.440.1
This starts Jenkins with the plugin and runs its test suite. If tests pass, the upgrade is likely safe. For production, schedule plugin updates during maintenance windows and roll back if builds fail.
Harden Pipeline Script Security
Audit Script Approval and Groovy Sandbox
The Script Security plugin controls what Groovy code can execute in pipelines. It has two modes: sandbox (whitelisted methods only) and admin-approved (arbitrary code after manual review). Navigate to Manage Jenkins โ In-process Script Approval to see pending signatures. A signature is a method call or field access that is not whitelisted, like method java.io.File getText or staticMethod java.lang.System setProperty. If you approve System.setProperty, any pipeline can modify JVM properties, potentially disabling security managers or redirecting logging.
Dangerous patterns to reject: readFile with arbitrary paths (reads /etc/passwd), sh or bat with unsanitized interpolation (shell injection), new File(...).delete(), Class.forName (arbitrary class loading), and evaluate (Groovy code injection). For example, this pipeline leaks secrets if approved:
@NonCPS
def leak() {
def secret = Jenkins.instance.getDescriptor('com.cloudbees.plugins.credentials.SystemCredentialsProvider')
.getStore().getCredentials().get(0).getPassword()
println(secret)
}
leak()
Enable the Groovy sandbox for all pipelines by default: in job config, check Use Groovy Sandbox under Pipeline. Sandbox mode blocks unapproved calls and restricts access to Jenkins internals. Some plugins break; test each pipeline. To enforce globally, install the Script Security plugin and configure Manage Jenkins โ Configure Global Security โ Script Security for Job DSL scripts (applies to Job DSL and Pipeline). You can blacklist specific methods via the script-security-blacklist file in $JENKINS_HOME. Add one signature per line:
staticMethod java.lang.System setProperty java.lang.String java.lang.String
method java.io.File delete
Restart Jenkins to apply. The official script approval documentation explains signature syntax and common pitfalls.
Restrict In-Pipeline Credential Access
Pipelines should access credentials only via withCredentials, which binds them to environment variables and masks them in logs. Direct access via Jenkins.instance.getDescriptorByType(...).getCredentials() bypasses logging and audit trails. To enforce this, deny script approval for any signature containing SystemCredentialsProvider or CredentialsProvider. For shared libraries, trust levels matter: a @Library('my-lib') annotation loads code from a configured SCM source. If the library is marked trusted under Manage Jenkins โ Configure System โ Global Pipeline Libraries, it runs outside the sandbox with full access. Only trust libraries in repos you control with protected branches and code review.
Untrusted libraries (loaded with library 'my-lib') run in the sandbox. To audit, check org.jenkinsci.plugins.workflow.libs.GlobalLibraries.xml in $JENKINS_HOME for <allowDefaultVersion>true</allowDefaultVersion> and <implicit>true</implicit>. These load the library automatically in every pipeline, expanding the attack surface. Disable implicit loading unless absolutely necessary. For credentials, prefer scoped credentials in folders: under Manage Jenkins โ Manage Credentials, create a folder-level domain and grant access only to jobs in that folder. This limits blast radius if a pipeline is compromised.
Review Network Exposure and Reverse Proxy Setup
Restrict Jenkins Listening Interfaces
Jenkins should never face the public internet directly. By default, it listens on 0.0.0.0:8080 (HTTP) and optionally a fixed or random port for inbound agents (often 50000). Check which ports are open:
ss -tlnp | grep java
# or
netstat -tlnp | grep java
You will see entries like 0.0.0.0:8080 and 0.0.0.0:50000. Binding to 0.0.0.0 exposes Jenkins on all interfaces. If Jenkins runs on a server with a public IP, attackers can enumerate /login, /api/json, and /script. Configure Jenkins to listen only on localhost by editing the systemd unit or Docker run command. For systemd, override /lib/systemd/system/jenkins.service:
sudo systemctl edit jenkins
Add --httpListenAddress=127.0.0.1 to JENKINS_ARGS in the [Service] section, reload, and restart. For Docker, use -p 127.0.0.1:8080:8080 instead of -p 8080:8080. The agent port is configured under Manage Jenkins โ Configure Global Security โ TCP port for inbound agents. Set it to Disable if agents use SSH or outbound JNLP, or restrict it to known agent IPs with firewall rules (iptables -A INPUT -p tcp --dport 50000 -s 192.0.2.0/24 -j ACCEPT).
Configure Reverse Proxy Headers and CSRF Protection
Place nginx, Caddy, or HAProxy in front of Jenkins to terminate TLS, enforce rate limiting, and whitelist source IPs. A minimal nginx config:
upstream jenkins {
server 127.0.0.1:8080 fail_timeout=0;
}
server {
listen 443 ssl http2;
server_name jenkins.example.com;
ssl_certificate /etc/ssl/certs/jenkins.crt;
ssl_certificate_key /etc/ssl/private/jenkins.key;
location / {
proxy_pass http://jenkins;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http:// https://;
proxy_max_temp_file_size 0;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffering off;
proxy_request_buffering off;
}
limit_req_zone $binary_remote_addr zone=jenkins:10m rate=10r/s;
limit_req zone=jenkins burst=20;
}
The X-Forwarded-For and X-Forwarded-Proto headers tell Jenkins the client's real IP and that the connection is HTTPS. In Jenkins, navigate to Manage Jenkins โ Configure System โ Jenkins Location and set Jenkins URL to https://jenkins.example.com/. Scroll down to CSRF Protection and check Enable proxy compatibility. This allows Jenkins to trust the X-Forwarded-Proto header when validating CSRF tokens.
CSRF protection is enabled by default since Jenkins 2.0. It requires API clients to request a crumb before making state-changing requests. To test, try creating a job without a crumb:
curl -X POST -u admin:token http://localhost:8080/createItem?name=test
You will get 403 No valid crumb was included. Fetch a crumb first:
CRUMB=$(curl -u admin:token http://localhost:8080/crumbIssuer/api/json | jq -r '.crumb')
curl -X POST -u admin:token -H "Jenkins-Crumb: $CRUMB" \
http://localhost:8080/createItem?name=test
A common mistake: leaving /script accessible without authentication. This endpoint executes arbitrary Groovy code and should only be reachable by admins. In the authorization matrix, ensure Anonymous and non-admin roles do not have Overall/RunScripts. If you use nginx, you can block it entirely:
location /script {
deny all;
}
Enable Audit Logging and Monitoring
Jenkins does not log security events in detail by default. Install the Audit Trail plugin to record job config changes, credential access, failed logins, and plugin installs. Navigate to Manage Jenkins โ Configure System โ Audit Trail. Add a logger: choose Log File (writes to a local file), Syslog (sends to rsyslog or syslog-ng), or Console (stdout, useful for containers). For file logging, set the path to /var/log/jenkins/audit.log and configure log rotation in /etc/logrotate.d/jenkins:
/var/log/jenkins/audit.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
}
The plugin logs events in this format:
Dec 15, 2025 10:23:45 AM user admin performed job/MyJob/configure
Dec 15, 2025 10:24:12 AM user alice performed credentials/domain/_/credential/aws-key/view
Dec 15, 2025 10:25:03 AM user anonymous failed login from 198.51.100.42
Search for suspicious activity with grep or jq. For example, find all credential views in the past hour:
grep 'credential.*view' /var/log/jenkins/audit.log | \
awk -v date="$(date -d '1 hour ago' '+%b %d, %Y %H:%M')" '$0 > date'
Or detect unauthorized job creation by non-admins:
grep 'createItem' /var/log/jenkins/audit.log | grep -v 'user admin'
If you use the Job DSL plugin, it can create jobs programmatically. An attacker with Job/Configure permission could inject a DSL script that creates a job with a reverse shell. Audit the seed job's build history and the generated jobs. For centralized monitoring, forward logs to the ELK stack (Elasticsearch, Logstash, Kibana) or Splunk. Configure Logstash to parse the Audit Trail format, extract user, action, and target, and index into Elasticsearch. Set up alerts in Kibana for repeated failed logins (action: "failed login" | count by user > 5 in 10m) or unexpected credential access.
The Prometheus Metrics plugin exposes Jenkins metrics at /prometheus. Install it, then add Jenkins as a scrape target in prometheus.yml:
scrape_configs:
- job_name: 'jenkins'
static_configs:
- targets: ['jenkins.example.com:8080']
metrics_path: '/prometheus'
scheme: https
Metrics include jenkins_node_online, jenkins_job_duration_seconds, and jenkins_plugins_failed. In Grafana, create dashboards for build queue length, executor utilization, and plugin errors. Alert on spikes in failed builds (possible credential rotation without updating Jenkins) or sudden drops in agent count (agents going offline due to network issues or compromise). For Docker-based Jenkins, ship container logs to a SIEM like Wazuh or OSSEC and correlate Jenkins audit events with host-level syscalls (execve, connect) to detect lateral movement.
Security Audit Checklist for Jenkins
Work through these steps in order, documenting findings and fixes. Prioritize items marked critical:
- Critical: Disable anonymous read access; verify in Configure Global Security and test
/api/jsonwithout auth. - Critical: Enable Agent โ Controller Access Control; review file path rules for overly broad patterns.
- Critical: Scan all plugins for CVEs using the Jenkins security advisories; update or disable vulnerable plugins.
- High: Enforce 2FA for all users via otp-credentials or SAML IdP; audit who has 2FA enabled.
- High: Migrate credentials to HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault; remove hardcoded secrets from
credentials.xml. - High: Restrict Jenkins to
127.0.0.1; place behind nginx/Caddy with TLS, rate limiting, and IP whitelisting. - Medium: Enable Groovy sandbox for all pipelines; reject script approvals for
System.setProperty,File.delete, andCredentialsProvideraccess. - Medium: Enable Audit Trail plugin; forward logs to SIEM; alert on repeated failed logins and unexpected credential access.
- Medium: Search build logs for leaked secrets with
zgrep; rotate any found credentials. - Low: Disable unused plugins; test in staging before removing.
- Low: Review agent launch methods; prefer SSH with keys over JNLP with secrets.
For broader server hardening (filesystem permissions, kernel parameters, SSH config), see Linux Security Hardening. Schedule quarterly audits: plugin CVEs are disclosed continuously, and misconfigurations creep back in as teams add jobs and agents. Automate checks where possible. Poll the plugin update API, grep logs for secrets, and export Prometheus metrics to a monitoring dashboard.
Frequently Asked Questions
How do I check if my Jenkins installation has anonymous read access enabled?
Navigate to Manage Jenkins โ Configure Global Security โ Authorization. Look at the Anonymous row in the matrix. If Overall/Read is checked, anyone can view job names, build history, and parameters without logging in. Uncheck it. To verify, log out and try accessing http://localhost:8080/api/json in a private browser window. If you see job data, anonymous read is still enabled. Check the CLI and JNLP agent ports as well. Disable or require authentication for those too.
What is the Jenkins agent-to-controller security subsystem and why does it matter?
The agent-to-controller subsystem restricts what build agents can request from the Jenkins controller. Without it, a compromised agent can read arbitrary files (/etc/shadow, master.key) or execute code on the controller via Java deserialization. Enable it under Manage Jenkins โ Configure Global Security โ Agents โ Enable Agent โ Controller Access Control. Review the whitelisted file paths and callables; reject overly broad rules like read /. If builds fail with Rejected: No entry currently matches, add specific paths, never disable the subsystem.
How can I search Jenkins build logs for leaked secrets and credentials?
Use zgrep to scan archived logs for common patterns. For example: zgrep -rE 'aws_secret_access_key|AKIA[0-9A-Z]{16}|password|BEGIN RSA PRIVATE KEY' /var/lib/jenkins/jobs/*/builds/*/log. Also search for base64-encoded secrets (echo $VAR | base64 in build steps). If you find a leaked secret, rotate it immediately in the external system (AWS, GitHub, etc.) and update the Jenkins credential. Prevent future leaks by auditing Jenkinsfiles for echo, printenv, or shell commands that dump environment variables. Use withCredentials and never explicitly print the variable.
Which Jenkins plugins have the most CVEs and should I remove them?
Historically, Script Security, Pipeline: Groovy, Git, and Matrix Authorization Strategy have had critical vulnerabilities due to their complexity and privilege level. Check the Jenkins security advisories for the latest CVEs. Plugins with RCE or authentication bypass should be updated immediately. If a plugin has no fix, check the advisory for workarounds. Often you can restrict access to admins or disable a specific feature. Do not remove core plugins like Script Security without testing; many pipelines depend on them. Disable unused plugins first, then remove if no jobs break.
How do I enforce two-factor authentication for all Jenkins users?
Install the otp-credentials plugin, then navigate to Manage Jenkins โ Configure Global Security โ OTP and check Enforce TOTP for all users. This blocks login until each user configures TOTP in their profile. Alternatively, integrate SAML with an identity provider (Okta, Azure AD) that enforces 2FA at the IdP level. For SAML, install the SAML Plugin, configure the IdP metadata URL under Configure Global Security โ SAML 2.0, and set Security Realm to SAML 2.0. The IdP handles 2FA; Jenkins validates the signed assertion.