Skip to content

fix(entrypoint): start ssh-agent without writing into the bind-mounted home - #135

Merged
so5 merged 4 commits into
mainfrom
fix/entrypoint-ssh-agent-no-home-pollution
Sep 10, 2026
Merged

so5 merged 4 commits into
mainfrom
fix/entrypoint-ssh-agent-no-home-pollution

Conversation

@so5

@so5 so5 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

背景

entrypoint.sh の ssh-agent 起動は 1cf80702(2025-08-06、HPCI-SS の E2E テスト追加と同時)で入りました。コミットメッセージは簡潔で、当時の意図が追いにくくなっています。追加されたのは次の 4 行です。

SSH_ENV_FILE=/root/ssh_env
ssh-agent -s > ${SSH_ENV_FILE}
source ${SSH_ENV_FILE}
echo "source ${SSH_ENV_FILE}" >> /root/.bashrc

compose.dev.yml および README の docker run 例は、いずれも ${HOME}:/root を bind mount します。この状態でこの entrypoint は、ホストの ~/ssh_env を生成し、コンテナ起動のたびにホストの ~/.bashrcsource /root/ssh_env を 1 行ずつ追記します。macOS はログインシェルが zsh のため気づきにくいものの、~/.bashrc は同様に汚染されます。

調査結果(この仕組みが実際に必要としているもの)

  • WHEEL サーバプロセス(node app/index)が SSH_AUTH_SOCK / SSH_AGENT_PID を持つこと。WHEEL は ssh-client-wrapper 経由で多数の ssh 子プロセスを spawn し、それらがサーバの環境を継承します。~/.ssh/configAddKeysToAgent yes があり対象鍵がパスフレーズ付きの場合、エージェントがあればパスフレーズ入力(ブラウザのプロンプト)が 1 回で済み、ProxyJump チェーンや同一リモート間のファイル配送(-A)でもエージェント転送が効きます。
  • 現行実装は source ${SSH_ENV_FILE} の後に npm start を実行しているため、サーバプロセスへは既にエージェント環境が渡っています
  • >> /root/.bashrc の唯一の目的は、docker exec ... bash の対話シェルが同じエージェントを参照できるようにすることです。通常運転には不要です。

ssh_env / SSH_ENV_FILE を参照する箇所は、テスト・CI を含めてこの 4 行のみでした。

変更内容

entrypoint.sh

該当 4 行を、固定・コンテナローカルなソケット上のエージェント起動に置き換えました。

export SSH_AUTH_SOCK=/tmp/wheel-ssh-agent.sock
rm -f "${SSH_AUTH_SOCK}"
ssh-agent -a "${SSH_AUTH_SOCK}" > /dev/null

exec npm start
  • /tmp は bind mount されないため、ホームディレクトリへは一切書き込みません。
  • 末尾を exec npm start とし、npm start をコンテナの PID 1 にしました(従来は非 exec で bash が PID 1)。

Dockerfile

イメージ内の /etc/bash.bashrc に 1 行追加し、当該ソケットが存在すれば SSH_AUTH_SOCK をエクスポートするようにしました(bind mount 外のためホスト非汚染)。docker exec -it wheel_dev bash(Debian の対話非ログイン bash)はこれを読み込み、同じエージェントに接続します。ソケットが無ければ何もしません。

動作確認(Linux ホスト、compose.dev.yml + ローカル override でビルド・再作成)

  • コンテナ再作成の前後で ~/.bashrc の mtime・SHA256 が不変。~/ssh_env は生成されない。
  • サーバプロセスの環境: SSH_AUTH_SOCK=/tmp/wheel-ssh-agent.sock
  • docker exec -it wheel_dev bash: SSH_AUTH_SOCK が設定され、ssh-add -l が応答。
  • HTTP 200。PID 1 が npm start

補足

  • 変更対象はシェルスクリプトと Dockerfile のため、eslintnpm run lint)は非対象。entrypoint.sh に依存するユニットテストはありません。
  • Issue は起票していません(本文に経緯を記載)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01C3jKNM1qubomM8UTRdkEWu

…d home

The ssh-agent startup added in 1cf8070 writes an env-dump file to /root/ssh_env
and appends `source /root/ssh_env` to /root/.bashrc on every container start.
With compose.dev.yml / the documented `docker run` bind-mounting ${HOME}:/root,
those land in the host user's ~/ssh_env and ~/.bashrc, the latter accumulating a
line per start. (macOS defaults to zsh so it went unnoticed there, but it still
pollutes ~/.bashrc.)

Only the WHEEL server process needs the agent env, and it already gets it in the
current code (the `source` before `npm start`). Replace the file + .bashrc write
with an agent on a fixed, container-local socket:

