Showing posts with label CI/CD. Show all posts
Showing posts with label CI/CD. Show all posts

Tuesday, October 27, 2026

Shift-Left Security in Jenkins: Secrets, SAST, Dependency and Image Scanning

Security reviews at the end of a release find problems when they are most expensive to fix. “Shift left” simply means running the same checks inside every pipeline run, so a leaked key or a vulnerable dependency fails the build the day it is introduced. Here is the set of stages I add to Java pipelines.

The stages

pipeline {
  agent { label 'linux' }
  environment {
    IMAGE = "nexus.example.com:8443/apps/account-search:${BUILD_NUMBER}"
  }
  stages {
    stage('Secrets scan') {
      steps { sh 'gitleaks detect --source . --redact --exit-code 1' }
    }
    stage('Build & unit tests') {
      steps { sh 'mvn -B clean verify' }
    }
    stage('SAST + quality gate') {
      steps {
        withSonarQubeEnv('sonar') { sh 'mvn -B sonar:sonar' }
        timeout(time: 10, unit: 'MINUTES') {
          waitForQualityGate abortPipeline: true
        }
      }
    }
    stage('Dependency check') {
      steps {
        sh 'mvn -B org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7'
      }
    }
    stage('Image build + scan') {
      steps {
        sh 'docker build -t $IMAGE .'
        sh 'trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed $IMAGE'
        sh 'trivy image --format cyclonedx --output sbom.json $IMAGE'
        archiveArtifacts artifacts: 'sbom.json'
      }
    }
  }
}

What each stage catches

  • Secrets scan (gitleaks) – API keys, passwords and tokens committed to the repo. Run it first; it takes seconds.
  • SAST with a quality gate (SonarQube) – injection risks, weak crypto and bugs in your code. waitForQualityGate makes the gate a hard stop instead of a dashboard nobody reads.
  • Dependency check (OWASP) – known CVEs in third-party libraries. Start with a CVSS threshold of 7 and tighten later.
  • Image scan (Trivy) – vulnerable OS packages and runtimes in the final image. --ignore-unfixed keeps the build focused on things you can actually fix.
  • SBOM – a CycloneDX list of everything inside the image. When the next big library vulnerability is announced, you can answer “are we affected?” in minutes.

Making it stick

  1. Put these stages in the shared library, not in each Jenkinsfile, so every team gets them and updates roll out in one place.
  2. Pin scanner and tool versions and pull them through your Nexus proxy. Recent npm and PyPI compromises showed that floating tags like latest are a supply-chain risk in themselves.
  3. Have a documented exception process (suppression files with an owner and an expiry date). Otherwise people disable the stage the first time it blocks a release.
  4. Start in warning mode for a week, fix the backlog, then switch to failing builds.

The goal is not zero findings; it is that every new risk is visible, owned and decided on before it reaches production.

Nexus as a Single Proxy for Maven, npm and Docker in CI

Our CI builds used to fail a few times a month for reasons that had nothing to do with our code: Maven Central was slow, npm returned a 503, or Docker Hub rate-limited the build agents. Putting Sonatype Nexus Repository in front of every public registry made those failures disappear, and it gave us one place to control what enters the build.

The repository model

For each format, create three kinds of repository:

  • proxy – caches a remote registry (Maven Central, npmjs, Docker Hub).
  • hosted – stores what your team publishes (release JARs, internal npm packages, app images).
  • group – one URL that combines hosted + proxies. Clients only ever talk to the group.
maven-public  (group)  = maven-releases + maven-snapshots + maven-central-proxy
npm-group     (group)  = npm-hosted + npmjs-proxy
docker-group  (group)  = docker-hosted + dockerhub-proxy

Maven: one mirror for everything

<!-- ~/.m2/settings.xml on the build agents -->
<settings>
  <mirrors>
    <mirror>
      <id>nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>https://nexus.example.com/repository/maven-public/</url>
    </mirror>
  </mirrors>
</settings>

