.env.example is the canonical list; this page is its documented form. Settings
are grouped by what they govern, and each required variable states what happens
if it is absent.
Required
Four variables must be present in a deployed environment. Two of them are also required in development, and two are generated for you there.
Generate both secrets with
openssl rand -hex 32.
JWT_SECRET_PREVIOUS (optional) is set only while rotating JWT_SECRET: the old
secret, which still verifies the tokens it signed — each token names its key in
a kid header — but never signs a new one. Set it to the old value when you
install the new JWT_SECRET, keep it for REFRESH_EXPIRY days (default 7),
then remove it. Rotating without it logs every user out. It must be at least 32
characters and must differ from JWT_SECRET.
What happens when a secret is missing
It depends onENV, and that is the point.
In development or test, the backend generates 32 random bytes per
process and logs loudly that it did so. There is no setup step, and sessions do
not survive a restart. A previously hardcoded default for JWT_SECRET — the
same published literal the config layer defaulted to — meant anyone who read the
repository could forge a token for any user. That was a real security finding,
and generating an ephemeral key is what replaced it: a key that does not exist
cannot leak.
In any other environment, a missing or weak secret is a fatal startup error.
Values equal to or prefixed by a known placeholder are refused at any length, in
any environment, because a placeholder that once shipped in a repository is
public by definition.
TOKEN_ENCRYPTION_KEY is a master key from which the cursor-signing key, the
webhook signing secret and the secret-finding salt are all derived. Setting one
value sets all four, so a deployment cannot forget one and silently fall back to
an empty secret. Rotating it makes every stored provider token unreadable and
forces users to reconnect their accounts; there is no key-rotation mechanism
yet.
Server
Frontend origins
DASHBOARD_URL is also the whole CORS allow-list, and CORS permits credentials,
so it matters more than a display URL would. WEBSITE_URL is not on the
allow-list: the website never calls the API, and because the website and the
API are usually same-site, listing it would let any script on the website read
authenticated API responses.
There is deliberately no wildcard option. A previous
AllowOrigins: "*" let any
origin call the API with a stolen bearer token, which combined with a token
readable from localStorage turned any XSS into a full account takeover.
Sessions are HttpOnly cookies with SameSite=Lax, which imposes a deployment
constraint: the API and the dashboard must share a registrable domain.
api.example.com and app.example.com do; example.com and example.net do
not. The default arrangement proxies the API under the dashboard’s own origin,
which is same-site trivially.
Sessions and tokens
Data
Redis is an optimisation, not the system of record. Job state lives in
PostgreSQL, so losing Redis costs latency rather than work — see
Scaling.
Scanning
See Scanning for how these interact.
Registry policy
The private-address rule is checked on the target when a scan is requested,
and again on every connection the scan makes, after the name is resolved. See
Scanning.
These are about network reachability, not authentication. Credentials for
registries that require a login are stored per registry host through
/api/v1/registry-credentials and are not configured by environment variable —
a secret in a process environment is a secret in docker inspect, in a crash
dump and in every child process.
Vulnerability database
See Vulnerability database, which
is where the reasoning behind two thresholds belongs.
Threat intelligence: KEV and EPSS
The two feeds the risk score reads. The worker fetches each on a schedule, stores it, and re-applies it to every stored finding. Nothing is fetched at startup, and a failed fetch never stops the process: the previous data stays in force, the error is recorded, and the data is flagged stale as it ages.GET /api/v1/system/status reports each feed’s source, version, age, staleness
and last error.
A URL source goes through the same SSRF-guarded transport as webhook delivery,
so it cannot reach a private or link-local address. An in-perimeter mirror is
served with the
file source. See Risk scoring.
Rate limiting and webhooks
The rate limit is protection, not billing — nothing here meters usage or prices
a plan. It is generous on purpose: a pipeline polling a scan every two seconds
for ten minutes makes three hundred requests, and a limiter that interrupts
legitimate work teaches customers to retry harder.
WEBHOOK_ALLOW_PRIVATE_DESTINATIONS logs a prominent warning when enabled, and
the server refuses to start with it set when ENV is not development. Even when set, the link-local range 169.254.0.0/16 — which hosts cloud
instance metadata — stays blocked.
Observability
Metrics are cross-tenant aggregates, so binding this to
0.0.0.0 publishes
facts about your business to anything that can route to the host. See
Health and metrics.
Identity providers
Optional. Configure only the providers you intend to offer.
Redirect URIs default to
http://localhost:8080/api/v1/auth/<provider>/callback
and must match what is registered with the provider.
Outbound mail
Optional. With no relay, address verification, password reset, invitation mail and email alerts answer that mail is not configured (503,
mail_unavailable: true); invitations still work, and the administrator is
handed the link to send. See Send mail through your SMTP relay.
A half-configured relay — any
SMTP_ variable without SMTP_HOST, or a host
without SMTP_FROM — stops the process at startup. Links in mail point at
DASHBOARD_URL.
Settings that no longer exist
S3_* were declared but never read, and were removed. If you find them in an
old .env, they do nothing. (SMTP_* were removed the same way and have since
returned with real meaning; see above.) The MinIO service they configured was
removed along with them — it is AGPL-3.0, the one copyleft licence whose network
clause reaches a service, and an unused dependency is cheap to delete and
expensive to explain in a compliance review.
