Files
tuliprox/docs/src/configuration/secrets.md
T
euzuandGitHub fa5d3237f1 added docker ulimits, .env support, ptt fix (#867)
- **New Features**
  - Added automatic `.env` loading for secrets and environment variables, with custom file paths and existing-environment precedence.
  - Added a `--env-file` option and a sample environment configuration template.
  - Increased Docker file descriptor limits for improved reliability under high connection counts.

- **Bug Fixes**
  - Fixed title parsing for multi-byte UTF-8 characters, preventing crashes and incorrect volume extraction.

- **Documentation**
  - Added guidance for `.env` configuration, Docker usage, restart requirements, and resolving file descriptor exhaustion.
2026-09-15 12:51:09 +02:00

12 KiB

🔐 Secrets & Environment Variables

Every deployment needs credentials: provider logins, output-user passwords, webhook tokens, web server secrets. Tuliprox keeps the repository free of real credentials while making it easy to inject them at runtime.

Host-agnostic. These instructions deliberately do not assume any particular host, container platform, or cloud provider. Secrets are read from the process environment and from files we generate on the machine itself, so the same workflow works whether you run a container, a service manager, a process supervisor, a small server, or a hosted secret store of your choice. The mechanism to supply an environment variable is always the same: it must be present in the environment of the tuliprox process at startup.

What counts as a secret

File Field Example env var
source.yml input url, username, password CLOUDTV_1_URL, CLOUDTV_1_USER, CLOUDTV_1_PASS
source.yml input epg.sources[].url CLOUDTV_1_EPG_URL
source.yml input panel_api.api_key (account management) PROVIDER_PANEL_API_KEY
api-proxy.yml output user password / token for every published user XTR_USER_LOCAL_PASS, XTR_USER_LOCAL_TOKEN
config.yml web_ui.auth.secret (JWT signing, 64-hex) TULIPROX_WEB_SECRET
config.yml messaging webhooks/tokens: Telegram bot token, Discord URL, Pushover token/key, ntfy/Gotify tokens, Slack URL, generic REST URL + signing_secret + Authorization headers TULIPROX_DISCORD_WEBHOOK, TULIPROX_TELEGRAM_TOKEN, TULIPROX_SIGNING_SECRET
config.yml metadata_update.tmdb.api_key TULIPROX_TMDB_API_KEY
config.yml reverse_proxy.rewrite_secret TULIPROX_PROXY_REWRITE_SECRET
config/user.txt Web UI Argon2 password hashes (see below — not env-injectable) file only

Anything a provider, a player, a notification bot, or a browser authenticates with is a secret and must not be in git.

${env:VAR} interpolation

Every config file that Tuliprox reads (config.yml, source.yml, api-proxy.yml, mapping.yml, template.yml) supports environment-variable interpolation with the syntax:

${env:VAR_NAME}

Variable names match [a-zA-Z_][a-zA-Z0-9_]*.

  • How it works: the placeholder is resolved before the file is parsed as YAML, from the environment of the running process. If the variable is missing, Tuliprox logs Could not resolve env var 'VAR_NAME' and leaves the literal ${env:VAR_NAME} in place so the problem is visible.

  • Quoting: keep string values quoted, exactly like the value the variable will expand to:

    inputs:
      - name: provider
        type: xtream
        url: "${env:CLOUDTV_URL}"
        username: "${env:CLOUDTV_USER}"
        password: "${env:CLOUDTV_PASS}"
    

    This stays valid YAML before and after substitution.

  • Use only for string fields. Numbers (exp_date, ports, token_ttl_mins) and booleans (enabled) still belong directly in the file; injecting them through env vars is fragile.

  • Not supported in user.txt. Web UI credential files are read directly and must be written on the machine (see below).

  • Also works in CLI paths: any --home / -c / -i / -a argument can be ${env:...} too.

.env file support

Tuliprox automatically loads environment variables from a .env file at startup. This makes it easy to keep protected credentials in a local .env file without committing them to source control (the repository's .gitignore already ignores .env and .env.*). A sample template is provided at config/.env.example.

Discovery and precedence

Tuliprox searches for a .env file in the following order:

  1. Explicit CLI argument or environment variable: -e, --env-file <PATH> or TULIPROX_ENV_FILE=<PATH>. If explicitly specified, the file must exist and have valid syntax; otherwise Tuliprox exits with an error.
  2. Config file directory: if -c <PATH> was provided, the parent directory of that file (e.g. <config_file_dir>/.env).
  3. Config directory: <config_path>/.env (default: <home_path>/config/.env).
  4. Home directory: <home_path>/.env.
  5. Current working directory: ./.env.

The first matching file found is loaded.

Docker usage

There are three common ways to use .env files with Docker:

  • Automatic via mounted config directory (Recommended): If your docker-compose.yml mounts ./config:/app/config, simply place your .env file at ./config/.env on the host. Because Tuliprox starts with -p /app/config, it automatically discovers and loads /app/config/.env at startup.

  • Custom location via TULIPROX_ENV_FILE: If your secrets file is located elsewhere on the host, mount it into the container and point the TULIPROX_ENV_FILE environment variable to it:

    services:
      tuliprox:
        image: ghcr.io/euzu/tuliprox:latest
        environment:
          - TULIPROX_ENV_FILE=/secrets/tuliprox.env
        volumes:
          - /etc/secrets/tuliprox.env:/secrets/tuliprox.env:ro
          - ./config:/app/config
    
  • Native Docker Compose env_file: directive: Docker Compose can directly populate container environment variables from a .env file on the host without mounting:

    services:
      tuliprox:
        image: ghcr.io/euzu/tuliprox:latest
        env_file:
          - .env
    

12-factor rules: Existing environment variables take precedence

Variables already defined in the system or container environment (e.g. via export VAR=value, Docker environment:, or Kubernetes secrets) are never overwritten by a .env file. The .env file serves as a default/fallback configuration.

⚠️ Restart required on change (no dynamic hot-reload)

Important: Changes to .env files do not take effect dynamically via configuration reload. For mounted .env files or bare-metal installations, restart the tuliprox service or container (docker compose restart). When using Docker Compose env_file:, recreate the container (docker compose up -d --force-recreate) to apply changed values.

Why: In Rust, modifying environment variables in a multi-threaded runtime (setenv) is inherently not thread-safe and can cause undefined behavior or data races with concurrent readers. .env values are loaded strictly once at application startup and require a restart to change.

Other ways of supplying variables

Because Tuliprox resolves from the process environment, any standard mechanism works alongside or instead of .env:

  • export TULIPROX_WEB_SECRET=... in the shell / init script that starts the process,
  • environment: entries in a container or compose file,
  • Environment= lines of a service unit,
  • the secret store / environment settings of whatever runtime you picked.

Web UI credentials (user.txt)

Tuliprox stores password hashes, never plain text. Each line is username:argon2_hash[:group1,group2]; without groups the user defaults to admin.

  1. Generate a hash with the interactive CLI prompt (it needs a real terminal and cannot read from stdin):

    tuliprox --genpwd
    
  2. Write the line into config/user.txt (or the user file configured in config.yml):

    myuser:$argon2id$v=19$m=19456,t=2,p=1$...
    
  3. Restart the server (or reload) and verify the login.

user.txt is environment-injectable only in the sense that the path may come from a CLI/${env:...} path; the hashes themselves are generated on the machine and are never committed. The config/user.txt shipped in this repository contains sample hashes for the demo accounts test / nobody documented in config/README.md — replace them before going live.

JWT secret (web_ui.auth.secret)

If web_ui.auth.secret is omitted, Tuliprox generates one in-memory and all active logins are invalidated on every restart. For production, pin a static 64-character hexadecimal string and keep it stable:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Put the result in an environment variable and reference it from config.yml:

web_ui:
  auth:
    secret: "${env:TULIPROX_WEB_SECRET}"

Rotating it invalidates all sessions — do it deliberately, not on a whim.

Pre-publish checklist

Run these before pushing anything to a public repository:

# Anything obviously credential-like in tracked configs
git grep -n -i -E "(password|secret|token|api_key)[[:space:]]*:" -- config/

# Explicit values that should have been placeholders or generated on-site
git grep -n -i -E "your_|TODO|changeme|example|\.secret|localsecret" -- config/

# Staged files (never a secret should be here)
git diff --cached --stat
git status --porcelain

Other good habits:

  • Keep logging.sanitize_sensitive_info: true (default). It masks passwords, provider URLs and client IPs in logs so shared logs can't leak credentials.
  • Keep runtime_config_report_enabled: false unless you need a startup dump; when enabled it masks sensitive values as *** anyway.
  • Never commit data/, target/, downloads/, cache/, backup/, .env, or any runtime directory — the repository .gitignore already covers the common ones.
  • If a secret ever lands in history, rotate it (it is compromised history, not just a file) and rewrite history with git filter-repo rather than committing new secrets on top.