In Jenkins, manage this file with the Config File Provider plugin and inject it with configFileProvider, so credentials for deploying to maven-releases never live in a repo.

npm

npm config set registry https://nexus.example.com/repository/npm-group/

Or commit an .npmrc with that line to each project, so local builds and CI resolve packages the same way.

Docker

Give the Docker group repository its own HTTPS connector or a path-based route through your reverse proxy, then pull through it:

docker pull nexus.example.com:8443/library/eclipse-temurin:21-jre
# in Dockerfiles
FROM nexus.example.com:8443/library/maven:3.9-eclipse-temurin-21 AS build

Push application images to the hosted repository and let Kubernetes pull them from the same group URL.

Housekeeping that saves you later

  1. Cleanup policies. Delete snapshot and feature-branch images older than 30 days, then run the compact blob store task. Otherwise the disk fills quietly.
  2. Separate blob stores per format, so a runaway Docker cache cannot starve Maven.
  3. Disable anonymous access unless you really need it, and use service accounts per pipeline.
  4. Keep Nexus itself patched. It sits in the middle of your supply chain; a compromise here reaches every build.
  5. Block known-bad versions. When a malicious package version is announced, removing or blocking it in the proxy protects every build agent at once.

The result: faster builds, no rate limits, and a single choke point where you can stop a bad dependency before it reaches production.

Building a Jenkins Shared Library for Multi-Project CI/CD

Every team I have worked with starts with a Jenkinsfile copied from the last project. Six months later there are forty slightly different pipelines, and a single change to the security scan step means forty pull requests. A Jenkins shared library fixes this: the pipeline logic lives in one Git repo, and each application keeps a five-line Jenkinsfile.

Repository layout

jenkins-shared-lib/
├── vars/
│   ├── javaPipeline.groovy      // full pipeline for Maven/Spring Boot apps
│   ├── nodePipeline.groovy
│   └── dockerBuild.groovy       // reusable step
├── src/org/shailapps/ci/
│   └── Utils.groovy             // plain Groovy helpers
└── resources/
    └── templates/sonar-project.properties
  • vars/ – global steps. The file name becomes the step name (javaPipeline()).
  • src/ – normal Groovy classes, imported with import org.shailapps.ci.Utils.
  • resources/ – static files, loaded with libraryResource 'templates/sonar-project.properties'.

A pipeline as a step

// vars/javaPipeline.groovy
def call(Map cfg = [:]) {
  def jdk   = cfg.get('jdk', 'jdk21')
  def image = cfg.get('image', env.JOB_BASE_NAME)

  pipeline {
    agent { label 'linux' }
    tools { jdk jdk; maven 'maven3' }
    options { timestamps(); disableConcurrentBuilds() }
    stages {
      stage('Build & Test') {
        steps { sh 'mvn -B clean verify' }
        post  { always { junit '**/target/surefire-reports/*.xml' } }
      }
      stage('Sonar') {
        steps { withSonarQubeEnv('sonar') { sh 'mvn -B sonar:sonar' } }
      }
      stage('Image') {
        when { branch 'main' }
        steps { dockerBuild(name: image) }
      }
    }
  }
}

The application side

@Library('jenkins-shared-lib@v1.4.0') _
javaPipeline(jdk: 'jdk21', image: 'account-search')

That is the whole Jenkinsfile. Register the library once under Manage Jenkins → System → Global Pipeline Libraries with the name jenkins-shared-lib and your Git URL.

Lessons learned

  1. Pin versions. Reference a tag (@v1.4.0), not @main. A broken commit on main should not break every build in the company at once.
  2. Keep config in maps. Pass options as a Map with sensible defaults, so adding a new option never breaks existing callers.
  3. Test the library. Use the JenkinsPipelineUnit framework to unit-test your vars/ steps before tagging a release.
  4. One sample project per stack. Keep a tiny Java, Node and Python sample repo that runs the latest library version nightly. It is the cheapest early warning you will ever get.

Once the library is in place, adding a new scan or a new deploy target becomes one pull request instead of forty.