- entrypoint.sh: `ssh-agent -a /tmp/wheel-ssh-agent.sock`, export SSH_AUTH_SOCK,
  then `exec npm start` (also makes npm the container's PID 1). /tmp is not
  bind-mounted, so nothing is written to the host home.
- Dockerfile: add one line to the image's /etc/bash.bashrc that exports
  SSH_AUTH_SOCK when that socket exists, so `docker exec ... bash` sessions still
  attach to the same agent - the only thing the /root/.bashrc write was for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3jKNM1qubomM8UTRdkEWu
@so5 so5 self-assigned this Sep 10, 2026
@so5
so5 merged commit 3e3e208 into main Sep 10, 2026
so5 added a commit that referenced this pull request Sep 11, 2026
…ime (#136) (#138)

Backport of PR #136 (main) to maintenance2023. This branch's Dockerfile
predates the run_base/dev/versioner-friendly layout on main (it's
builder -> base -> {UT, exec}, node:hydrogen-slim, no separate dev
stage), so the backport is adapted rather than a literal copy:
- versioner is `FROM base AS versioner` (base already has git installed)
  followed by `COPY . .` + `git describe`, same idea as main.
- the version is baked only in the `exec` stage (there is no `dev` stage
  here); paths use this stage's WORKDIR (/usr/src/server, so
  app/db/version.json, not server/app/db/version.json).
- kept "master" branch naming used in this line's run_test.yml /
  build_and_deploy.yml comments (unlike main, not yet renamed here).
- did not add the entrypoint.sh ssh-agent block (PR #135, not backported
  to this branch) or the doc files that don't exist here yet (AGENTS.md,
  TEST_GUIDE.md, documentMD/design/{developer_guide,testing}.md).

The `[skip ci] update version number` step in run_test.yml rewrote
server/app/db/version.json and pushed it to every feature branch, so
parallel PRs conflicted on that file and a stale `-beta` value rode onto
main/maintenance at merge (issue #1014).

- run_test.yml: drop the version-stamp / commit / push steps.
- version.json: the committed value is a fixed placeholder
  ("not defined - this is a development build"). Nothing in CI edits the
  tracked file, so it never diverges between branches -> no more conflicts.
- build_and_deploy.yml: pass the computed version through the
  WHEEL_VERSION build-arg instead of editing the worktree; repack
  source.tar.gz from a clean tree with the real version dropped in
  (git archive shipped the placeholder before).

versionInfo.js is unchanged - it still statically imports version.json.

(cherry picked from commit 931f0ae)


Claude-Session: https://claude.ai/code/session_01C3jKNM1qubomM8UTRdkEWu

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
so5 added a commit that referenced this pull request Sep 11, 2026
…ime (#136) (#137)

Backport of PR #136 (main) to maintenance2026. Adjusted for this branch:
- kept "master" branch naming used in this line's run_test.yml /
  build_and_deploy.yml comments (unlike main, not yet renamed here)
- kept `RUN npm prune --production` in the exec stage as-is (this branch
  still prunes; main's "don't prune" comment doesn't apply here)
- did not add the entrypoint.sh ssh-agent block (PR #135, not backported
  to this branch) or the doc files that don't exist here yet (AGENTS.md,
  TEST_GUIDE.md, documentMD/design/{developer_guide,testing}.md)

The `[skip ci] update version number` step in run_test.yml rewrote
server/app/db/version.json and pushed it to every feature branch, so
parallel PRs conflicted on that file and a stale `-beta` value rode onto
main/maintenance at merge (issue #1014).

- run_test.yml: drop the version-stamp / commit / push steps and the
  now-unused GITHUB_TOKEN from the test job. CI no longer touches the file.
- version.json: the committed value is a fixed placeholder
  ("not defined - this is a development build"). Nothing in CI edits the
  tracked file, so it never diverges between branches -> no more conflicts.
- build_and_deploy.yml: pass the computed version through the
  WHEEL_VERSION build-arg instead of editing the worktree; repack
  source.tar.gz from a clean tree with the real version dropped in
  (git archive shipped the placeholder before).
- Dockerfile: a throwaway `versioner` stage runs `git describe` on the
  build context, so a plain `docker build .` still bakes a
  commit-identifying version (`<describe>-local`). `--build-arg
  WHEEL_VERSION` overrides it; an un-stamped source build keeps whatever
  version.json ships. No --dirty: .dockerignore hides tracked paths
  (documentMD/) from the stage's `COPY . .`, so --dirty would always fire.

versionInfo.js is unchanged - it still statically imports version.json.

(cherry picked from commit 931f0ae)


Claude-Session: https://claude.ai/code/session_01C3jKNM1qubomM8UTRdkEWu

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

2 participants