Skip to content

feat: support *_FILE env variables for secrets - #209

Open
bebricoOOOOOOf wants to merge 2 commits into
remnawave:mainfrom
bebricoOOOOOOf:feat/env-file-secrets
Open

feat: support *_FILE env variables for secrets#209
bebricoOOOOOOf wants to merge 2 commits into
remnawave:mainfrom
bebricoOOOOOOf:feat/env-file-secrets

Conversation

@bebricoOOOOOOf

Copy link
Copy Markdown

Opening this as a PR for concreteness — happy to move the discussion to an issue first, as CONTRIBUTING asks, if you prefer.

Problem

Every secret the panel needs — APP_SECRET, DATABASE_URL (which carries the database password), TELEGRAM_BOT_TOKEN, METRICS_PASS, WEBHOOK_SECRET_HEADER — can only be passed as a plain environment variable, so on a Docker/Podman host they sit in a plaintext .env next to the compose file and show up in docker inspect and in every process environment.

Docker Compose already has secrets: for this: the value is mounted as a file in /run/secrets/<name>. The official postgres / mysql images consume it through the <VARIABLE>_FILE convention. This PR teaches the panel the same convention.

What it does

For any variable X known to the config schema, the value can be supplied in a file by setting X_FILE:

APP_SECRET_FILE=/run/secrets/app_secret
  • the file is read before env validation, trailing newlines stripped;
  • X and X_FILE both set → startup aborts with a clear message instead of silently picking one;
  • X_FILE pointing at an unreadable or empty file → startup aborts; the message carries the path and the errno, never the contents;
  • secret values are never logged or put into an error message.

Backward compatibility

Nothing happens unless a *_FILE variable is set — the helper walks the known keys, finds no *_FILE, and returns the config untouched. Existing .env deployments behave exactly as before.

Why resolution sits in three places

Resolved values go into process.env as well as into the validated config, because several processes read process.env directly:

surface why it needs it
common-config.module.ts (validate) api / scheduler / processor. Runs before Nest boots, so the in-process PrismaClient sees the resolved DATABASE_URL
prisma.config.ts prisma migrate deploy and prisma db seed run from docker-entrypoint.sh as a separate process before the app starts. Inlined (~10 lines) on purpose: only this file, not src/, is copied into the runtime image
cli.ts, config.seed.ts both build their own PrismaClient / Redis from process.env at import time

Doing it in docker-entrypoint.sh alone would not cover the app: docker-compose-advanced-prod.yml sets entrypoint: [] for the rest-api and processor services, so those containers never run that script.

Compose example

services:
  remnawave:
    image: remnawave/backend:3
    env_file: .env
    environment:
      APP_SECRET_FILE: /run/secrets/app_secret
      DATABASE_URL_FILE: /run/secrets/database_url
      TELEGRAM_BOT_TOKEN_FILE: /run/secrets/telegram_bot_token
    secrets:
      - app_secret
      - database_url
      - telegram_bot_token

secrets:
  app_secret:
    file: ./secrets/app_secret
  database_url:
    file: ./secrets/database_url
  telegram_bot_token:
    file: ./secrets/telegram_bot_token

The variables handed over to secrets must then be removed from .env. postgres supports POSTGRES_PASSWORD_FILE natively, so the db service can share the same secret files.

How it was verified

  • npm run build, npm run build:seed and oxlint are clean; oxfmt --check reports the same 18 pre-existing files as main does, none of them touched here.
  • Helper behaviour, compiled standalone and asserted: plain variable untouched; *_FILE read with trailing newline stripped; both set → throws without leaking either value; missing file → message has path + ENOENT and no contents; empty file → throws; empty *_FILE treated as unset.
  • Built bundles, actual startup:
    • node dist/app.js with plain APP_SECRET / DATABASE_URL and with APP_SECRET_FILE / DATABASE_URL_FILE — both pass env validation and reach NestFactory (they stop later at Redis/DB, which were not running locally);
    • both set → ❌ APP_SECRET and APP_SECRET_FILE are both set...; missing file → ❌ APP_SECRET_FILE points to "...", which can not be read: ENOENT;
    • prisma migrate deploy with only DATABASE_URL_FILE set connects to the host from the file (P1001 at that address); the same env without this change fails with P1012: Environment variable not found: DATABASE_URL;
    • node dist/cli.js and node dist/seed.js with only DATABASE_URL_FILE set pick the URL up from the file.
  • Not verified: a full boot against a live Postgres/Valkey — no Docker on the machine used.

