Showing posts with label DevSecOps. Show all posts
Showing posts with label DevSecOps. 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.

Injecting HashiCorp Vault Secrets into Kubernetes Pods

Kubernetes Secrets are only base64-encoded, they end up in Git or Helm values far too easily, and rotating a database password means redeploying everything. With HashiCorp Vault and the Vault Agent Injector, pods authenticate with their Kubernetes service account and receive secrets as files, without the application knowing Vault exists.

1. Enable Kubernetes auth in Vault

vault auth enable kubernetes

vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc:443"

When Vault runs inside the same cluster, it can use its own service account token and CA to validate other pods' tokens.

2. Policy and role

# account-search.hcl
path "secret/data/account-search/*" {
  capabilities = ["read"]
}
vault policy write account-search account-search.hcl

vault write auth/kubernetes/role/account-search \
    bound_service_account_names=account-search \
    bound_service_account_namespaces=apps \
    policies=account-search \
    ttl=1h

Only pods running as the account-search service account in the apps namespace can log in with this role, and they can only read their own path.

3. Annotate the Deployment

spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "account-search"
        vault.hashicorp.com/agent-inject-secret-db.properties: "secret/data/account-search/db"
        vault.hashicorp.com/agent-inject-template-db.properties: |
          {{- with secret "secret/data/account-search/db" -}}
          spring.datasource.username={{ .Data.data.username }}
          spring.datasource.password={{ .Data.data.password }}
          {{- end }}
    spec:
      serviceAccountName: account-search

The injector adds an init container and a sidecar. The rendered file appears at /vault/secrets/db.properties and the sidecar keeps it fresh when the secret changes or the lease renews.

4. Let Spring Boot read it

# application.yml
spring:
  config:
    import: "optional:file:/vault/secrets/db.properties"

No Vault SDK, no code change. Locally, developers use their own properties file at the same path.

Things to watch

  • The template path uses secret/data/... for KV version 2. Forgetting /data/ is the most common “permission denied”.
  • Rotation is not reload. The file updates, but a running connection pool keeps the old password until restart unless your app re-reads it. Pair rotation with a rolling restart or use Vault dynamic database credentials.
  • Alternatives: the Vault Secrets Operator syncs into native Kubernetes Secrets, and the Secrets Store CSI driver mounts secrets as volumes. Pick one approach per cluster and stick to it.
  • Keep Vault patched and restrict who can restore snapshots; Vault is now the most valuable target in the cluster.