DefectDojo in Kubernetes: centralising security findings
Published: 2026-03-05
CI pipelines produce security reports: SAST from SonarQube, container scans from Trivy, dependency audits from OWASP Dependency-Check. Without a central place to aggregate, deduplicate, and track them, findings live in artifact tabs that nobody looks at. DefectDojo is that central place.
What DefectDojo does
- Ingests reports from 100+ scanner formats (Trivy JSON, SonarQube, Bandit, npm audit, etc.)
- Deduplicates findings across scans (same CVE in the same component → one finding)
- Tracks finding status: Active, Mitigated, False Positive, Accepted Risk
- Enforces SLA per severity (Critical: 7 days, High: 30 days, Medium: 90 days)
- Provides a REST API for CI integration
- Sends notifications when SLAs are breached
Deploy via FluxCD HelmRelease
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: defectdojo
namespace: defectdojo
spec:
interval: 1h
timeout: 30m
chart:
spec:
chart: defectdojo
version: "1.x"
sourceRef:
kind: HelmRepository
name: defectdojo
namespace: flux-system
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
retries: -1
values:
tag: latest
createSecret: true
createValkeySecret: true
createPostgresqlSecret: true
postgresql:
enabled: true
django:
uwsgi:
livenessProbe:
enabled: true
initialDelaySeconds: 120
periodSeconds: 10
readinessProbe:
enabled: true
initialDelaySeconds: 120
periodSeconds: 5
The initialDelaySeconds: 120 is necessary: DefectDojo runs Django migrations on startup, which takes 1–2 minutes on first boot. Without it, the readiness probe kills the pod before migrations finish.
Getting the admin password
On first deploy, the chart creates a random admin password:
bashkubectl get secret defectdojo \
-n defectdojo \
-o jsonpath='{.data.DD_ADMIN_PASSWORD}' | base64 -d
Log in at https://defectdojo.example.com with admin and this password. Immediately create a non-admin user for day-to-day use.
CI integration: pushing Trivy results
yaml# .gitlab-ci.yml
trivy-scan:
image: aquasec/trivy:latest
script:
- trivy image
--format json
--output trivy-report.json
registry.example.com/myapp:$CI_COMMIT_SHORT_SHA
artifacts:
paths: [trivy-report.json]
expire_in: 1 day
upload-to-defectdojo:
image: curlimages/curl:latest
needs: [trivy-scan]
script:
- |
curl -s -X POST \
-H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
-F "file=@trivy-report.json" \
-F "scan_type=Trivy Scan" \
-F "product_name=${CI_PROJECT_NAME}" \
-F "engagement_name=${CI_COMMIT_BRANCH}" \
-F "auto_create_context=True" \
-F "close_old_findings=True" \
https://defectdojo.example.com/api/v2/import-scan/
close_old_findings=True marks findings from the previous scan as resolved if they don't appear in the new scan. This keeps the finding list current without manual cleanup.
Pushing SonarQube findings
Export from SonarQube, then push:
bash# Export from SonarQube
curl -u "${SONAR_TOKEN}:" \
"https://sonar.example.com/api/issues/search?componentKeys=my-project&resolved=false" \
> sonar-report.json
# Push to DefectDojo
curl -X POST \
-H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
-F "file=@sonar-report.json" \
-F "scan_type=SonarQube Scan" \
-F "product_name=my-service" \
-F "engagement_name=main" \
-F "auto_create_context=True" \
https://defectdojo.example.com/api/v2/import-scan/
Pushing OWASP Dependency-Check results
yamldependency-check:
image: owasp/dependency-check:latest
script:
- /usr/share/dependency-check/bin/dependency-check.sh
--project ${CI_PROJECT_NAME}
--scan .
--format JSON
--out dependency-check-report.json
artifacts:
paths: [dependency-check-report.json]
upload-dc:
image: curlimages/curl:latest
needs: [dependency-check]
script:
- curl -X POST \
-H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
-F "file=@dependency-check-report.json" \
-F "scan_type=Dependency Check Scan" \
-F "product_name=${CI_PROJECT_NAME}" \
-F "engagement_name=${CI_COMMIT_BRANCH}" \
-F "auto_create_context=True" \
-F "close_old_findings=True" \
https://defectdojo.example.com/api/v2/import-scan/
SLA enforcement and notifications
Configure SLA in Settings → SLA Configuration:
| Severity | Days to resolve |
|---|---|
| Critical | 7 |
| High | 30 |
| Medium | 90 |
| Low | 180 |
DefectDojo tracks creation date and sends email/Slack notifications when SLAs are breached. Findings past their SLA appear in a dedicated dashboard view. This creates accountability — findings can't silently sit in "Active" forever.
API token management
Create a dedicated service account for CI:
- Admin → Users → Add User → role "API"
- Profile → API v2 Key → generate token
- Store in GitLab CI as a protected, masked variable
DEFECTDOJO_API_TOKEN
Never use the admin token in CI. If it leaks, an attacker can delete all findings or create false mitigations.
Viewing findings
After the first import:
- All Findings: full list with deduplication
- Product: group findings by application/service
- Engagement: per-branch/deployment context
- Tags: filter by scanner type
For a team working on multiple services, create one Product per service and push each scanner's output into the same engagement. DefectDojo correlates findings from Trivy, SonarQube, and Dependency-Check into a single view.
Troubleshooting
Import fails with 400:
- Wrong
scan_type— must match exactly (e.g., "Trivy Scan", not "trivy") - Check the DefectDojo pod logs:
kubectl logs -n defectdojo deployment/defectdojo-django
Pod crashloops on startup:
- Likely migration timeout — increase
initialDelaySecondsto 180 or 240 on slow nodes.
Deduplication not working:
- DefectDojo deduplicates by hash. If the scanner report format changes between versions, hashes won't match and old findings won't be closed.