Skip to content

Stop a Bot's shell reading the deployment's keys from a neighbouring process - #552

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/shell-proc-environ-leak
Sep 15, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/shell-proc-environ-leak

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

Closes #551.

What this changes

In the all-in-one image, the API server and the Bot's browser both run as pwuser. A Bot's shell
(computer_run_command) is a child of the browser, so it also runs as pwuser — and Linux lets a
process read another same-user process's /proc/<pid>/environ. The shell's own environment is
scrubbed (agent-computer/src/shell.ts), but /proc/$PPID/environ (the browser) and
/proc/<api-pid>/environ are not, and with-contenv hands both of those processes the whole
deployment environment. So one allowed command — tr '\0' '\n' < /proc/$PPID/environ — returns
KEY_ENCRYPTION_KEY, BETTER_AUTH_SECRET, INTELLIGENCE_API_KEY and the scram DATABASE_URL, with
no privilege escalation and nothing on the audit trail but the command text. This walks around the
environment scrub of #66 and the scram change of #226, both of which closed the shell's own
environment and neither of which closed a sibling's. Full write-up in the linked issue.

Three changes, each closing one door:

  • The API and migrations move to their own account, apiuser (docker/s6/s6-rc.d/api/run,
    docker/s6/scripts/migrate.sh, useradd in Dockerfile). The shell stays pwuser, so the kernel
    now refuses its read of /proc/<api-pid>/environ. This is what closes the path to
    KEY_ENCRYPTION_KEY, the session secret and the database password.
  • The browser is handed an allow-list environment (docker/s6/s6-rc.d/computer/run).
    with-contenv still brings the whole environment in as root, but the browser is then dropped with
    only the variables it reads: a fixed set, the LC_* and EGRESS_PROXY_* families, and whatever
    COMPUTER_SHELL_ENV names for the shell. The deployment's keys never enter the process the shell
    can read as its parent.
  • The /run/s6/container_environment files are closed to the shell (chmod 0700 in
    docker/s6/scripts/computer-token.sh, which runs as root before the browser starts). s6 writes the
    environment there as files, readable by a plain cat; a with-contenv service still reads the
    directory as root, the shell (pwuser) can no longer enter it.

One thing deliberately unchanged: the browser still holds COMPUTER_TOKEN, and the shell can still
read it. The browser needs it to authenticate to its own :4100, and in the all-in-one there is one
computer per deployment, so this grants the shell nothing it does not already have over its own
computer. It is not a deployment secret.

Where it runs

This is a container-user change, not a request-path one. No new state, no new listener, no fan-out.

  • New state that outlives a request? None.
  • What happens on the second replica? Identical: the change is to which OS account each s6
    service runs as, the same on every replica. No shared state is involved.
  • Anything serialised? N/A — nothing is written.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No. computer-token gains a chmod; no new service.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Unchanged —
    this touches no acting path. It removes a non-acting path (reading secrets out of /proc)
    that produced no audit row, which is part of why the leak was invisible.
  • New refusals and new failures each write a row. N/A — no new refusals.
  • Nothing new is trusted from the client that the server can resolve itself. Unchanged.

Changelog

  • Added under Unreleased: "A Bot's shell can no longer read the deployment's keys from a
    neighbouring process".

Proof

Honest about what could and could not run here.

Ran, and passed:

  • The computer/run allow-list logic, executed against a seeded environment (four secrets + the two
    families + a COMPUTER_SHELL_ENV passthrough): KEY_ENCRYPTION_KEY, BETTER_AUTH_SECRET,
    DATABASE_URL, INTELLIGENCE_API_KEY are dropped; COMPUTER_TOKEN, proxies, LC_*,
    EGRESS_PROXY_* and the operator-named variable are kept.
  • The mechanism, under identical uid conditions: a process reading a same-uid sibling's
    /proc/<pid>/environ gets the secrets; a different-uid process gets EACCES; environ is
    -r--------, so a shared group (needed for /workspace) does not reopen it.
  • sh -n on every edited script.
  • The API writes nothing to local disk (only Postgres, and file tools it forwards to the computer),
    so running it as apiuser on a read-only /app is safe by inspection.

Added as the standing check: a step in the image CI job asserts a pwuser shell can read
KEY_ENCRYPTION_KEY neither from any sibling /proc/<pid>/environ nor from
/run/s6/container_environment. It fails on main (API is pwuser) and passes on this branch.

Could not run here: the all-in-one image build is blocked in my environment (the s6-overlay
download times out), so I have not booted the fixed image myself. Three integration facts therefore
rest on the CI image job rather than on a local run: that apiuser boots the API ("boots and
serves"), that the narrowed environment boots the browser ("supervision stable"), and that the
chmod 0700 does not disturb boot (same). Each fails that job loudly if wrong. A reviewer with a
build runs docker build . then the CI step's two docker exec probes to see red-on-main,
green-here directly.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-verified clean; CI green on this sha.

@davidmckayv
davidmckayv merged commit 4d4ed61 into CopilotKit:main Sep 15, 2026
16 checks passed
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.

A Bot's shell reads the deployment's secrets from a sibling process's /proc, whatever the environment scrub hides

2 participants