Configuration
Every environment variable
Edit this pageOn this page
Loomscope is configured entirely by environment variable. There is no config file, no command-line flags and nothing to template — which means a deployment is reproducible from its environment alone.
.env.example in the repository is the annotated version of this page; this
one is the reference.
Control plane
Required
| Variable | Purpose | Generate with |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | — |
BETTER_AUTH_SECRET | Signs session cookies | openssl rand -hex 32 |
BETTER_AUTH_URL | Public URL. Must match the browser address, scheme and port | — |
LOOMSCOPE_KMS_KEY | AEAD key encrypting stored credentials | openssl rand -base64 32 |
BETTER_AUTH_URLmust match exactly. Better-Auth signs callbacks against it, so a wrong value produces sign-ins that appear to work and then fail on redirect — includinghttpwhere the browser useshttps, or a missing port.
LOOMSCOPE_KMS_KEYcannot be recovered. It encrypts every credential in thecredentialstable: SNMP community strings, cloud keys, SSH material. Losing it makes all of them permanently unreadable; leaking it means treating all of them as compromised. It belongs in a secret manager, with a copy somewhere that is not the server.
Optional
| Variable | Default | Purpose |
|---|---|---|
LOOMSCOPE_DB_POOL_MAX | 25 | PostgreSQL connections this process may hold in total, across all its pools |
LOG_LEVEL | info | trace, debug, info, warn, error |
LOOMSCOPE_JOBS_IN_WEB | on | Set false once a separate worker container owns scheduled work |
LOOMSCOPE_DEPENDENCY_WINDOW_HOURS | 24 | Flow window the application topology considers |
| LOOMSCOPE_MAGIC_LINK_ECHO | off | Prints magic-link sign-in URLs to the log. Development only |
LOOMSCOPE_DB_POOL_MAX is a per-process budget divided across seven internal
pools by fixed shares, not a per-pool limit. The default of 25 leaves room,
against a stock max_connections of 100, for a rolling deploy (two servers),
a worker — which holds one connection per scheduled job on top of this — and
a psql session. Raise it only alongside max_connections.
LOOMSCOPE_JOBS_IN_WEB defaults to on deliberately. Making it opt-in
would mean any deployment that had not yet added the worker container
silently ran no scans, no CVE matching and no snapshots. Running it in both
places is safe rather than harmful — every tick takes a PostgreSQL advisory
lock, so only one process executes each one — but it puts the work in the
process serving requests, which is the thing worth moving away from.
AI assistant
Off unless a provider is configured. See AI assistant.
| Variable | Purpose |
|---|---|
LOOMSCOPE_AI_DEFAULT_PROVIDER | anthropic, openai or ollama |
LOOMSCOPE_ANTHROPIC_API_KEY | Enables the assistant with Anthropic |
LOOMSCOPE_OPENAI_API_KEY | Alternative provider |
LOOMSCOPE_OLLAMA_BASE_URL | Local models — nothing leaves the network with this one |
Daemon
The daemon is configured entirely by environment. Under systemd, use
EnvironmentFile=/etc/loomscope/daemon.env.
| Variable | Required | Default | Purpose |
|---|---|---|---|
LOOMSCOPE_SERVER_URL | Yes | — | Control plane URL |
LOOMSCOPE_DAEMON_API_KEY | Yes | — | Authentication. The process exits if unset |
LOOMSCOPE_DAEMON_NAME | No | default | Identity in the UI. Give each daemon a distinct one |
LOOMSCOPE_DAEMON_SITE_CODE | No | — | Binds to a site on first registration. UI assignment wins |
LOOMSCOPE_NETWORK_CIDRS | No | — | Comma-separated ranges to scan without registering them |
LOOMSCOPE_SIGNATURES_DIR | No | /etc/loomscope/signatures.d | Custom YAML signatures, reloaded on SIGHUP |
LOOMSCOPE_DOCKER_DISCOVERY | No | false | Read the Docker socket. Requires mounting it |
LOOMSCOPE_TLS_DISABLED | No | false | Skip the TLS handshake probe |
LOOMSCOPE_NETFLOW_PORTS | No | 2055,6343 | UDP listeners for NetFlow v5/v9, IPFIX and sFlow |
LOOMSCOPE_FLOW_DELIVERY_INTERVAL | No | 60s | How often collected flows are posted, independent of scans |
LOG_LEVEL | No | info | Same levels as the control plane |
LOOMSCOPE_DOCKER_DISCOVERY is opt-in because mounting the Docker socket
grants effective root on the host. It is worth it for a container-dense node
and worth refusing everywhere else.
CVE ingest worker
| Variable | Purpose |
|---|---|
DATABASE_URL | Same database as the control plane |
LOOMSCOPE_CVE_MIRROR | Directory holding the offline OSV/NVD mirror, for air-gapped installs |
LOOMSCOPE_CVE_MASTER_SOURCE | Which feed wins when two describe the same CVE. Default nvd. The master overwrites and nothing overwrites it; other feeds still write advisories the master never issued |
LOOMSCOPE_OPENCVE_URL | A self-hosted OpenCVE instance to ingest from. Unset means the feed is off |
LOOMSCOPE_OPENCVE_TOKEN | Organization API token, opc_org.<token_id>.<secret> |
LOOMSCOPE_OPENCVE_FIRST_RUN_DAYS | How far back the first pass reaches when there is no cursor. Default 30 |
Database bootstrap (compose only)
These are read by the postgres service in the compose file, not by the
application.
| Variable | Default | Notes |
|---|---|---|
POSTGRES_USER | loomscope | |
POSTGRES_PASSWORD | — | Required. Generate with -hex, never -base64 — see the note below |
POSTGRES_DB | loomscope |
Generate the password with openssl rand -hex 24. Base64 output contains /
and + about half the time, and this value ends up inside DATABASE_URL —
where those characters make the connection string an invalid URL that the
driver refuses outright.
Secret handling
- Nothing secret belongs in
.envon a shared host. In Kubernetes, every secret comes from aSecretyou create yourself; the Helm chart references them and refuses to generate any. See Kubernetes. - Secrets are never logged. A redacting logger drops credentials, API keys, SNMP community strings and session tokens before they reach a log line.
- API keys are hashed at rest, so a daemon key or a SCIM token is shown once and then only ever compared.
Verifying configuration
# Is the control plane healthy, and is the database reachable?
curl -fsS http://localhost:3000/api/health
# Did the daemon lose its ICMP privilege and fall back?
docker logs loomscope-daemon | grep "falling back to TCP"
# Is the connection budget being respected?
psql "$DATABASE_URL" -c "SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();"A variable that used to be here
NEXT_PUBLIC_AUTH_URL is gone. It was documented as "the client-side auth
URL, when it differs from BETTER_AUTH_URL", and it could not work:
NEXT_PUBLIC_* is substituted at build time, so a released image
carried whatever the build machine had — in practice the fallback,
http://localhost:3000 — compiled into the JavaScript sent to browsers.
A page served from any real hostname then tried to sign in against the visitor's own machine, and the browser refused it as cross-origin before the request left. The symptom was a sign-in that span forever rather than failing, because the rejected fetch had nothing waiting on it. No server-side setting could fix it; the wrong value was already in the bundle.
The client now uses the origin the page came from, which is where the auth routes are. There is nothing to configure.