Trivy Viewer is a hub-pull security dashboard that aggregates Trivy Operator reports (VulnerabilityReport, SbomReport) from many Kubernetes clusters into one SQLite-backed API and embedded web UI.
Hub-pull: collectors run only on the hub. Edge clusters need Trivy Operator CRDs, the operator (or existing report CRs), and a read-only ServiceAccount — no agent pod on each edge.
flowchart TB
subgraph hub [Hub cluster]
server[server Deployment]
scraper[scraper Deployment]
db[(SQLite PVC)]
server --> db
scraper --> db
end
subgraph edges [Edge clusters]
ea[edge: Trivy Operator + read-only SA]
eb[edge-n: Trivy Operator + read-only SA]
end
scraper -->|watch API| ea
scraper -->|watch API| eb
user[Engineer] -->|UI + /api/v1| server
flowchart TB
subgraph phase1 [1. Provision]
kindHub[Create hub cluster]
kindEdge[Create edge cluster]
end
subgraph phase2 [2. Hub]
crdsHub[Apply Trivy CRDs]
helmViewer[helm install trivy-viewer]
portFwd[Port-forward or Ingress]
end
subgraph phase3 [3. Edge]
operator[Install Trivy Operator]
rbac[Apply reader RBAC]
workload[Deploy sample workload]
end
subgraph phase4 [4. Register]
adminUI[Admin UI Clusters wizard]
secret[Hub registration Secret]
scraper[Scraper watches edge API]
end
subgraph phase5 [5. Verify]
crs[VulnerabilityReport CRs on edge]
ui[Reports in UI]
end
kindHub --> crdsHub --> helmViewer --> portFwd
kindEdge --> operator --> rbac --> workload
portFwd --> adminUI
rbac --> adminUI
adminUI --> secret --> scraper --> crs --> ui
| Feature | Status |
|---|---|
Single binary (--mode server | scraper) |
done |
| SQLite storage (WAL, migrations) | done |
| Multi-cluster hub-pull (Secret watcher + per-cluster informers) | done |
REST API /api/v1 (dashboard, reports, SBOM, hub cluster CRUD) |
done |
| Embedded React UI | done |
| Helm chart (server + scraper, shared PVC, RBAC) | done |
| OIDC / auth | planned |
| PostgreSQL backend | planned |
Install on the hub cluster:
kubectl apply -f examples/kind/trivy-crds.yaml
helm install trivy-viewer oci://ghcr.io/duynhlab/charts/trivy-viewer \
--namespace trivy-system --create-namespaceInstalling from a source checkout instead: use the local path
charts/trivy-viewer as the chart reference.
Expose the UI (port-forward, Ingress, or LoadBalancer):
kubectl -n trivy-system port-forward svc/trivy-viewer-server 3000:3000The UI stays empty until you register edge clusters (below) and the operator has created report CRs on those edges.
Open the UI → Admin → Clusters (/admin/clusters).
| Field | Example (Kind) | Notes |
|---|---|---|
| Cluster name | edge-1 |
DNS-1123 label; shown on the dashboard |
| Edge namespace | trivy-system |
Where the reader SA lives (same as operator namespace is OK) |
| Context | kind-edge |
Your kubectl context for the edge cluster |
1-a — Apply on edge (admin kubeconfig):
kubectl --context kind-edge apply -f examples/kind/edge-reader-rbac.yamlOr paste the manifest from the wizard (it matches the example file).
1-b — Extract credentials (run on your workstation):
EDGE_CTX=kind-edge
EDGE_NS=trivy-system
TOKEN=$(kubectl --context "$EDGE_CTX" -n "$EDGE_NS" \
get secret trivy-viewer-reader-token -o jsonpath='{.data.token}' | base64 -d)
CA=$(kubectl --context "$EDGE_CTX" -n "$EDGE_NS" \
get secret trivy-viewer-reader-token -o jsonpath='{.data.ca\.crt}')
SERVER=$(kubectl --context "$EDGE_CTX" config view --minify \
-o jsonpath='{.clusters[0].cluster.server}')
# Kind only: the kubeconfig URL points at your host loopback, which the hub
# scraper pod cannot reach. Use the edge node's Docker-network IP instead:
# SERVER="https://$(docker inspect edge-control-plane \
# --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'):6443"
echo "server: $SERVER"
echo "ca: $CA"
echo "token: $TOKEN"
kubectl --context "$EDGE_CTX" --token "$TOKEN" \
auth can-i list vulnerabilityreports --all-namespacesClick Next: Register → in the wizard.
| Field | Source | Kind tip |
|---|---|---|
| API server URL | SERVER from step 1-b |
Use the edge node's Docker-network IP, e.g. https://172.18.0.3:6443 (see Kind networking) — not https://127.0.0.1:6444 |
| CA certificate | CA (base64) |
Paste the full base64 string |
| Bearer token | TOKEN |
Read-only SA token |
| Skip TLS verify | checkbox | Usually unnecessary for Kind — the API certificate includes the node IP, so the CA verifies |
Click Register cluster. The hub creates a Secret labelled trivy-viewer.io/secret-type=cluster. The scraper attaches within seconds; Registered Clusters shows Synced once reports are in the database (typically 30–90s after CRs exist on the edge).
The hub scraper pod must reach the edge Kubernetes API URL. Both Kind nodes sit on the same Docker network, so register the edge node's container IP:
EDGE_IP=$(docker inspect edge-control-plane \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
# register: https://$EDGE_IP:6443The host port from examples/kind/cluster-edge.yaml (127.0.0.1:6444) is
only for kubectl from your workstation — pods on the hub cannot reach your
host loopback.
curl -sS -X POST http://localhost:3000/api/v1/hub/clusters \
-H 'Content-Type: application/json' \
-d '{
"name": "edge-1",
"server": "https://127.0.0.1:6444",
"ca_data": "<base64-ca>",
"bearer_token": "<token>",
"insecure": true
}'Prefer the Admin UI wizard for day-to-day use.
After you understand the flow above:
- Follow examples/README.md — create hub + edge,
kubectl apply, Helm, operator. - Complete Register edge clusters via Admin UI (this README).
- Verify reports on the dashboard.
| Hub mode | Edge | Hub |
|---|---|---|
| Hub-pull only | install-edge.sh + reader RBAC |
CRDs + trivy-viewer Helm |
| Hub + local scans | operator on edges | operator on hub or CRDs + watchLocal |
go build -o bin/trivy-viewer ./cmd/trivy-viewer
go test ./...
cd web && npm ci && npm run build # embeds UI into internal/web/staticOn push/PR to main: Go vet, unit tests + coverage, web build, Helm lint/template, golangci-lint.
On tag v*: multi-arch image push to GHCR.
Dependabot updates GitHub Actions, Go modules, npm (web/), and the root Dockerfile weekly.
Maintainers use local-only paths (not published on GitHub): docs/, e2e/, and Makefile. See AGENTS.md.
| Symptom | Fix |
|---|---|
| UI empty after Helm | Register edges in Admin → Clusters |
| No reports after register | Install Trivy Operator on the edge (examples/README.md); deploy examples/kind/demo-workload.yaml; wait 1–5 minutes for CRs |
| Edge unreachable from hub | Check API server URL, host networking, TLS / Skip TLS verify |
| Cluster not Synced | Confirm kubectl get vulnerabilityreports -A on the edge; check scraper logs on the hub |
Trivy Viewer reads Trivy Operator CRs, not Pods directly.
MIT — see LICENSE. The embedded React UI is derived from younsl/o (MIT); see the third-party notice in the license file.