Documentation menu

Architecture & data flow

How the StackRadar scanner works: what runs in your cluster, exactly what data leaves it, how SBOMs are matched to vulnerabilities, and where data is stored.

Last updated

The two halves

StackRadar is split deliberately into a scanner agent (Apache-2.0) that runs in your cluster and a server (AGPL-3.0) that stores SBOMs and does vulnerability matching. Both are on GitHub — the scanner and the server and dashboard. On the cloud we run the server, so the heavy state — a 900K+ record vulnerability database and your scan history — lives outside your cluster and there's nothing to operate or keep fresh in it. Self-hosted, the same server runs in your cluster next to the scanner, and the diagram below has one box fewer on the far side of the line.

YOUR CLUSTERSSTACKRADAR CLOUD · EUYOUR BROWSERK8sPodPodPodStackRadar ScannerK8sPodPodPodStackRadar ScannerSBOMsStackRadarapi.stackradar.ioOSV mirror · 900K+ CVEsfindingsapp.stackradar.ioStackRadarlivenginx / nginx:1.243 critpayments / node:1812 highpostgres:165 highgateway / go:1.224 medredis:7-alpine

What runs in your cluster

A single Helm release in the stackradar namespace. The agent watches the Kubernetes API for pods starting anywhere in the cluster. When a pod starts with a container image the agent hasn't already scanned, it:

  1. Asks the API whether this cluster already has an SBOM for that image (a digest lookup via GET /v1/sboms/check?imageDigest=…) before pulling anything — one image running in five namespaces is still one pull and one scan.
  2. Pulls the image from your registry and generates a CycloneDX 1.6 SBOM — a structured inventory of every package inside it (see the CycloneDX format reference).
  3. Uploads it via POST /v1/sboms/upload/cyclonedx, authenticated with the cluster-scoped API key.

Separately from SBOMs, the agent reports the cluster's workload inventory (POST /v1/inventory): the namespaces, workloads, and containers it can see, the Helm release that packaged each workload, and the ArgoCD application that delivers it. Each report replaces the previous one for the cluster. This is what lets the dashboard show every discovered container — per cluster, and org-wide on the Containers page — with its scanned-vs-unscanned coverage, before and after the SBOMs land, and what ties a finding to the chart version and GitOps app you'd actually change to fix it.

The agent also sends a periodic heartbeat (POST /v1/heartbeat) carrying its version in an X-Scanner-Version header and the cluster's Kubernetes version in X-Kubernetes-Version, which is what powers the cluster health, scanner version, and Kubernetes version display in the dashboard.

Because scanning is triggered by pod starts rather than a schedule, coverage tracks your deploys: a new image is scanned the moment it starts running, and there is no window where a scheduled scan hasn't caught up yet. A periodic sweep (every 6 hours by default) re-lists the cluster so nothing is missed between events.

Exactly what data leaves the cluster

YOUR CLUSTERSENT · HTTPS 443CLOUD · EUNEVER LEAVESImage layers & source codeEnv vars & command argsSecrets & pull credentialsLogs & application trafficScannerbuilds SBOMsin-clusterCycloneDX SBOMsImage metadataWorkload inventoryHeartbeatsStackRadarEU-hosted API

The diagram is the summary; the table is the full list.

SentNot sent
CycloneDX SBOMs — syft's output, with nothing removed: package names, versions, PURLs, licenses, hashes, component properties (which include in-image file paths and the image's own LABELs as syft:image:labels:*), the distro syft identified, and syft's own tool metadataSource code or image layer contents
Image metadata (scanner 0.4.0 and later, on by default; off with scanner.imageMetadata=false), added to the SBOM as stackradar:* properties: the image's ordered layer list with the Dockerfile line that made each layer — redacted in the agent first (recorded build args dropped, secret-named values, Bearer/Basic credentials and URL userinfo masked) and cut to 200 characters; the OCI base-image, source, version, revision, vendor, title, URL and licence annotations; whether it is a Docker Official Image; the buildpacks run image; architecture, OS and the image's default user; the executable path of its ENTRYPOINT and CMD; and ENV keys ending _VERSION. The full list is a snapshot-tested constant in the scanner sourceThe arguments of an image's CMD / ENTRYPOINT, and any image ENV key not ending _VERSION
Workload inventory: namespace and workload names, image references, an allowlisted set of well-known workload labels and the Helm/ArgoCD attribution annotations, running-pod counts and first-started timestamps, Helm release names and chart versions, ArgoCD application names and their repository URL (credentials redacted), chart, path and target revisionYour workloads' environment variables and command-line arguments, ConfigMaps, Secrets, registry credentials
Heartbeats: scanner version, cluster ID, cluster Kubernetes versionApplication traffic, logs, or arbitrary labels and annotations

Namespace labels are never sent: the agent isn't granted the RBAC to read them, and the inventory reports them as null rather than guessing — a missing permission is never made to look like an unlabelled namespace.

All data leaving the cluster goes over HTTPS to a single endpoint, api.stackradar.io; the agent's only other egress is the pulls it makes from your own registries, and syft's update check-in is disabled. All data is stored in the EU — relevant if you're working toward CRA or NIS2 compliance and need to answer "where does our dependency inventory live?". For the broader data-handling picture, see the security page.

What happens server-side

On upload, the SBOM is validated against the CycloneDX 1.6 schema, parsed, and its components stored. Each component is then matched against StackRadar's local mirror of the OSV.dev vulnerability database — matching is by Package URL and affected version ranges, described in detail in how vulnerability matching works.

The OSV mirror is kept fresh by an hourly incremental sync that polls OSV.dev's change feed, so a newly published advisory is matched against SBOMs you already uploaded — you don't need to redeploy or rescan anything to learn that yesterday's image is affected by today's CVE.

Each finding is also scored for priority, not just severity: its contribution to the Radar Score weights CVSS by the advisory's EPSS exploitation probability (synced daily from FIRST.org), and a listing in the CISA Known Exploited Vulnerabilities catalogue replaces that likelihood term with certainty (1.0) — observed exploitation is a score input, not just a badge. The full formula is published in how the Radar Score works.

Authentication model

The agent authenticates with a cluster-scoped API key sent in the X-API-Key header. A key identifies exactly one cluster in one organization, which is what lets the dashboard attribute every SBOM to the cluster it came from — and lets you revoke one cluster's access without touching the others. Everything derived from an upload stays scoped to that cluster; nothing one cluster reports is ever shared with another. Full details in API keys & cluster scoping.

Verifying this yourself

Everything above is observable. Watch what the agent does:

bash
# See the scanner's workload and its (lack of) privileges
kubectl get pods --namespace stackradar
kubectl describe pod --namespace stackradar -l app.kubernetes.io/name=stackradar-scanner

# Read the source of what's running
# https://github.com/lockdep/stackradar-scanner
Security teams reviewing StackRadar: the scanner source is published, the chart is on Artifact Hub, and the upload payloads are plain CycloneDX JSON you can intercept and inspect with any egress proxy.

Next steps