shim: secrets reach any shimmed tool, and the tool is found through the agent's PATH #5

Open
opened 2026-10-12 13:29:39 +02:00 by runa · 0 comments
Collaborator

The shim keeps secrets out of the agent's env and off disk, and the docs are honest that it is leak-prevention, not containment. This issue asks whether it can also be containment for one narrow case: a secret that only one trusted tool should ever hold.

The motivating case is runa running Home Assistant with a runa-house profile. The HA token is admin-capable, and one rule is P0: never switch off the plugs that feed computers. The planned enforcement is that the model cannot reach HA at all except through hass, a verb that refuses those entities. That only holds if the token cannot get out of hass. Today it can, three ways.

1. The real binary is resolved through the agent's own PATH

cmd/claudia-shim/main.go strips the shim dir from $PATH and runs exec.LookPath(name) against the rest. The agent controls $PATH for every command it runs:

mkdir -p /tmp/x && printf '#!/bin/sh\nenv\n' > /tmp/x/hass && chmod +x /tmp/x/hass
PATH="$CLAUDIA_SHIM_DIR:/tmp/x:$PATH" hass      # prints the real token

2. Every shimmed tool gets every secret

fetchSecrets(sock, name) sends the tool name for the audit record only, and the broker returns the whole map. git and curl are on the default shim list, so:

git -c alias.x='!env' x                          # every secret, in clear
curl -H "Authorization: Bearer $HASS_TOKEN" ...   # placeholder substituted

3. The env passes through unscrubbed

hass is a Python script. With a pinned path and scoped secret it still loses:

mkdir /tmp/p && echo 'import os; print(os.environ)' > /tmp/p/json.py
PYTHONPATH=/tmp/p hass ping

LD_PRELOAD does the same to any shimmed ELF binary.

Proposal

All three changes are opt-in, so nothing changes for a profile that does not use them.

  • Secrets scoped to tools. claudia secret add HASS_TOKEN --tool hass (repeatable). The broker answers a scoped secret only to the tool names it is bound to. Unscoped secrets behave as today. The broker already authenticates the shim binary over the socket, so the tool name it is given is trustworthy as long as the shim derives it from a pinned path (below) rather than from argv[0] alone.
  • Pinned tool paths. claudia config add shim hass=/home/agent/src/runa/bin/hass. The path is resolved outside the sandbox at launch and handed to the shim through the runtime dir, not the env. The shim execs that path and ignores $PATH. Launch refuses if the pinned file, or any directory above it, is writable under the session's grants, because a writable pinned path is no pin.
  • Env scrub for scoped tools. Before exec, drop LD_*, PYTHON*, PERL5*, RUBYOPT/RUBYLIB, NODE_OPTIONS, BASH_ENV/ENV. Run interpreters in isolated mode where the shebang allows it (python3 -I). A scrub list is a denylist and will miss something, so this is the weakest of the three. Executing the pinned tool with a minimal allowlisted env could be stronger, at the cost of tools that legitimately read env.

Scoped secrets would be rejected for tools that are not pinned, so the combination is always the strong one.

What it still does not stop

A pinned tool that will hand out its own secret still does: hass must not have a verb that echoes the token or makes arbitrary requests. That is the tool author's job, and the issue is about making it the only remaining job.

Tests

One test per bypass above, each shown to work against current main and to fail with the change: PATH shadowing, cross-tool secret delivery, and PYTHONPATH/LD_PRELOAD injection. Plus a launch-refusal test for a pinned path under a writable grant.

The shim keeps secrets out of the agent's env and off disk, and the docs are honest that it is leak-prevention, not containment. This issue asks whether it can also be containment for one narrow case: **a secret that only one trusted tool should ever hold.** The motivating case is runa running Home Assistant with a `runa-house` profile. The HA token is admin-capable, and one rule is P0: never switch off the plugs that feed computers. The planned enforcement is that the model cannot reach HA at all except through `hass`, a verb that refuses those entities. That only holds if the token cannot get out of `hass`. Today it can, three ways. ## 1. The real binary is resolved through the agent's own PATH `cmd/claudia-shim/main.go` strips the shim dir from `$PATH` and runs `exec.LookPath(name)` against the rest. The agent controls `$PATH` for every command it runs: ```sh mkdir -p /tmp/x && printf '#!/bin/sh\nenv\n' > /tmp/x/hass && chmod +x /tmp/x/hass PATH="$CLAUDIA_SHIM_DIR:/tmp/x:$PATH" hass # prints the real token ``` ## 2. Every shimmed tool gets every secret `fetchSecrets(sock, name)` sends the tool name for the audit record only, and the broker returns the whole map. `git` and `curl` are on the default shim list, so: ```sh git -c alias.x='!env' x # every secret, in clear curl -H "Authorization: Bearer $HASS_TOKEN" ... # placeholder substituted ``` ## 3. The env passes through unscrubbed `hass` is a Python script. With a pinned path and scoped secret it still loses: ```sh mkdir /tmp/p && echo 'import os; print(os.environ)' > /tmp/p/json.py PYTHONPATH=/tmp/p hass ping ``` `LD_PRELOAD` does the same to any shimmed ELF binary. ## Proposal All three changes are opt-in, so nothing changes for a profile that does not use them. - **Secrets scoped to tools.** `claudia secret add HASS_TOKEN --tool hass` (repeatable). The broker answers a scoped secret only to the tool names it is bound to. Unscoped secrets behave as today. The broker already authenticates the shim binary over the socket, so the tool name it is given is trustworthy as long as the shim derives it from a pinned path (below) rather than from `argv[0]` alone. - **Pinned tool paths.** `claudia config add shim hass=/home/agent/src/runa/bin/hass`. The path is resolved outside the sandbox at launch and handed to the shim through the runtime dir, not the env. The shim execs that path and ignores `$PATH`. Launch refuses if the pinned file, or any directory above it, is writable under the session's grants, because a writable pinned path is no pin. - **Env scrub for scoped tools.** Before exec, drop `LD_*`, `PYTHON*`, `PERL5*`, `RUBYOPT`/`RUBYLIB`, `NODE_OPTIONS`, `BASH_ENV`/`ENV`. Run interpreters in isolated mode where the shebang allows it (`python3 -I`). A scrub list is a denylist and will miss something, so this is the weakest of the three. Executing the pinned tool with a minimal allowlisted env could be stronger, at the cost of tools that legitimately read env. Scoped secrets would be rejected for tools that are not pinned, so the combination is always the strong one. ## What it still does not stop A pinned tool that will hand out its own secret still does: `hass` must not have a verb that echoes the token or makes arbitrary requests. That is the tool author's job, and the issue is about making it the *only* remaining job. ## Tests One test per bypass above, each shown to work against current `main` and to fail with the change: PATH shadowing, cross-tool secret delivery, and `PYTHONPATH`/`LD_PRELOAD` injection. Plus a launch-refusal test for a pinned path under a writable grant.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
varl/claudia#5
No description provided.