NOTE / TERRAIN

DefectDojo on Kubernetes: One Inbox for Homelab Vulnerabilities.

DefectDojo is where security findings stop being files produced by scanners and start becoming work.

Trivy, Gitleaks, Kubeconform and Nessus can all produce useful results. Without a common intake, those results remain scattered across CI logs and exported reports. DefectDojo gives them a shared product, engagement and test history so repeated scans can update findings instead of creating another pile of disconnected output.

CI scanners and scheduled scans
            │
            ▼
       DefectDojo API
            │
            ▼
Traefik → nginx → Django / uWSGI
                    │
                    ├──► PostgreSQL
                    ├──► Valkey
                    └──► Celery worker and beat

This is one of the more substantial application stacks in my homelab. The complexity is justified by the workflow it creates, but it deserves an equally explicit recovery plan.

One inbox for findings

My import automation uses the v2 API and re-import semantics. The first upload establishes the scan context; later pipeline runs update the same Test rather than duplicating every finding.

The current sources include:

Trivy       filesystem and dependency findings
Gitleaks    secret-scanning findings
Kubeconform Kubernetes schema findings
Nessus      scheduled infrastructure findings

The important output is not a larger finding count. It is a shorter path from observation to triage, remediation and evidence that the issue stayed fixed.

Deployment layout

DefectDojo uses a Fleet-native Helm release with a Helm-free Kustomize post-render layer. A separate Helm-enabled Kustomize entry point remains available for local rendering.

defectdojo/
├── base/
├── overlays/
│   ├── homelab/
│   │   ├── values
│   │   └── fleet post-render resources
│   └── local/
│       ├── values
│       └── fleet post-render resources
└── fleet.yaml

The homelab environment currently uses chart 1.9.29; the local, production-like environment uses chart 1.9.37.

The application lives in the defectdojo namespace. Its runtime includes Django, nginx, Celery worker and scheduler, PostgreSQL, Valkey and an initializer Job.

Why the render path is split

Fleet inflates the Helm chart itself. Its post-render Kustomize directory therefore must not contain another helmCharts block. The manual entry point does contain that block and must be rendered with Helm support enabled.

Fleet path
  Helm render by Fleet
  → Helm-free Kustomize post-render

manual path
  Kustomize with Helm enabled
  → complete local render

Mixing the two paths produces a memorable but unhelpful failure: the controller asks Kustomize to render a Helm chart without the Helm flag. Keeping the paths separate makes both uses explicit.

There is a second Helm wrinkle around the PostgreSQL password. During an upgrade, the subchart tries to recover an existing value with lookup(). Fleet renders in a dry-run context where that lookup cannot see the downstream Secret. I provide the same value through Fleet’s protected values input at render time while the pod still reads its runtime credential from the namespace Secret.

The lesson is broader than this chart: render-time configuration and runtime configuration can have different visibility, even when they represent the same credential.

Node placement and storage

The local cluster spans several cloud providers. I pin all DefectDojo components to one labelled node so Django, workers, PostgreSQL, Valkey and local-path storage remain together.

The chart does not honour one global node selector, so the overlay applies placement to every component. If the label disappears, the failure appears as a collection of Pending pods and unbound volumes.

Persistent state includes:

PostgreSQL   findings, users and workflow state
Valkey       queue and cache support
media PVC    uploaded reports and attachments

The media claim is 20 GiB. PostgreSQL and Valkey also have their own persistent claims. Database backup and media snapshot belong to the same recovery conversation.

Two authentication paths

The browser UI and automation need different ingress behaviour.

The UI passes through Authelia. The API route bypasses the browser-oriented forward-auth middleware and relies on DefectDojo API tokens. Traefik gives the API path a higher-priority match so CI receives JSON from DefectDojo instead of an HTML login page.

browser
  → Authelia
  → DefectDojo UI

CI importer
  → token-authenticated API route
  → DefectDojo API

Bypass does not mean unauthenticated. It means the API uses the authentication mechanism designed for automation.

Operating DefectDojo

I begin with the initializer and the main Django containers:

kubectl -n defectdojo get pods,job,svc,pvc,ingressroute
kubectl -n defectdojo logs job/defectdojo-initializer --tail=200
kubectl -n defectdojo logs deploy/defectdojo-django -c uwsgi --tail=200
kubectl -n defectdojo logs deploy/defectdojo-django -c nginx --tail=200

Then I inspect the background and stateful components:

kubectl -n defectdojo logs deploy/defectdojo-celery-worker --tail=100
kubectl -n defectdojo logs statefulset/defectdojo-postgresql --tail=100
kubectl -n defectdojo logs statefulset/defectdojo-valkey --tail=100

The first diagnostic split is:

first boot fails
  └── initializer, bootstrap inputs or database readiness

login or page requests restart
  └── uWSGI memory pressure or application settings

imports queue but do not finish
  └── Celery, Valkey or worker resources

CI receives a login page
  └── API route lost priority to Authelia-protected UI route

all pods Pending on local cluster
  └── dedicated node label or local volume binding

A platform with several processes needs several health checks. The Django page can load while background imports are stuck.

Backup and recovery

The useful backup set is:

PostgreSQL backup
+
media volume snapshot
+
runtime cryptographic and database inputs
+
Fleet render inputs
+
Git configuration

My recovery order is:

1. Restore placement prerequisites on the target cluster
2. Restore runtime and Fleet render inputs
3. Restore PostgreSQL and media state
4. Reconcile the correct environment
5. Confirm the initializer and all pods
6. Sign in through the UI
7. Submit or re-import a small known scan

The final import validates the API route, token, Django application, queue, database and finding workflow together.

Rollback

Chart changes, overlay changes and database migrations do not have identical rollback boundaries. A Git revert can restore the previous render. It cannot guarantee that a newer application has not changed persistent state.

For significant upgrades I keep the matching PostgreSQL backup and media snapshot until the UI and an ingestion cycle have been validated. I also change the smallest layer possible: chart version, values, post-render resources and import automation should not all move in one opaque change.

Things worth remembering

At the time of writing:

Role          central SecOps findings intake
Namespace     defectdojo
Runtime       Django, nginx, Celery, PostgreSQL, Valkey
Deployment    Fleet Helm + Kustomize post-render
Charts        1.9.29 and 1.9.37 by environment
UI auth       Authelia forward auth
API auth      DefectDojo token on a separate route
Recovery test login plus a real scan re-import

DefectDojo earns its complexity when it turns another scanner report into a finding with an owner, history and next action. The platform is healthy when that complete loop works—not merely when Django returns a page.