Happy to trim the scope (drop the CLI/seed call sites, reword the .env.sample note) if you would rather keep the surface smaller.

🤖 Generated with Claude Code

Any variable known to the config schema can be provided in a file by setting
<VARIABLE>_FILE (e.g. APP_SECRET_FILE=/run/secrets/app_secret), the convention
official postgres/mysql images use, so secrets can be mounted from Docker
Compose `secrets:` instead of being kept in plaintext .env.

Resolution happens in every process that reads env directly: env validation
(api/scheduler/processor), prisma.config.ts (migrate deploy, db seed) and the
rescue CLI. Setting both <VARIABLE> and <VARIABLE>_FILE aborts the startup,
values are never logged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@snyk-io

snyk-io Bot commented Aug 21, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@what-the-diff

what-the-diff Bot commented Aug 21, 2026

Copy link
Copy Markdown

PR Summary

  • Enhanced Documentation in .env.sample
    Added instructions on how to configure Docker secrets, making it a more secure and feasible way to provide environment variables instead of using hard-set values.

  • Increased Security in prisma.config.ts
    Introduced code to fetch database details from specified file paths through the use of DATABASE_URL_FILE and DIRECT_URL_FILE environment variables. This helps secure sensitive data by implementing them as secrets.

  • Seed Configuration Update in prisma/seed/config.seed.ts
    Included a function call to loadSecretsFromFiles to retrieve variable data from files as determined by the schema configuration.

  • Function Addition in src/bin/cli/cli.ts
    The utility function loadSecretsFromFiles was added to make it easier to load environment variables from file paths, consistent with changes made in prisma.config.ts.

  • Configuration Loading Modifications in src/common/config/common-config/common-config.module.ts
    Updated the environment variable loading process to accommodate secret-variable files and ensure compatibility with the specified schema.

  • New Utility File: load-secrets-from-files.ts
    Integrated a utility function to efficiently read variables from secret files and safely incorporate them into the configuration. This also includes the functionality to handle errors tied to file access and empty values.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Docker-style *_FILE support for validated environment variables across Nest application startup, Prisma commands, the CLI, and database seeding.

  • Adds a shared loader that reads file-backed values, updates process.env, rejects conflicts, and avoids exposing secret contents.
  • Resolves secrets before constructing standalone Prisma and Redis clients.
  • Adds a Prisma CLI-specific resolver for migration and seed commands.
  • Documents the convention in .env.sample.

Confidence Score: 4/5

The PR should not merge until Prisma commands reject empty secret files consistently instead of failing later or silently falling back to another connection.

The application, CLI, and seed paths enforce the advertised empty-secret contract, but the production migration path uses separate logic that accepts an empty value and can silently replace an empty direct connection URL.

Files Needing Attention: prisma.config.ts

Important Files Changed

Filename Overview
src/common/utils/load-secrets-from-files.ts Adds the shared file-backed secret resolver with conflict, read-error, and empty-value handling.
src/common/config/common-config/common-config.module.ts Resolves file-backed values during Nest configuration validation before application services initialize.
prisma.config.ts Adds Prisma CLI secret resolution but omits the shared loader's empty-file rejection, allowing failure or silent fallback.
prisma/seed/config.seed.ts Resolves schema-backed secrets before constructing the seed process's Prisma client.
src/bin/cli/cli.ts Resolves schema-backed secrets before constructing CLI database and Redis clients.
.env.sample Documents the new *_FILE convention and mutual-exclusion requirement.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Secret["X_FILE environment variable"] --> AppLoader["Shared secret loader"]
    Secret --> PrismaLoader["Prisma config loader"]
    AppLoader --> Nest["Nest configuration validation"]
    AppLoader --> CLI["CLI and seed clients"]
    PrismaLoader --> Migrate["Prisma migrate deploy"]
    PrismaLoader --> Seed["Prisma db seed"]
    AppLoader --> Env["process.env X"]
    PrismaLoader --> Env
Loading

Reviews (1): Last reviewed commit: "feat: support *_FILE env variables for s..." | Re-trigger Greptile

Comment thread prisma.config.ts Outdated
The Nest-side loader already refuses a file that holds only newlines. The
inlined copy in prisma.config.ts did not, so an empty DIRECT_URL_FILE was
silently replaced by DATABASE_URL and an empty DATABASE_URL_FILE surfaced as an
unrelated datasource error. Both now fail with the same message as the loader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant