Integrating security scanning into CI/CD pipelines was one of the changes with the biggest payoff in my career: on one team it reduced vulnerabilities reaching production by 60%. The trick is not the tools themselves — it is wiring them in so they help developers instead of blocking them.
The two cheapest wins
If you only add two security jobs to your pipeline, make them these:
| Job | Catches | Typical runtime |
|---|---|---|
| Gitleaks | API keys, passwords and tokens committed to Git | seconds |
| Trivy | Known CVEs in OS packages and app dependencies inside the image | < 1 minute |
Step 1 — Secret detection with Gitleaks
Secrets must be caught before the image is built, so this runs in the first stage:
stages: [secrets, build, scan, deploy]
gitleaks:
stage: secrets
image:
name: zricethezav/gitleaks:v8.18.4
entrypoint: [""]
script:
- gitleaks detect --source . --redact --verbose
--report-format json --report-path gitleaks-report.json
artifacts:
when: always
paths: [gitleaks-report.json]
expire_in: 1 week
--redact keeps the secret itself out of the job log — otherwise your scanner becomes the leak.
Step 2 — Image scanning with Trivy
Scan the exact image you just pushed, and export results in GitLab’s format so findings appear in the merge request:
trivy:
stage: scan
image:
name: aquasec/trivy:0.56.2
entrypoint: [""]
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
TRIVY_USERNAME: $CI_REGISTRY_USER
TRIVY_PASSWORD: $CI_REGISTRY_PASSWORD
TRIVY_CACHE_DIR: .trivycache/
script:
# 1) Full report for the GitLab security widget (never fails the job)
- trivy image --exit-code 0 --format template
--template "@/contrib/gitlab.tpl"
--output gl-container-scanning-report.json "$IMAGE"
# 2) Quality gate: fail only on fixable CRITICAL issues
- trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed "$IMAGE"
cache:
paths: [.trivycache/]
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
Make the gate fair
A security gate that fails on everything gets disabled within a week. A few rules that kept ours alive:
- Fail only on what developers can fix.
--ignore-unfixedskips CVEs with no available patch. - Start with
CRITICAL, then tighten. AddHIGHonce the backlog is under control. - Allow documented exceptions. A
.trivyignorefile in the repo, reviewed like code, beats a disabled job. - Cache the vulnerability DB.
TRIVY_CACHE_DIRplus GitLab cache turns a 40-second download into a no-op.
What’s next
Secrets and image CVEs are the floor, not the ceiling. The next layers are SAST with SonarQube, DAST with OWASP ZAP against a review environment, and aggregating everything in DefectDojo so security has a single dashboard. I’ll cover those in follow-up posts.