Security Scanning - Secrets, Dependencies, and Container Image Scans in Pipelines
Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 2: Supply Chain
Prerequisites: woodpecker-first-pipeline, ci-secrets-management, dockerfile-guide
Time: 3 hours
Tags: security, scanning, trivy, gitleaks, dependencies, woodpecker, supply-chain
Summary
Add automated security gates to your Woodpecker pipelines: scan commits for leaked secrets, check dependencies for known CVEs, and scan built container images with Trivy or Grype before anything is published. Fail fast, fail loudly, and keep the noise manageable.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Run secret detection (gitleaks) on every push and PR
- โ Scan container images with Trivy in a pipeline step
- โ Choose failure thresholds that catch real problems without alert fatigue
- โ Handle findings: suppress correctly vs. fix
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
- Sources & Related
Context / Why This Matters
Your CI pipeline touches everything sensitive: source code, deploy keys, registry credentials (ci-secrets-management). It's also the last automated checkpoint before an image runs on production hosts. Three classes of risk map naturally onto pipeline stages:
- Secrets in code โ a committed API key outlives the commit forever once pushed. Detect at push time.
- Vulnerable dependencies โ CVEs land in libraries you already use. Detect on every build.
- Image vulnerabilities โ base images age even when your code doesn't change. Detect before publish.
These checks complement host-level hardening (security-hardening, fail2ban-setup); they reduce what you ship instead of defending what already runs.
Implementation / Core Content
Stage 1: secret scanning with gitleaks
steps:
secrets:
image: zricethezav/gitleaks:v8
commands:
# full history scan is expensive; diff mode for PRs, detect for pushes
- gitleaks detect --source . --verbose --redact
when:
event: [push]
For pull requests, scan only the new commits so reviewers see a targeted report:
secrets-pr:
image: zricethezav/gitleaks:v8
when:
event: [pull_request]
commands:
- git fetch origin ${CI_COMMIT_TARGET_BRANCH}
- gitleaks detect --source . --log-opts="--origin/${CI_COMMIT_TARGET_BRANCH}..HEAD" --verbose --redact
Baseline existing leaks instead of failing forever: run gitleaks detect --report-format sarif once, triage genuinely leaked credentials (rotate them โ do not just delete the file; history retains it), then add false positives to .gitleaksignore.
Stage 2: dependency vulnerability scanning
For language ecosystems with audit tooling:
deps-audit:
image: node:22
commands:
- npm ci
- npm audit --omit=dev --audit-level=high || true # warn first, tighten later
Start permissive (|| true, log-only) for one sprint, review the volume of findings, then remove the || true and set --audit-level=critical. Raising the bar incrementally prevents the "pipeline is permanently red, everyone ignores it" anti-pattern.
Stage 3: image scanning with Trivy
Scan the image after build, before publish:
steps:
build:
image: plugins/docker
settings:
dry_run: true # don't publish yet
repo: registry.fogserv.cloud/homelab/myapp
tags: "${CI_COMMIT_SHA}"
scan:
image: aquasec/trivy:latest
depends_on: [build]
commands:
# import the locally built image or scan by tag after a real push
- trivy image --exit-code 1 --severity CRITICAL,HIGH \
--ignore-unfixed registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_SHA}
publish:
image: plugins/docker
when:
event: [tag]
depends_on: [scan]
settings:
repo: registry.fogserv.cloud/homelab/myapp
tags: ["${CI_COMMIT_TAG}"]
username:
from_secret: registry_user
password:
from_secret: registry_pass
Key Trivy flags:
--exit-code 1โ make the step fail on findings (default is report-only).--severity CRITICAL,HIGHโ start narrow; widen as your backlog allows.--ignore-unfixedโ no upstream fix exists yet, so blocking buys nothing.--scanners vuln,secretโ also re-check the final image layers for embedded secrets.
Grype is the drop-in alternative if you prefer its DB:
grype registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_SHA} --fail-on high
Keep .trivyignore in the repo root with one CVE per line plus a comment explaining each suppression โ auditable, unlike verbal waivers.
Where to gate
| Check | Push | PR | Tag/release |
|---|---|---|---|
| Secret scan | block | block | block |
| Dependency audit | log | log | block (critical only) |
| Image scan | log | log | block (CRITICAL/HIGH) |
Blocking only at release time keeps development velocity while guaranteeing published artifacts are clean.
Practical Examples
Example: complete gated release workflow
when:
event: [push, tag]
steps:
test:
image: python:3.12
commands:
- pip install -e . pytest && pytest
secrets:
image: zricethezav/gitleaks:v8
commands:
- gitleaks detect --source . --redact --verbose
build:
image: plugins/docker
settings:
dry_run: true
repo: registry.fogserv.cloud/homelab/myapp
tags: "${CI_COMMIT_SHA}"
scan:
image: aquasec/trivy:latest
depends_on: [build]
commands:
- trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed \
--scanners vuln,secret registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_SHA}
- trivy image --format table registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_SHA} > trivy-report.txt
&& echo "Full report in artifacts" || true
publish:
image: plugins/docker
when:
event: [tag]
depends_on: [test, secrets, scan]
settings:
repo: registry.fogserv.cloud/homelab/myapp
tags: ["${CI_COMMIT_TAG}", "stable"]
username:
from_secret: registry_user
password:
from_secret: registry_pass
Expected behavior: any commit containing a secret fails instantly; tags with CRITICAL unfixed... er, fixable CVEs never reach the registry.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Pipeline permanently red from day one | Thresholds too strict for current dependency state | Ship log-only first; ratchet severity down over time |
| Trivy can't find the built image | dry_run build never pushed; scanner pulls remotely |
Build+load locally (plugins/docker with settings.repo + local daemon) or push to a private staging tag first |
| False-positive secret in test fixtures | Test tokens/keys trigger detectors | Add exact fingerprint to .gitleaksignore; never weaken the ruleset globally |
| Old leak still found after deleting file | Git history retains it | Rotate the credential; rewrite history only if rotation is impossible |
| Trivy DB download slow/failing on runner | Rate limits or offline runner | Schedule trivy image --download-db-only via cron on the runner, mount cache dir as volume (see caching-strategies) |
| Findings ignored because nobody reads logs | Report-only steps produce no signal | Attach reports to step output and alert on failures via ci-monitoring |
Next Steps / Ops Actions
- Cache Trivy/dependency DBs to keep scans fast: caching-strategies
- Block unscanned releases at the Forgejo level too: branch-protection
- Keep pipeline failures visible: ci-monitoring
Sources & Related
External references consulted:
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.