TRUSTIVAN is four binaries and two dependencies. This page covers how to arrange them and what the arrangement guarantees.

The components

PostgreSQL holds the job. Redis carries a wakeup. That is all Redis does. A queue whose only record of a job is in a cache is not durable, whatever the cache is configured to persist, so job state was never put there. Losing Redis entirely costs one poll interval of latency: the dispatcher finds every queued scan in PostgreSQL and republishes it. That division decides most of your operational plan. PostgreSQL is the thing to back up, to size for durability, and to fail over carefully. Redis and the scanner cache can both be lost and rebuilt.

Two supported shapes

Single process

One api binary, one PostgreSQL, one Redis. This is the default when you set nothing, and it is a complete TRUSTIVAN — not a degraded mode. It is the right answer for an evaluation, a small team, or a first on-premises install that will not want to run more processes than the product needs.

Split processes

This is what the shipped compose file does, and it is deliberate:
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.
Splitting them by default means the durability path is exercised every time anyone runs the stack. It also matches the workload’s actual shape — requests are milliseconds, scans are minutes — so the two want to scale independently. See Scaling. Both shapes run the same code. cmd/worker and an in-process worker assemble the same runner; only the HTTP server differs.

What the worker needs, and does not

The worker needs outbound network access to the registries you scan, a writable cache directory, and connections to PostgreSQL and Redis. It does not need a Docker socket, a container runtime, privileged mode, or any access to a local image store. The scanner resolves images 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. That absence is a real security property, and it simplifies placement: the worker is an ordinary unprivileged process that makes outbound HTTPS calls.

Network position

TRUSTIVAN does not terminate TLS. Put it behind a reverse proxy that does. One constraint that catches people: sessions are HttpOnly cookies with SameSite=Lax, so the API and the dashboard must share a registrable domain. api.example.com and app.example.com work; example.com and example.net do not. The default arrangement proxies the API under the dashboard’s own origin, which is same-site trivially. A genuinely cross-site deployment must use API keys, which the middleware accepts and which are not ambient. DASHBOARD_URL and WEBSITE_URL are the CORS allow-list, and CORS permits credentials. There is deliberately no wildcard option, which is what makes that safe.

What the deployment tooling includes, and what it does not

Stated plainly, because discovering either later is worse. Compose on a single host is the recommended first deployment, and it is a gated sequence rather than one up -d: preflight (refuses on anything unsafe), backup, migrate as a one-shot job, start, health, smoke, and a rollback that restores the previous images and leaves the schema alone. Kubernetes is the growth path and it exists. Kustomize base plus a DEMO and a PRODUCTION overlay, with make k8s-validate, make deploy-demo, make deploy-prod, make status, make logs and make rollback. The DEMO overlay is verified end to end on a real k3s cluster; the PRODUCTION overlay has never been applied, and cannot be until images are published to the registry it pins by digest. Terraform provisions a cluster, optionally. It provisions the machine and never deploys TRUSTIVAN, so a cluster that already exists needs none of it. Only Hetzner is implemented, and it has never been applied to a real account. What genuinely does not exist:
  • No Helm chart. Kustomize, deliberately: the decisions encoded in the manifests are the valuable part, and a templating language would replace them.
  • No AWS, Azure or GCP Terraform modules. Not implemented and not stubbed.
  • No make deploy-staging or make deploy-production. Those names do not exist. The Kubernetes targets are make deploy-demo and make deploy-prod; the Compose sequence is infra/scripts/deploy.sh, run on the target host. There is deliberately no single make deploy — a deploy targets an environment that owns its own secrets.
  • No CLI. Every operation a person or a pipeline performs goes through the HTTP API.
  • No SLSA provenance. Every published image is signed — the release workflow signs each digest with keyless cosign and attaches the image’s CycloneDX SBOM as a signed attestation, both stored beside the image as referrers so the digest you pin does not change — but nothing records how the image was built beyond what the signing certificate says (that workflow, at that tag, at that commit). Verify a release before pinning it:
    Use cosign v3 (the workflow pins v3.1.3). Keyless signing writes to Sigstore’s public transparency log: each entry names the repository, the workflow, the tag, the commit and the image digest.
  • TRUSTIVAN verifies no signature on anything it scans. Signing its own releases is not the same as checking yours: no image, SBOM or artifact you point a scan at is signature- or provenance-verified.
The processes are ordinary containers with no orchestrator-specific requirements, so running them under something this repository does not ship for is a matter of translating the compose file rather than of missing functionality — but you would be writing and maintaining that translation yourself.

A production checklist

  • ENV anything other than development — unset included — means missing secrets are fatal rather than generated. A typo fails closed.
  • DATABASE_URL must not name the schema owner. Tenant isolation is enforced in the database as well as in the application, and the schema applies FORCE ROW LEVEL SECURITY, so an owner connection is still filtered — but the owner holds the privilege to disable that in one statement, and to drop the tables. The owner belongs in MIGRATION_DATABASE_URL, which only the migration step uses. No role in any connection string should be a superuser or hold BYPASSRLS; those bypass the policies outright, and outside development the backend refuses to start with one in DATABASE_URL or JOB_DATABASE_URL.
  • JOB_DATABASE_URL is required wherever the worker runs: a login role granted trustowl_job. Without it the worker’s cross-tenant queue queries would run as the application role and see no queued scan, so the worker refuses to start rather than accept scans it can never run.
  • Persist SCANNER_CACHE_DIR on a volume, or every restart re-downloads the vulnerability database.
  • Leave SCAN_ALLOW_PRIVATE_REGISTRIES and WEBHOOK_ALLOW_PRIVATE_DESTINATIONS at false. The second is refused at startup outside development anyway.
  • Point your orchestrator’s liveness probe at /health and its readiness probe at /ready. They are different endpoints for a reason.

Next