Deliver sidecar secrets as environment without exposing them#14
Merged
Conversation
The credential tongs we want to run are off-the-shelf servers that read their
secrets from the process environment at startup, so they must work unmodified.
A resolved secret still must never reach a tong as a docker -e value (anything
holding the docker socket could read it back via docker inspect), a command
argument, or a file on disk.
So a tong whose env references ${secret:...} is now started behind a /bin/sh
wrapper: the launcher creates a host FIFO, bind-mounts it read-only into the
tong, and overrides the entrypoint with a wrapper that reads the FIFO, exports
each NAME=value into the environment, then execs the image's real
entrypoint+command (read via docker inspect, or declared on the tong as
entrypoint:/command:). The read and eval are guarded (secret_env=$(cat ...) ||
exit 1; eval "$secret_env" || exit 1) so the real process never starts if
delivery fails -- a bare eval "$(cat ...)" would succeed on a failed or empty
read and run the target without its secrets. The launcher resolves the secret
env and writes export lines into the FIFO once the wrapper has opened the read
end. The values arrive as ordinary environment variables -- set before the real
process starts, since the wrapper blocks on the FIFO until delivery -- while the
bytes only ever live in the kernel pipe buffer, never an -e value, an argv, or
disk.
The write end is opened non-blocking and times out if no reader attaches, so a
tong that never starts does not hang the launch; the payload is written in a
loop so a value larger than the pipe buffer is delivered whole rather than
truncated, and a reader that closes early fails the launch rather than silently
dropping bytes. A delivery that fails (docker error or Ctrl-C) removes the
just-started container before re-raising, so a half-configured shared tong is
not cached and reused while missing its secret, and the FIFO is always cleaned
up. A tong with secret env needs a /bin/sh in its image; one without secrets
runs its image entrypoint unchanged.
Plain (non-secret) env keeps flowing through -e. mcp and volume interfaces, and
a shared tong that mounts the workspace, are still refused.
run_opencode / run_claude now hand the launcher the user-layer
secret-providers.yaml path so a tong's ${secret:...} references resolve
through the configured provider CLIs. The path is a launcher option consumed
before the anvil command, so with no tongs discovered the file is never read
and the anvil argv is forwarded unchanged.
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.
Summary
A sidecar definition can hold credentials in its
env:as${secret:<provider>:<ref>}references. This teaches the host launcher(
scripts/run_anvil.py) to resolve those references and hand the resolved valuesto the sidecar as ordinary environment variables -- so an off-the-shelf server
that reads them from its environment at startup works unmodified -- without ever
exposing them to the harness, to
docker inspect, or to disk.What this does
declared in the user-layer table (
--providers, defaulting to~/.swarmforge/secret-providers.yaml), so interactive unlocks happen in theuser's terminal before the harness starts. Plain (non-secret) env keeps flowing
through
-e.-e. A resolved secret must never bea docker
-evalue (anything holding the docker socket could read it back viadocker inspect), a command-line argument, or a file on disk. The launchercreates a host FIFO, bind-mounts it read-only into the sidecar, and overrides
the entrypoint with a
/bin/shwrapper that reads the FIFO, exports eachNAME=valueinto its environment, then execs the image's realentrypoint+command (read via
docker inspect, or declared on the sidecar asentrypoint:/command:). The read and eval are guarded, so the real processnever starts if delivery fails rather than running without its secrets. The
wrapper blocks on the read, so the env is set before the real process
starts -- no startup race -- and the secret bytes only ever live in the kernel
pipe buffer.
attaches; the payload is written in a loop so a value larger than the pipe
buffer is delivered whole rather than truncated; a reader that closes early
fails the launch. A delivery that fails (docker error or Ctrl-C) removes the
just-started container before raising, so a long-lived sidecar is not cached and
reused while missing its secret, and the FIFO is always cleaned up.
run_opencode/run_claudepass the provider-table path to thelauncher.