How to Secure Jenkins CI/CD Pipelines Against Common Attack Vectors

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
Jenkins sits at the intersection of your source code, your credentials, and your production environments. A Jenkins instance with excessive permissions is not just a server to compromise: it is a key to every repository it can clone, every secret it uses for deployment, and every environment it can reach.
The most common Jenkins compromises follow a predictable pattern: anonymous access or weak credentials on the web console, secrets hardcoded in Jenkinsfiles visible to anyone with read access to the repo, and agents running as root or with host filesystem access.
Authentication and Authorization Hardening
Disable anonymous access immediately:
Navigate to: Manage Jenkins > Security > Authorization
- Set to: Matrix-based security or Role-Based Strategy (requires Role Strategy plugin)
- Remove the Anonymous row or set all permissions to Deny
- Add authenticated users with minimum required permissions
Minimum permission sets:
# Developers: read access only unless they need more
Anonymous: none
Authenticated users: Job/Read, View/Read
Developers: Job/Read, Job/Build, Job/Cancel, View/Read
CI Admins: All job permissions but NOT Jenkins admin
Jenkins Admin: Full permissions (restrict to 2-3 people)
Enable CSRF protection (should be on by default, verify):
Manage Jenkins > Security > CSRF Protection: Enable
Enforce HTTPS:
# /etc/default/jenkins (Debian/Ubuntu)
# Add --httpsPort and point to your certificate
JENKINS_ARGS="--httpsPort=8443 --httpsCertificate=/etc/ssl/jenkins/jenkins.crt --httpsPrivateKey=/etc/ssl/jenkins/jenkins.key --httpPort=-1"
# Or put Jenkins behind an nginx/Apache reverse proxy that handles TLS
Configure session timeouts:
// Groovy script in Jenkins Script Console (Manage Jenkins > Script Console)
// Set session timeout to 30 minutes
import hudson.model.Hudson
Hudson.instance.securityRealm.sessionTimeoutInSeconds = 1800
Hudson.instance.save()
Integrate with your identity provider (LDAP, SAML, OIDC): Install the relevant plugin and configure under Manage Jenkins > Security > Security Realm. This centralizes authentication so Jenkins credentials are managed with your standard user lifecycle (provisioning, deprovisioning, MFA).
Credential Management: Stop Storing Secrets in Jenkinsfiles
The most common Jenkins security failure: secrets stored as plain text in environment variables in the Jenkinsfile, visible to anyone with repository read access:
// Bad: secret visible in source control and build logs
pipeline {
environment {
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYNEWKEYEXAMPLE"
DB_PASSWORD = "mysuperinsecurepassword"
}
}
// Good: credential stored in Jenkins Credentials Store, referenced by ID
pipeline {
stages {
stage('Deploy') {
steps {
withCredentials([
aws(credentialsId: 'aws-prod-deploy', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY'),
string(credentialsId: 'db-password', variable: 'DB_PASSWORD')
]) {
sh 'aws s3 sync ./dist s3://my-bucket'
sh './deploy.sh' // DB_PASSWORD available as env var but masked in logs
}
}
}
}
}
Credential scoping: store credentials at the right level:
- System scope: Available only to Jenkins admin tasks, not pipeline code
- Global scope: Available to all pipelines (use for shared credentials)
- Folder scope: Available only to pipelines in a specific folder (use for team/project isolation)
Never use Global scope for production credentials. Create a folder for production jobs and store production credentials at folder scope: only pipelines in that folder can access them.
Integrate with external secrets managers:
// HashiCorp Vault integration (requires Vault Plugin)
pipeline {
environment {
// Vault retrieves the secret at runtime: nothing stored in Jenkins
VAULT_ADDR = 'https://vault.internal.company.com:8200'
}
stages {
stage('Deploy') {
steps {
withVault(configuration: [
vaultUrl: 'https://vault.internal.company.com:8200',
vaultCredentialId: 'vault-approle-jenkins'
], vaultSecrets: [
[path: 'secret/prod/webapp', secretValues: [
[envVar: 'DB_PASSWORD', vaultKey: 'db_password'],
[envVar: 'API_KEY', vaultKey: 'stripe_key']
]]
]) {
sh './deploy.sh'
}
}
}
}
}
Mask secrets in build logs:
// Jenkins automatically masks credentials from withCredentials blocks
// For any secret not from withCredentials, mask manually:
wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: [[password: env.MY_SECRET, var: 'MY_SECRET']]]) {
sh 'echo using $MY_SECRET'
// Output: "using ****"
}
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Pipeline Code Security: Restricting What Pipelines Can Do
The pipeline code injection risk: If a developer can modify a Jenkinsfile in a branch and that branch automatically triggers a build, the developer can execute arbitrary code in the Jenkins build environment: potentially accessing secrets, modifying artifacts, or pivoting to deployment environments.
Protected branches and pull request validation:
// Only run privileged stages when building from protected branches
pipeline {
stages {
stage('Test') {
steps {
sh 'npm test'
}
}
stage('Deploy to Production') {
when {
// Only run on main branch: not feature branches or PRs
branch 'main'
// Or check that this is not a PR build
not { changeRequest() }
}
steps {
withCredentials([...]) {
sh './deploy-prod.sh'
}
}
}
}
}
Shared Libraries for privileged operations: Move sensitive operations into a Jenkins Shared Library in a separate, access-controlled repository. The Jenkinsfile calls the library function: it cannot see the library's implementation or modify it (unless they have write access to the library repo):
// vars/deployToProd.groovy in the shared-library repo (controlled access)
def call(String serviceName, String version) {
withCredentials([
aws(credentialsId: 'aws-prod-deploy', ...)
]) {
sh "./deploy.sh ${serviceName} ${version}"
}
}
// Jenkinsfile in application repo (developer-accessible)
@Library('jenkins-shared-library') _
pipeline {
stages {
stage('Deploy') {
when { branch 'main' }
steps {
deployToProd('webapp', env.BUILD_NUMBER)
}
}
}
}
Groovy sandbox restrictions:
- Keep the Groovy sandbox enabled for all Pipelines
- Require explicit Script Approval review before any code runs outside the sandbox
- Audit the approved scripts list in Manage Jenkins > In-process Script Approval regularly
- Treat any request to approve a new script as a security review event
Agent Security and Isolation
Never run agents as root:
# Create a dedicated, unprivileged Jenkins agent user
useradd -m -d /var/lib/jenkins-agent -s /bin/bash jenkins-agent
# The agent runs as this user: no sudo, no docker socket access unless explicitly needed
# If Docker access is required, add to the docker group (not run as root)
usermod -aG docker jenkins-agent
Restrict agent workspace access:
# Agent workspace should be isolated from the host filesystem
# Use tmpfs workspaces that are cleared after each build
# Or use ephemeral Kubernetes agents (recommended for cloud deployments)
Kubernetes ephemeral agents (best practice for isolation):
// Jenkinsfile using Kubernetes pod agents
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsUser: 1000
runAsNonRoot: true
containers:
- name: build
image: node:20-alpine
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # Build needs write access
capabilities:
drop: [ALL]
resources:
limits:
memory: 2Gi
cpu: '1'
"""
}
}
stages {
stage('Build') {
steps {
container('build') {
sh 'npm ci && npm run build'
}
}
}
}
}
Ephemeral Kubernetes agents spin up for each build and terminate immediately after: there is no persistent agent state that one build can contaminate or inspect from a previous build.
Audit the attack surface regularly:
# Check which plugins have known vulnerabilities
# Jenkins Security Advisories: https://www.jenkins.io/security/advisories/
# Plugin version audit via Jenkins CLI
java -jar jenkins-cli.jar -s http://localhost:8080 list-plugins | grep -v '(latest)'
# Use the OWASP Dependency-Track or Trivy to scan the Jenkins WAR and plugins
trivy fs /var/lib/jenkins/plugins/
The bottom line
Jenkins security failures follow a pattern: anonymous or weak authentication, secrets in Jenkinsfiles, feature branch pipelines with production credential access, and agents running as root. Fix them in order: disable anonymous access and integrate with your IdP, move all credentials to Jenkins Credential Store at folder scope or an external vault, restrict production deployments to protected branches only, use Shared Libraries to encapsulate privileged operations, and run agents as unprivileged users or ephemeral Kubernetes pods. Audit the installed plugins monthly for known CVEs.
Frequently asked questions
How do you secure a Jenkins CI/CD pipeline?
Disable anonymous access and require authentication (integrate with LDAP, SAML, or OIDC). Store all secrets in the Jenkins Credentials Store at folder scope or an external vault (HashiCorp Vault): never in Jenkinsfiles or environment variables in source code. Restrict production deployment stages to protected branches using 'when { branch main }'. Run build agents as unprivileged users or ephemeral Kubernetes pods. Audit plugins monthly against the Jenkins Security Advisories page.
What is the biggest security risk in Jenkins?
The combination of anonymous access and secrets stored as plain text in Jenkinsfiles or environment variables. Anonymous access lets any internet user trigger builds or read build logs. Secrets in Jenkinsfiles are visible to anyone with repository read access and appear in build logs. Together they mean credentials visible to unauthenticated users. Fix both immediately: enable authentication and move secrets to the Jenkins Credentials Store.
How do I prevent secrets from leaking in CI/CD build logs?
Build logs are the most common source of CI/CD secret leaks. Three controls: (1) Use your CI/CD platform's native secret masking: Jenkins, GitHub Actions, GitLab CI, and CircleCI all mask registered secrets in log output. Add every secret to the secrets store, not just some. (2) Never echo or print secrets in build steps: 'echo $MY_TOKEN' will print the secret to logs even if the variable is registered. (3) Run secret scanning on build artifact outputs and deployment manifests before they are stored. In GitHub Actions, any string stored as a repository or organization secret with 'secrets.NAME' is automatically redacted in logs — but only if it is never split across multiple variables or concatenated in a way that breaks the masking pattern.
What is GitHub Actions OIDC and why is it better than stored credentials?
GitHub Actions OIDC (OpenID Connect) allows workflows to authenticate to cloud providers (AWS, Azure, GCP) without storing long-lived credentials as repository secrets. The workflow requests a short-lived OIDC token from GitHub, presents it to the cloud provider (which trusts GitHub as an identity provider via a configured trust relationship), and receives temporary credentials with a configurable expiry (typically 1 hour). Benefits: no static credentials in repository secrets, automatic expiry, and the trust relationship can be scoped to specific repositories or branches (preventing fork attacks). Configure in AWS via: creating an IAM OIDC identity provider for token.actions.githubusercontent.com and an IAM role with a trust policy that validates the GitHub repository and branch.
How do I audit my CI/CD pipeline for security risks?
Six audit areas for CI/CD pipelines: (1) Anonymous access -- can unauthenticated users trigger builds or view logs? (2) Secret exposure -- grep Jenkinsfiles, .github/workflows/*.yml, and .gitlab-ci.yml for hardcoded credentials; check build logs for unmasked secrets. (3) Dependency integrity -- are third-party actions or pipeline dependencies pinned to specific commit SHAs, or mutable tags that could be replaced? (4) Principle of least privilege -- does the CI/CD service account have only the permissions needed for each pipeline? (5) Build artifact signing -- are artifacts signed to verify they have not been tampered with? (6) Self-hosted runner isolation -- are self-hosted GitHub Actions runners or GitLab runners isolated and ephemeral (destroyed after each job), or shared across projects?
How do you prevent pull request-based attacks where a contributor injects malicious code into a CI/CD pipeline through a PR?
Pull request-based pipeline attacks (sometimes called 'poisoned pipeline execution') exploit the fact that many CI/CD systems run pipeline code from the contributor's branch, not just the main branch. An attacker submits a PR that modifies the pipeline configuration (.github/workflows, Jenkinsfile) to exfiltrate secrets or deploy malicious artifacts. Defenses: configure your CI/CD system to require approval before running pipelines on PRs from new contributors or contributors without prior merged PRs (GitHub's 'Require approval for all outside collaborators' setting). Never allow pipeline code from untrusted contributors to run with production secrets -- use separate secret scopes for PR validation pipelines that have read-only access and no deployment permissions. Pin third-party GitHub Actions to specific commit SHAs (SHA pinning) rather than floating tags: a tag like actions/checkout@v3 can be modified by the action maintainer or a compromised account to point to malicious code. Store pipeline configuration in a separate, protected repository that requires code owner review for changes -- this prevents a contributor from modifying the pipeline configuration in the same PR as application code. Implement branch protection rules that require code review from a designated security reviewer for any change to pipeline configuration files.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
