NVIDIA-NeMo / NVIDIA-NeMo/Gym

Server config included in process args can expose secrets from env.yaml

Open
#2,909 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

community-request
Dominant language
Python
Stars
1.2k
Forks
349
Avg merge
1d 21h
Merged PRs (30d)
318

Description

Server config is included in process arguments

Describe the bug

Gym includes the full server config in the shell command it uses to start each server. That makes config values visible in the process arguments. A secret stored literally in env.yaml, or otherwise materialized in the in-memory config, is included too. Since model and judge configs can contain API keys, those values should not be put there.

The bug only applies when the secret value is actually present in the serialized in-memory config (as opposed to something like n unresolved OmegaConf reference like ${oc.env:MODEL_API_KEY}).

I found this while checking a local Gym run with ps. Process arguments can also be collected by CI diagnostics, cluster monitoring, and similar tools. No real credential is used or included in this report.

How long the config stays visible depends on the shell and logging wrapper. I measured these cases with a fake value and a clean pre-launch process-table baseline:

  • On Linux with Bash 5.2, the value is present in a Bash argv while setup is running. Without nemo_gym_log_dir, Bash replaces the launcher after setup, so the value is absent while app.py runs.
  • With nemo_gym_log_dir set, run_command() wraps the command in set -o pipefail; (...) | tee -a. On Linux, the value remains in a Bash argv while app.py runs.
  • On macOS with Bash 3.2, the value remained in a Bash argv while app.py ran in both logging modes.

Steps/code to reproduce the bug

I reproduced this on Gym main at commit 44181d3e4acc7856b7e3596a7ae135de5a039ea3.

  1. Set up the repository environment normally:

    uv sync --extra dev
    
  2. Run the attached reproducer from the Gym repository root:

    PYTHONPATH="$PWD" uv run python reproduce_config_argv_exposure.py
    

The reproduction script uses the fake value fake-api-key-for-gym-reproducer. It follows Gym's real RunHelper.start() server-launch path and stops immediately before the process is created, so it does not actually start a server.

Output on main:

fake API key: fake-api-key-for-gym-reproducer
fake API key present in process command: True
captured command fragment: api_key: fake-api-key-for-gym-reproducer
fake API key passed through child environment: False

The attached reproduce_literal_env_yaml.py also calls Gym's real config parser with a temporary env.yaml. It confirms the fake key reaches the launch serialization, while an ${oc.env:...} doesn't:

fake API key loaded from env.yaml: True
fake API key present in launch serialization: True
fake API key resolved from an environment reference: True
referenced fake API key present in launch serialization: False

The attached reproduce_linux_process_window.py separately checks the setup and active-app windows using the same Popen(..., executable="/bin/bash", shell=True) call and logging wrapper as Gym. It should print something similar to below:

logging unset
  matching processes before launch: []
  setup started: True
  matching processes while setup runs: [(7, 'bash')]
  app started: True
  matching processes while app runs: []
logging enabled
  matching processes before launch: []
  setup started: True
  matching processes while setup runs: [(9, 'bash'), (10, 'bash')]
  app started: True
  matching processes while app runs: [(9, 'bash')]

Expected behavior

The serialized config, especially secret values, should not be part of the shell command.

Config

The reproducer uses this minimal in-memory config:

policy_model:
  responses_api_models:
    demo:
      entrypoint: app.py
      api_key: fake-api-key-for-gym-reproducer

Environment details

  • Gym commit: 44181d3e4acc7856b7e3596a7ae135de5a039ea3
  • OS: macOS 26.5.2 (25F84)
  • Python: 3.13.14
  • Environment created with uv sync --extra dev from the repository lock
  • Full uv pip list --format freeze output is attached as uv-pip-list.txt
  • No GPU or sandbox provider is needed for reproduction
  • Also reproduced on linux with env: Linux 6.8.0 aarch64, Bash 5.2.37, Python 3.12 container image digest sha256:e5931cdb4a8cec0ad083277c16a39114f14123b8b6c858c8c9689b677789975c

Additional context

The issue is in RunHelper.start() in nemo_gym/cli/env.py. It serializes the global config and adds it to the command passed to run_command(), which then uses Popen(..., shell=True).

Gym could instead pass NEMO_GYM_CONFIG_DICT and NEMO_GYM_CONFIG_PATH through the environment mapping given to Popen.

With that change, the same reproducer prints:

fake API key: fake-api-key-for-gym-reproducer
fake API key present in process command: False
fake API key passed through child environment: True

Moving the config into the child environment keeps it out of the more visible process command line. Someone who can inspect another process's environment may still be able to read it.

A tested patch is available on rystewart-nvidia:secure-server-config. It passes the two config values through Popen(env=...) instead and adds a regression test that checks a fake key is absent from the command. The branch also lets callers name credential variables that Gym should remove from the orchestrator environment before Ray starts. That keeps unrelated Ray workers from inheriting those variables.

uv-pip-list.txt
reproduce_literal_env_yaml.py
reproduce_linux_process_window.py
reproduce_config_argv_exposure.py

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue points to RunHelper.start() in nemo_gym/cli/env.py and the run_command() Popen path; read those entry points first. Review the regression test mentioned in the issue and consider the work done when serialized secrets no longer appear in the launch command while the documented config handoff still works.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.