Skip to main content

Environment variables

The portal reads its configuration from .env. The bundled .env.example enumerates every supported key. The install wizard (scripts/install.sh) populates the required keys with strong defaults; the rest you set as needed.

Audience

Operators tuning a deployment. Familiarity with .env files and Docker Compose's variable substitution.

Reading order

  1. .env in the repo root is loaded by docker-compose automatically.
  2. Backend code calls os.getenv() at runtime, never at module import time. This is CLAUDE.md rule #11. Restarting the container is enough to pick up a changed value — no rebuild needed.
  3. Compose substitutes ${VAR} references in docker-compose.yml from .env at docker-compose up time.

Every key listed below is read by apps/backend/core/config.py, docker-compose.yml, or scripts/* — the Read by column tells you which.

Required keys

These four must be present and non-empty. The wizard sets them.

KeySet byRead byNotes
SECRET_KEYwizard (openssl rand -hex 32)config.pyJWT signing key (HS256). Minimum 32 chars in non-dev. Rotating invalidates every refresh token.
DATABASE_URLwizardconfig.py, docker-compose.ymlpostgresql+asyncpg://user:pass@postgres:5432/trustedoss. Must use the postgres host (compose service name).
CORS_ALLOWED_ORIGINSwizardconfig.pyComma-separated. Production must enumerate origins explicitly — * is rejected at boot when allow_credentials=true.
DOMAINwizarddocker-compose.ymlHostname used by Traefik's host-rule. Stripped of scheme and path.

Application

KeyDefaultRead byDescription
APP_ENVdevconfig.pydev, staging, or prod. Drives a few CORS / log defaults.
LOG_LEVELINFOconfig.pyDEBUG, INFO, WARNING, ERROR.
DEMO_READ_ONLYfalseconfig.pyWhen truthy (1/true/yes/on), the backend runs as a read-only live demo: every non-auth mutation (POST/PUT/PATCH/DELETE) is rejected with an RFC 7807 403. Surfaces on GET /health so the SPA shows a banner. See Live demo.
IMAGE_TAG0.11.0docker-compose.ymlPinned tag for ghcr.io/trustedoss/trusca-backend, …/trusca-backend-worker, …/trusca-frontend.

Database

DATABASE_URL (above) is the canonical setting. The composed alternative below is provided so the GCP Cloud Run module can mount DB_PASSWORD from Secret Manager without baking the DSN into Terraform state. Set either DATABASE_URL or the four DB_* keys — never both.

KeyDefaultRead byDescription
DATABASE_URLconfig.py, docker-compose.ymlSee above.
DB_USERconfig.pyComposed-DSN: username. URL-encoded in the resulting DSN.
DB_PASSWORDconfig.pyComposed-DSN: password. URL-encoded so @, :, /, #, % survive parsing.
DB_HOSTconfig.pyComposed-DSN: host. May be a Cloud SQL Auth Proxy unix socket path (/cloudsql/...).
DB_PORT5432config.pyComposed-DSN: port.
DB_NAMEconfig.pyComposed-DSN: database name.
POSTGRES_USERtrustedossdocker-compose.ymlUsed by the postgres container's init. Must match DATABASE_URL.
POSTGRES_PASSWORDdocker-compose.ymlGenerated by the wizard.
POSTGRES_DBtrustedossdocker-compose.ymlDatabase name.

If any of the four DB_* keys is set, all of them must be set (or the composed branch raises at boot). The portal uses async SQLAlchemy + asyncpg. Connection pool defaults are tuned for the FastAPI worker count (4 uvicorn workers × 5 connections each = 20).

Redis & Celery

KeyDefaultRead byDescription
REDIS_URLredis://redis:6379/0config.pyBroker + result backend.
CELERY_CONCURRENCY2docker-compose.ymlWorker process count. Each slot needs ~2 GB RAM at peak.

Authentication

KeyDefaultRead byDescription
SECRET_KEYconfig.pySee Required keys. HS256 signing.
ACCESS_TOKEN_EXPIRE_MINUTES30config.pyJWT access token lifetime.
REFRESH_TOKEN_EXPIRE_DAYS7config.pyRefresh token lifetime. Rotation + reuse detection enabled.

Vulnerability data

The portal correlates SBOMs against CVEs using a local Trivy DB — a compiled bundle of NVD + OSV + GHSA + EPSS + KEV. See Vulnerability data (Trivy DB) for the lifecycle.

KeyDefaultRead byDescription
TRIVY_DB_REPOSITORYghcr.io/aquasecurity/trivy-dbconfig.pyOCI repository the Trivy DB is pulled from. Override for an air-gapped internal mirror — see Air-gapped operation.
TRIVY_DB_REFRESH_HOURS168 (weekly)config.pyCelery Beat schedule for the trivy_db_refresh task. Lower for fresher feeds, higher to reduce egress.
TRIVY_CACHE_DIR/var/lib/trivyintegrations/trivy.pyDirectory the DB is unpacked into. Backed by the shared trivy-cache volume — worker (rw) and backend (ro) mount it so the admin health / disk panels can read the DB state.
TRIVY_TIMEOUT_SECONDS300config.pyPer-scan timeout for trivy sbom. Raise to 600900 for very large monorepos.

Build / policy gate

The CI build gate fails a build on Critical CVEs and forbidden licenses out of the box; those conditions are not env-driven. The single env knob below adds an optional EPSS dimension.

KeyDefaultRead byDescription
GATE_EPSS_THRESHOLD(unset)config.pyOptional EPSS gate. A value from 0 to 1. When set, the build gate also fails if any open finding has epss_score >= GATE_EPSS_THRESHOLD, and the gate result carries epss_gate_count + epss_threshold. Unset (the default) disables the EPSS gate — only the existing Critical-CVE / forbidden-license conditions apply. Findings without an EPSS value never trip the gate. EPSS data is sourced from the Trivy DB, so only CVEs Trivy supplies a value for are eligible.

See build gate for the gate model and Gate the build on EPSS for the CI walkthrough.

Scan pipeline

KeyDefaultRead byDescription
TRUSTEDOSS_SCAN_BACKENDrealconfig.pyreal (subprocess cdxgen / scancode / Trivy) or mock (fixture JSON). mock is the dev / CI default for the test harness; production must leave this as real.
SCANCODE_TIMEOUT_SECONDS600config.pyHard wall-clock limit for the scancode first-party license stage. On timeout the scan continues with declared licenses only (best-effort).
SCANCODE_MAX_FILES20000config.pyCeiling on eligible first-party files (after the exclude filter). Over this, scancode is skipped and the scan keeps declared licenses only.
SCANCODE_MAX_DETECTIONS5000config.pyCap on the number of detected-license findings persisted per scan.
SCANCODE_MAX_RESULT_BYTES268435456 (256 MB)config.pyCeiling on the scancode JSON artefact before parsing — guards against an OOM from a hostile tree.
WORKSPACE_HOST_PATH/tmp/trustedossconfig.py, docker-compose.ymlHost directory mounted into the worker as /workspace. Holds repo clones + scan artefacts (cdxgen SBOM, scancode output). The compose stack overrides this to /workspace inside the container.
ORT_RULES_PATH/opt/trustedoss/ort/rules.ktsdocker-compose.ymlLegacy path inside the worker, vestigial after the ORT stage was removed. The file is a placeholder and has no effect in this release — license-tier classification comes from _LICENSE_CATEGORY_DEFAULTS in apps/backend/tasks/scan_source.py.
JSONB_ROW_SIZE_LIMIT_BYTES262144 (256 KB)config.pyPer-row JSON byte ceiling before the writer truncates and emits a warning. Guards the I-1 unbounded-payload class.

Scan retention

These keys tune the automatic retention sweep that reclaims superseded and stale scan snapshots. The sweep runs as a Celery beat task every 6 hours. See Scan retention for the full model.

KeyDefaultRead byDescription
SCAN_RETENTION_SUPERSEDED_GRACE_DAYS7config.pyDays a superseded snapshot is kept before the sweep reclaims it. A snapshot is superseded when a newer successful scan lands on the same (project, normalized ref) target. Set higher to keep more rollback history per target.
SCAN_RETENTION_KEEP_LAST30config.pyMinimum number of ref-less and failed scans kept per project, regardless of age. The sweep never trims below this floor — it protects ad-hoc and diagnostic scans that carry no ref target.
SCAN_RETENTION_MAX_AGE_DAYS180config.pyHard age ceiling. Any non-release scan older than this is reclaimed by the sweep even if it is still the live snapshot for its target. Scans labelled metadata.release are exempt and kept forever.

WebSocket gateway

KeyDefaultRead byDescription
WEBSOCKET_MAX_CONNECTIONS_PER_USER3config.pyPer-user concurrent connection ceiling. The 4th connection from the same user evicts the oldest with close code 1001 (reason="newer_connection"). The cap is enforced per worker process — multi-worker deployments allow up to N × worker-count.
WEBSOCKET_AUTH_TIMEOUT_SECONDS1.0config.pyHow long the gateway waits for the first {"type":"auth"} frame. Connections that miss the window are closed with 1008 / reason="auth_timeout".

Notifications

KeyDefaultRead byDescription
SMTP_HOST(empty)config.pySMTP server. Without it, email notifications raise NotificationDisabled and the channel is skipped.
SMTP_PORT587config.pySMTP port. STARTTLS expected on 587.
SMTP_USER(empty)config.pySMTP username.
SMTP_PASSWORD(empty)config.pySMTP password.
SMTP_USE_STARTTLStrueconfig.pySet false only for SMTP servers that demand implicit TLS on 465 or are testing on 25.
SMTP_FROMno-reply@trustedoss.localconfig.pyFrom: header for outgoing notifications. Override per environment.
SMTP_TIMEOUT_SECONDS10config.pyPer-call SMTP socket timeout.
SLACK_WEBHOOK_URL(empty)config.pyOrg-wide Slack webhook for super_admin notifications. Per-team webhooks are configured in the UI.
TEAMS_WEBHOOK_URL(empty)config.pyOrg-wide MS Teams webhook.
NOTIFICATION_HTTP_TIMEOUT_SECONDS10config.pyOutbound HTTP timeout for Slack / Teams webhooks.

Password reset

KeyDefaultRead byDescription
PASSWORD_RESET_BASE_URLhttp://localhost:5173config.pyFrontend base URL embedded in reset emails. The link template is {base}/reset-password?token={token}.
PASSWORD_RESET_RATE_LIMIT5/minuteconfig.pyPer-IP slowapi limit for POST /auth/forgot-password.
PASSWORD_RESET_EMAIL_COOLDOWN_SECONDS300config.pyMinimum seconds between two reset emails to the same address. Returned as Retry-After on cooldown.

OAuth (demo SaaS only)

These apply to the demo SaaS deployment. Self-hosted installs leave them empty (the /auth/oauth/{provider}/authorize endpoint then returns 503 with oauth_provider_disabled = true).

KeyDefaultRead byDescription
GITHUB_CLIENT_ID(empty)config.pyGitHub OAuth App client ID.
GITHUB_CLIENT_SECRET(empty)config.pyGitHub OAuth App client secret.
GOOGLE_CLIENT_ID(empty)config.pyGoogle OAuth client ID.
GOOGLE_CLIENT_SECRET(empty)config.pyGoogle OAuth client secret.
OAUTH_STATE_TTL_SECONDS300config.pyLifetime of the signed state JWT (CSRF guard). RFC 6749 §10.12.
OAUTH_HTTP_TIMEOUT_SECONDS10config.pyOutbound HTTP timeout to OAuth provider APIs.
OAUTH_LOGIN_REDIRECT_DEFAULThttp://localhost:5173/config.pyWhere the SPA lands after a successful OAuth callback.
OAUTH_LOGIN_REDIRECT_FAILUREhttp://localhost:5173/loginconfig.pyWhere the SPA lands when the callback fails. Receives ?error=oauth_failed.

Backups

KeyDefaultRead byDescription
BACKUP_RETENTION_DAYS7scripts/backup.shscripts/backup.sh --no-prune overrides on a per-run basis.
BACKUP_DIR<repo>/backupsscripts/backup.shWhere the backup script writes.

Disk guards

KeyDefaultRead byDescription
DISK_HARD_LIMIT_PCT95.0apps/backend/services/scan_service.pyRed gauge + new scans blocked + admin notification.

Traefik / TLS

KeyDefaultRead byDescription
DOMAINdocker-compose.ymlSee Required keys.
TLS_EMAILdocker-compose.ymlEmail used by Let's Encrypt's HTTP-01 challenge. Required for cert issuance.
TRAEFIK_LOG_LEVELINFOdocker-compose.ymlDEBUG is useful when chasing routing issues.

Optional integrations

KeyDefaultRead byDescription
JIRA_ENABLEDfalse(none)Stub only — not consumed by any code path in this release. Reserved for the Phase B Jira integration; included in .env.example so existing deployments do not break when the feature lands.
JIRA_URL(empty)(none)Stub. See above.
JIRA_TOKEN(empty)(none)Stub. See above.
HTTP_PROXY / HTTPS_PROXY / NO_PROXY(empty)subprocess envHonored by git clone, cdxgen, and the trivy --download-db-only boot / refresh path.

Bootstrap / scripts

These keys are read only by the bootstrap and demo-seed scripts. They are not consumed by the running backend, but you set them at install / demo time.

KeyDefaultRead byDescription
ADMIN_EMAILapps/backend/scripts/create_super_admin.pyEmail of the first super-admin to provision when the script is invoked. Lower-cased and stripped on read.
ADMIN_PASSWORDapps/backend/scripts/create_super_admin.pyPassword for the bootstrap super-admin. Must be ≥ 12 characters; the script aborts otherwise.
DEMO_SUPER_ADMIN_PASSWORD(auto-generated)apps/backend/scripts/seed_demo.pyOverride for the demo seed's super-admin password. Required when APP_ENV is staging or prod; must be ≥ 12 characters when set.

Validation

The backend validates the configuration on startup (apps/backend/main.py lifespan):

  • Refuses to start if SECRET_KEY is shorter than 32 chars (any non-dev APP_ENV).
  • Refuses to start if CORS_ALLOWED_ORIGINS contains * while credentials are allowed.
  • Refuses to start in APP_ENV=prod if any origin uses plain http://.
  • Refuses to start if DB_* keys are partially set (all-or-nothing on the composed DSN path).

Failures emit a structured log line and crash the process — there is no permissive fallback.

Verify it worked

After editing .env:

docker-compose -f docker-compose.yml restart backend worker beat
docker-compose -f docker-compose.yml logs --tail=50 backend | grep backend_starting

The startup log emits a single backend_starting event with the app_env field. Secrets are never logged.

See also