Skip to content

Run each command with only the env vars it may have - #191

Open
ndbroadbent wants to merge 12 commits into
mainfrom
nathan/command-env
Open

ndbroadbent wants to merge 12 commits into
mainfrom
nathan/command-env

Conversation

@ndbroadbent

Copy link
Copy Markdown
Member

Every run and exec command now gets only the env vars it may have, instead of every env var of the app's release.

What changes

  • The gateway wraps each command (internal/cmdenv). Before forwarding an exec or a process start, it replaces the Command header with a short sh wrapper that starts the command with env -i and only the env vars it gets. An env var the release doesn't set stays unset. A command given every env var has RACK_URL and the read-only env vars unset instead. The audit log, the deploy approval checks and exec_commands all read the command as it was sent.
  • No command gets RACK_URL. Convox puts the rack's API password in every app process unless the rack's system-env-disable feature gate is on.
  • New app settings:
    • command_env_vars: the env vars every command gets. When it's empty, commands get every env var except RACK_URL.
    • command_env_read_only: gives an env var another one's value, e.g. {"DATABASE_URL": "READ_ONLY_DATABASE_URL"}.
    • Changing either one needs MFA on the request.
  • CLI flags on run and exec:
    • --env NAME: another env var.
    • --all-env-vars: every env var except RACK_URL.
    • --write: the real values of the read-only env vars.
    • The flags travel as X-Command-* headers, which the gateway reads and doesn't forward.
  • What the CLI shows before MFA: the PIN dialog lists the env vars the command gets and warns in red when --write lets it change data, when --all-env-vars gives it every env var in an app that limits them, and for every exec in such an app (an exec can still read its process's environment). The touch prompt says when the command can change data. To show the red warnings, the PIN dialog moves from AppleScript to a JXA NSAlert, like the script review dialog (internal/cli/macdialog).
  • approved_deploy_commands entries are objects: {"command": ..., "env": [...], "all_env_vars": bool, "write": bool}.
    • A forward-only migration rewrites stored string lists.
    • Under a deploy approval, the command's entry decides its env vars, and the CLI flags are refused.
    • The env var form must be a JSON array of objects; a list of strings stops the gateway starting.
  • The placeholder start of an attached run always gets no env vars, so its process doesn't hold the app's env for the command that follows to read.
  • Audit log and Slack record the env vars each command got (env_vars or all_env_vars, env_read_only, write), and the web UI shows them.
  • Web: approved deploy commands are edited as JSON, with the image patterns card, now generalized as JsonSettingCard. New cards edit the two env var settings.
  • Mock rack: it unwraps commands with cmdenv.Parse, refuses unwrapped ones, and prints each command's env vars for the CLI E2E tests.
  • Cleanup: unused settings request DTOs and their stale generated schemas are deleted.

Upgrade notes

See "Each command gets only the env vars it may have (v0.1.25)" in operations/upgrades.mdx:

  • Change an APPROVED_DEPLOY_COMMANDS env var in the same release as the new image.
  • Images need sh and env.
  • A process start needs a command.
  • Upgrade the CLI after every gateway.

Testing

  • cmdenv runs the wrapper under sh, dash, bash and busybox. It checks:
    • only the listed names reach the command, with values intact (spaces, quotes, $, newlines, empty)
    • unset names are skipped
    • substitutes are applied, or omitted when their source is unset
    • RACK_URL is dropped
    • argv and exit codes pass through
    • Build and Parse round-trip
  • Proxy tests cover:
    • wrapping for process starts and for a real websocket exec
    • the audit details
    • the placeholder
    • approved entries deciding the env
    • flags refused under an approval
    • undecided commands refused
    • bad headers refused
  • CLI tests cover headers, refusals sent before any request, the dialog summary, warnings and touch text, and dry-run output.
  • The CLI E2E tests check the default, --env with command_env_vars and command_env_read_only, --write, and the refusal of --env RACK_URL.
  • Web unit tests cover the JSON card and the audit env display.
  • Mutation checks: the tests fail with env -i removed, with the env headers dropped, and with the exec warning removed.

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 88 billable files and costs up to $22.00.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 46 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 62 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: afe02df2-50c1-4301-9e94-10303d55ef69
Commits

Reviewing files that changed from the base of the PR and between c7445a1 and 9d2771d.

Files ignored due to path filters: 2
  • internal/gateway/openapi/generated/swagger.json is excluded by !**/generated/**
  • web/src/lib/generated/mfa-requirements.ts is excluded by !**/generated/**
Files selected for processing: 88
  • Dockerfile.mock-convox
  • cmd/mock-convox/handlers_processes.go
  • convox.yml
  • docs/src/content/docs/concepts/audit-logging.mdx
  • docs/src/content/docs/configuration/environment-variables.mdx
  • docs/src/content/docs/deployment/production-checklist.mdx
  • docs/src/content/docs/integrations/deploy-approvals/index.mdx
  • docs/src/content/docs/integrations/deploy-approvals/workflow.mdx
  • docs/src/content/docs/operations/upgrades.mdx
  • docs/src/content/docs/security/hardening.mdx
  • docs/src/content/docs/security/rbac/permissions.mdx
  • docs/src/content/docs/user-guide/cli/commands.mdx
  • docs/src/content/docs/user-guide/cli/mfa-verification.mdx
  • internal/cli/AGENTS.md
  • internal/cli/convox_process_approval.go
  • internal/cli/convox_process_command.go
  • internal/cli/convox_process_command_test.go
  • internal/cli/convox_process_env.go
  • internal/cli/convox_process_env_test.go
  • internal/cli/convox_process_review.go
  • internal/cli/convox_processes.go
  • internal/cli/macdialog/macdialog.go
  • internal/cli/pinentry/pinentry.go
  • internal/cli/pinentry/pinentry_test.go
  • internal/cli/scriptreview/scriptreview.go
  • internal/cli/scriptreview/scriptreview_test.go
  • internal/cli/webauthn/types.go
  • internal/cli/webauthn/webauthn.go
  • internal/cmdenv/cmdenv.go
  • internal/cmdenv/cmdenv_test.go
  • internal/cmdenv/request.go
  • internal/cmdenv/request_test.go
  • internal/gateway/AGENTS.md
  • internal/gateway/audit/command_env.go
  • internal/gateway/audit/logger.go
  • internal/gateway/db/export_test.go
  • internal/gateway/db/migration_approved_commands_test.go
  • internal/gateway/db/migrations/20261026000000_approved_deploy_command_objects.sql
  • internal/gateway/envutil/envutil.go
  • internal/gateway/handlers/dto.go
  • internal/gateway/handlers/settings_app_values.go
  • internal/gateway/proxy/approval_window_test.go
  • internal/gateway/proxy/command_env.go
  • internal/gateway/proxy/command_env_test.go
  • internal/gateway/proxy/command_length_test.go
  • internal/gateway/proxy/deploy_approval_binding_test.go
  • internal/gateway/proxy/env_filters.go
  • internal/gateway/proxy/env_protected_test.go
  • internal/gateway/proxy/forward.go
  • internal/gateway/proxy/handler.go
  • internal/gateway/proxy/script_command_test.go
  • internal/gateway/proxy/websocket.go
  • internal/gateway/rbac/gateway_routes.go
  • internal/gateway/rbac/mfa_requirements.go
  • internal/gateway/routes/route_registration.go
  • internal/gateway/settings/command_env.go
  • internal/gateway/settings/command_env_test.go
  • internal/gateway/settings/defaults.go
  • internal/gateway/settings/env.go
  • internal/gateway/settings/env_test.go
  • internal/gateway/settings/service_commands.go
  • internal/gateway/settings/service_env_helpers.go
  • internal/gateway/settings/validate.go
  • internal/gateway/settings/validate_test.go
  • internal/gateway/slack/process_notifications.go
  • internal/gateway/slack/process_notifications_test.go
  • scripts/lib/cli-e2e/db.sh
  • scripts/lib/cli-e2e/stages.sh
  • web/e2e/db.ts
  • web/package.json
  • web/src/api/generated.ts
  • web/src/api/openapi.json
  • web/src/api/schemas/deleteAppsAppSettingsCommandEnvReadOnly200.ts
  • web/src/api/schemas/deleteAppsAppSettingsCommandEnvVars200.ts
  • web/src/api/schemas/handlersUpdateAllowDestructiveActionsRequest.ts
  • web/src/api/schemas/handlersUpdateAppImagePatternsRequest.ts
  • web/src/api/schemas/handlersUpdateApprovedCommandsRequest.ts
  • web/src/api/schemas/handlersUpdateProtectedEnvVarsRequest.ts
  • web/src/api/schemas/index.ts
  • web/src/api/schemas/putAppsAppSettingsCommandEnvReadOnly200.ts
  • web/src/api/schemas/putAppsAppSettingsCommandEnvReadOnlyBody.ts
  • web/src/api/schemas/putAppsAppSettingsCommandEnvVars200.ts
  • web/src/api/schemas/settingsApprovedCommand.ts
  • web/src/components/audit-logs/command-details.test.tsx
  • web/src/components/audit-logs/command-details.tsx
  • web/src/pages/app-settings-page.tsx
  • web/src/pages/app-settings/json-setting-card.test.tsx
  • web/src/pages/app-settings/json-setting-card.tsx
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@deepsource-io

deepsource-io Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in c7445a1...9d2771d on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Oct 26, 2026 1:47a.m. Review ↗
Go Oct 26, 2026 1:47a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

… it may have

The rack splits a command into words and runs them without a shell, in a process holding every env var of the
app's release, RACK_URL (the rack's API password) included. cmdenv builds the plan of what a command gets (every
env var less RACK_URL, or only the listed ones, with read-only substitutes such as DATABASE_URL from
READ_ONLY_DATABASE_URL) and wraps the command in a fixed sh script that runs it with exactly that environment.
Parse reads a wrapper back, for the mock rack. The tests run the wrapper with sh, dash, bash and busybox.
Every exec and process start reaches the rack wrapped by cmdenv, so the command runs without RACK_URL (the rack's
API password, which Convox gives every process) and, in an app that sets command_env_vars, with only those env vars
and the ones it asks for (X-Command-Env, or X-Command-All-Env-Vars for every one). command_env_read_only gives
read-only vars their substitute's value (DATABASE_URL from READ_ONLY_DATABASE_URL, say) unless the command asks to
write (X-Command-Write). The audit log records the command as sent, with the env vars it got.

approved_deploy_commands entries are objects now ({"command", "env", "all_env_vars", "write"}): under a deploy
approval the entry decides the command's env vars, and a command asking for any itself is refused. A migration
turns each stored command string into an object.

Also refused before anything runs: a command the rack can't split into words, an empty one (a process start
without a command runs the service's own command, which the gateway can't wrap), and one starting with NAME=value.
`convox run`'s placeholder gets no env vars at all. Slack messages say which env vars a command got, and warn when
it ran with --write.
…heir env vars

The mock rack reads the env wrapper back (cmdenv.Parse) and prints the command and the env vars it gets. A process
start or exec the gateway did not wrap fails, so the CLI E2E tests catch a command reaching the rack with every
env var.
… --write

exec and run send what the flags ask for in the X-Command-Env, X-Command-All-Env-Vars
and X-Command-Write headers, which the gateway reads to decide the command's env vars.
The CLI checks the names and the command (cmdenv.CheckCommand) before anything is sent.

The PIN dialog lists the env vars the command gets, read from the app's settings, and
shows warnings in red: --write gives the real value of a read-only env var, --all-env-vars
gives every env var where the app limits them, and an exec can read its process's
environment. The touch prompt says when the command can change data. The script review
and --dry-run show the same warnings.

The PIN and script dialogs move from AppleScript to a JXA NSAlert (internal/cli/macdialog)
so they can show the warnings with a red icon.
… without the gateway

cmdenv used envutil.ValidName, which pulled the gateway's config and database packages into
everything that imports cmdenv, and broke the mock rack's image (it copies only the packages
it needs). ValidName moves to cmdenv, and envutil and the settings checks call it there.
The mock rack prints the env vars the gateway's wrapper gives each command. The stages check
the default (every env var except RACK_URL), an app's command_env_vars and
command_env_read_only with --env and --write, and the refusal of --env RACK_URL.
Nothing used UpdateProtectedEnvVarsRequest, UpdateApprovedCommandsRequest,
UpdateAppImagePatternsRequest, UpdateAllowDestructiveActionsRequest or
UpdateMFASettingsRequest, and the OpenAPI spec no longer had them. Their generated web schemas
were left over from an older spec.
…ings, and show a command's env vars

Approved deploy commands are objects now, so the card edits them as JSON, with the card the
service image patterns used (now JsonSettingCard). New cards edit command_env_vars (a list)
and command_env_read_only (a JSON object). The audit log shows the env vars each command got,
each read-only one with its source, and says when a command ran with --write.
…1.25 upgrade

The commands guide explains command_env_vars, command_env_read_only, --env, --all-env-vars
and --write, the wrapper and what an exec can still read. The deploy approval, settings,
audit logging, permissions and checklist pages cover the new settings and the approved
command objects, and the upgrade notes say what to change in the same release.
…teway does

The hardening guide covers command_env_vars, command_env_read_only and RACK_URL, which Convox
gives every app process unless the rack's system-env-disable feature gate is on. The upgrade
notes say the old gateway approves no commands between the migration and the promote, and
that run --env KEY=VALUE never set anything.

This branch has not been deployed

No deployments
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