Why Shift-Left Security Matters
Finding a vulnerability in production costs roughly 30x more to fix than catching it in development. The DevSecOps principle — shift security left into the development pipeline — is an economic argument as much as a technical one.
At Dell, I worked on embedding security tooling into GitLab CI/CD pipelines for internal microservices. This post covers what we learned, what failed, and the config patterns that actually stuck.
The Pipeline Before and After
Before: build → test → deploy. After: build → sast → dependency-scan → container-scan → dast → deploy.
SAST — Semgrep and Bandit
We used Semgrep for multi-language static analysis and Bandit for Python-specific checks. The critical config decision: fail the pipeline on HIGH severity findings only. Failing on MEDIUM immediately created alert fatigue — developers started ignoring pipeline failures entirely, which is worse than no SAST at all.
sast:
stage: sast
script:
- semgrep --config=auto --severity=ERROR --error .
- bandit -r . -ll # only HIGH severity
allow_failure: falseLesson: a pipeline that always fails teaches developers to ignore failures. Tune your severity thresholds before rolling out to teams.
Dependency Scanning — Trivy
We evaluated Dependabot, Snyk, and Trivy. Trivy won for our use case — open source, fast, and scans container images as well as package manifests in a single pass. The gotcha: Trivy's CVE database updates daily. We added --ignore-unfixed to only block on CVEs that have a fix available — otherwise you're blocking deploys for vulnerabilities with no remediation path.
Container Scanning
Base image choice matters enormously. Switching from python:3.11 to python:3.11-slim reduced our critical CVE count from 47 to 3 in a single change. Most CVEs live in packages you never use. Use minimal base images.
DAST — The Hard Part
Dynamic Application Security Testing requires a running instance of your app. We used OWASP ZAP in API scan mode against a staging environment. ZAP's active scan takes 20+ minutes on large APIs — run it on schedule, not on every merge request. False positive rates on internal APIs are high — maintain a suppression list.
What Didn't Work
- Mandatory MR blocking on all findings — developers bypassed by adding allow_failure: true
- Too many tools — we ran 6 scanners initially; 3 covered 90% of findings with 10% of the noise
The Setup That Stuck
After six months of iteration: Semgrep on every MR (blocking on CRITICAL only), Trivy on every container push, ZAP on a nightly schedule against staging. Security findings go into a shared dashboard rather than blocking MRs by default — with escalation rules for critical severity.
Takeaway
DevSecOps is a cultural change as much as a technical one. Start with high-signal, low-noise tooling, don't block deploys until you've tuned thresholds, and measure false positive rates obsessively.