#2618·runtipi

Keep core secrets (`JWT_SECRET`, `POSTGRES_PASSWORD`) out of every app's env scope (hardening)

Author: etvtCreated Jul 6, 2026Updated Jul 6, 2026

Follow-up to #2617 — a separate, related hardening idea (different root cause, filing on its own).

What I noticed: each app's generated app.env is seeded from the global Runtipi .env, then app vars are merged on top — so every app's app.env carries the core infra secrets (JWT_SECRET, POSTGRES_PASSWORD, RABBITMQ_PASSWORD), not just its own.

  • packages/backend/src/modules/apps/app.helpers.tsgenerateEnvFile reads the global env as its base, writes the merged result to app.env.
  • packages/backend/src/core/config/configuration.service.ts — those secrets live in that global env.
  • app.env is written to the app-data root ({app-data}/{store}/{app}/app.env), a sibling of the data/ folder templates mount.

Two ways those core secrets can reach an app container:

  1. docker compose --env-file app.env puts them all in the interpolation scope, so a template can reference them directly. Not theoretical — apps/calcom/docker-compose.yml resolves ${POSTGRES_PASSWORD} / ${POSTGRES_USERNAME} (calcom has no such form fields, so these are the core creds), injects them into its containers, and sets its bundled DB to the core password.
  2. A volume misconfig — mounting ${APP_DATA_DIR} instead of ${APP_DATA_DIR}/data hands the whole app.env to the container.

Why it compounds with #2617: with POSTGRES_PASSWORD in hand, runtipi-db is reachable over the shared tipi_main_network → a compromised app can reach the core DB. And FWIW no official app actually reuses the core DB (no runtipi-db references anywhere; every DB app runs its own Postgres), so there's no efficiency reason for apps to hold the core creds — the calcom case looks like an accidental footgun, not intended reuse.

Suggestion — two small, independent changes:

  1. Scope app.env to app-only vars (don't seed from the global .env). Core secrets stay in Runtipi's own env. Bonus: templates leaning on ${POSTGRES_PASSWORD} would surface instead of silently getting the core secret.
  2. Move app.env out of the mountable data dir — it's Runtipi metadata, not app data, so I think it'd fit better next to the generated compose in the install dir rather than in app-data/ which users bind-mount. Fixes the wrong-mount exposure with no new var. (Backup-safe: backupApp already archives both the data dir and the install dir — backup.manager.ts:70-77 / restore :169-170 — so the env still rides along in restores.)

(calcom's template probably also wants its own random CALCOM_DB_PASSWORD regardless.)

Understood there may be history behind the current layout — just flagging in case it's worth tightening.

(Same heads-up: put together with AI assistance — Claude Opus 4.8 — so please sanity-check the code refs.)