Stop a Bot's shell reading the deployment's keys from a neighbouring process - #552
Merged
davidmckayv merged 2 commits intoSep 15, 2026
Merged
Conversation
zopeVaibhav
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso,
mxmzb and
tylerslaton
as code owners
September 15, 2026 11:59
davidmckayv
approved these changes
Sep 15, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Code-verified clean; CI green on this sha.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 aspwuser— and Linux lets aprocess read another same-user process's
/proc/<pid>/environ. The shell's own environment isscrubbed (
agent-computer/src/shell.ts), but/proc/$PPID/environ(the browser) and/proc/<api-pid>/environare not, andwith-contenvhands both of those processes the wholedeployment environment. So one allowed command —
tr '\0' '\n' < /proc/$PPID/environ— returnsKEY_ENCRYPTION_KEY,BETTER_AUTH_SECRET,INTELLIGENCE_API_KEYand the scramDATABASE_URL, withno 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:
apiuser(docker/s6/s6-rc.d/api/run,docker/s6/scripts/migrate.sh,useraddinDockerfile). The shell stayspwuser, so the kernelnow refuses its read of
/proc/<api-pid>/environ. This is what closes the path toKEY_ENCRYPTION_KEY, the session secret and the database password.docker/s6/s6-rc.d/computer/run).with-contenvstill brings the whole environment in as root, but the browser is then dropped withonly the variables it reads: a fixed set, the
LC_*andEGRESS_PROXY_*families, and whateverCOMPUTER_SHELL_ENVnames for the shell. The deployment's keys never enter the process the shellcan read as its parent.
/run/s6/container_environmentfiles are closed to the shell (chmod 0700indocker/s6/scripts/computer-token.sh, which runs as root before the browser starts). s6 writes theenvironment there as files, readable by a plain
cat; awith-contenvservice still reads thedirectory as root, the shell (
pwuser) can no longer enter it.One thing deliberately unchanged: the browser still holds
COMPUTER_TOKEN, and the shell can stillread it. The browser needs it to authenticate to its own
:4100, and in the all-in-one there is onecomputer 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.
service runs as, the same on every replica. No shared state is involved.
computer-tokengains achmod; no new service.Boundary and audit
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.
Changelog
Unreleased: "A Bot's shell can no longer read the deployment's keys from aneighbouring process".
Proof
Honest about what could and could not run here.
Ran, and passed:
computer/runallow-list logic, executed against a seeded environment (four secrets + the twofamilies + a
COMPUTER_SHELL_ENVpassthrough):KEY_ENCRYPTION_KEY,BETTER_AUTH_SECRET,DATABASE_URL,INTELLIGENCE_API_KEYare dropped;COMPUTER_TOKEN, proxies,LC_*,EGRESS_PROXY_*and the operator-named variable are kept./proc/<pid>/environgets the secrets; a different-uid process getsEACCES;environis-r--------, so a shared group (needed for/workspace) does not reopen it.sh -non every edited script.so running it as
apiuseron a read-only/appis safe by inspection.Added as the standing check: a step in the
imageCI job asserts apwusershell can readKEY_ENCRYPTION_KEYneither from any sibling/proc/<pid>/environnor from/run/s6/container_environment. It fails onmain(API ispwuser) 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
imagejob rather than on a local run: thatapiuserboots the API ("boots andserves"), that the narrowed environment boots the browser ("supervision stable"), and that the
chmod 0700does not disturb boot (same). Each fails that job loudly if wrong. A reviewer with abuild runs
docker build .then the CI step's twodocker execprobes to see red-on-main,green-here directly.