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:

  1. Admin → Users → Add User → role "API"
  2. Profile → API v2 Key → generate token
  3. 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 initialDelaySeconds to 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.