Where the worker runs
Left at its default, a single
api binary with a PostgreSQL and a Redis is a
complete TRUSTIVAN. That is the right shape for a small deployment and for the
first on-premises install, which will not want to run more processes than the
product needs.
The shipped compose file nevertheless sets it to false on the API and runs
cmd/worker as its own service, because if the two can only run together, “a
scan survives a worker restart” is a story rather than a property, and nobody
discovers otherwise until production.
Set it to false on API replicas once you scale workers separately. See
Scaling.
Concurrency
These bound different things and neither substitutes for the other.
SCAN_CONCURRENCY is a resource bound. It is small on purpose: the scanning
engine shares an on-disk cache between concurrent scans, so raising it without
limit produces lock contention rather than throughput. If you need more scanning
capacity, add a worker replica before raising this.
The per-tenant bound is a fairness bound, and it comes from each
organisation’s plan: the scan_concurrency limit (1 on Free, 5 on Pro, 20
on Enterprise), with any per-organisation override applied. Without it, one
tenant clicking “scan” on ten thousand images occupies every worker and
everyone else waits. It is a throttle rather than a quota: work beyond it stays
queued and is dispatched as that tenant’s running scans finish. Nothing is
dropped and nothing fails. SCAN_TENANT_CONCURRENCY is only the fallback for a
tenant whose plan cannot be read.
The plan’s number is a ceiling, not a promise. How many scans run at once
across the whole deployment is SCAN_CONCURRENCY × worker replicas: the
default install runs one worker at SCAN_CONCURRENCY=2, so no organisation
runs more than two at once there, whatever its plan allows.
It is honest about its own strength. Two workers claiming two different scans of
one tenant in the same instant can both see room, so the cap can be exceeded by
the number of workers racing. Making it exact would need a per-tenant lock on
the hot path of every claim, which would cost more than the overshoot it
prevents.
The scan timeout
The scan target is supplied by the caller. Scanning untrusted input without a
bound is a denial-of-service vector against a shared worker: one deliberately
pathological image would otherwise hold a worker slot indefinitely.
Ten minutes is comfortable for ordinary images. Raise it only if you have
genuinely large images and have measured them; a scan that exceeds it fails and
is retried, so an over-tight limit turns slow images into repeatedly failing
ones.
The cache
Set this to a persistent path. The compose file uses
/cache, backed by the
scanner_cache volume.
Sharing the cache across scans is what makes rescanning an unchanged image
nearly free, and persisting it across restarts is the difference between a scan
taking seconds and taking minutes — without it, every worker restart
re-downloads roughly 50 MB before it can match anything.
If you run the worker as a non-root user, as the shipped image does, the
directory must be writable by that user. A worker that cannot write its cache
does not fail loudly; it just re-downloads the database on every scan.
Registry policy
This is the part to read carefully. The image reference is caller-supplied, so these settings decide what an arbitrary API user can make your worker connect to.Why private addresses are refused by default
The link-local range includes 169.254.169.254 — the cloud instance metadata endpoint, which serves credentials to anything that asks. A scanner that will fetch any address a caller names is a server-side request forgery primitive with a registry client attached. This is a deployment decision precisely so that a caller cannot make it per request. There is no field in the scan API that relaxes it. If you need to scan an internal registry, prefer naming it:Enforced when the worker connects
The private-address rule is checked twice. The first check is on the reference when the scan is requested. The second is on every connection the scan makes, after the name is resolved, so a public-looking name that resolves to a private address is refused. The allow-list is applied per connection, to the host being connected to. If your registry sends its token service or its blobs to a different private host, name that host too, or useSCAN_ALLOW_PRIVATE_REGISTRIES. Link-local
addresses, which include the cloud metadata service, are refused under either
setting.
Repository targets follow the same rule. SCAN_ALLOW_PRIVATE_GIT_HOSTS opens
loopback and private addresses for clones. SCAN_ALLOWED_GIT_HOSTS only
narrows which hosts may be named. It does not open private addresses.
The connections a scan makes to fetch its target do not use HTTP_PROXY or
HTTPS_PROXY. A proxy would connect on the worker’s behalf to an address the
check never saw. If your worker reaches registries and git hosts only through
a proxy, those scans will fail. The vulnerability database download is
operator-configured, and it still honours the proxy settings. The
misconfiguration checks bundle is fetched during the scan, so it follows the
scan’s rule. When it cannot be fetched, the scan uses the checks built into
the binary.
The policy is re-evaluated on every scheduled run
A schedule created whileSCAN_ALLOW_PRIVATE_REGISTRIES was on outlives the
setting. Each scheduled run therefore rebuilds the image reference from the
asset’s parsed coordinates and pushes it back through the current policy, rather
than reusing the string stored on the last scan.
If an operator turns private registries off, every stored reference pointing at
an internal address stops being fetched, including the ones already scheduled.
Trusting the stored string would leave a permanent, self-renewing exception to a
security setting — created before the setting changed, and invisible afterwards.
Authentication is a separate thing
None of these settings supply credentials.SCAN_ALLOW_PRIVATE_REGISTRIES and
SCAN_ALLOWED_REGISTRIES make a registry reachable; they do not make it
authenticated.
Authentication comes from a stored credential, managed through
/api/v1/registry-credentials and gated on the private_registries
entitlement. The two are independent, and an internal registry that is both
privately addressed and password-protected needs both: the network permission
here, and a credential.
A scan whose credential cannot be resolved fails rather than retrying
anonymously. Falling back would risk pulling a public image that happens to
share the name and reporting its findings as yours.
What the worker deliberately does not have
No Docker socket, no container runtime, no access to a local image store. Images are resolved from remote registries only. Mounting a runtime socket into the process that handles untrusted input would be root on the host, and consulting a local image store would let a caller-supplied reference resolve to another tenant’s image. Neither is a trade TRUSTIVAN makes, which is also why there is nothing to install alongside the worker.Next
- Vulnerability database — the data the scanner matches against, and the freshness gate.
- Scaling — adding workers rather than raising concurrency.
- Troubleshooting scans — when a scan fails.

