HOW-TO GUIDE | APPSEC
Updated 10 min read

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

CICD-SEC-1
OWASP CI/CD Security Top 10 #1: Insufficient Flow Control: attackers who modify pipeline definitions can execute arbitrary code in the build environment
50,000+
Exposed Jenkins instances found on Shodan with authentication disabled: many with direct access to production deployment credentials
Anonymous
Most common Jenkins misconfiguration: anonymous read (or execute) access enabled, allowing unauthenticated users to trigger builds or read credentials
Script Sandbox
Jenkins Groovy sandbox that restricts Scripted Pipeline API calls: bypassed by requesting 'In-process Script Approval' which should require explicit review

SponsoredRetool

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.

Start building for free today

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 ****"
}
Free daily briefing

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

  1. Jenkins: Security Hardening Guide
  2. OWASP: CI/CD Security Top 10
  3. CISA: Defending Against Software Supply Chain Attacks

Free resources

25
Free download

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.

No spam. Unsubscribe anytime.

Free download

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.

No spam. Unsubscribe anytime.

Free newsletter

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.

Eric Bang
Author

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.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.