Skip to content

#4033 以降に claude / codex から commit できなくなったので修正をしましょう #4059

Closed

Description

@KATO-Hiro

WHAT

Activity

  1. added theissue type on Sep 18, 2026
  2. KATO-Hiro commented on Sep 19, 2026

    @KATO-Hiro
    CollaboratorAuthor

    Plan: devcontainer からsecretsを排除し、入れ子のサンドボックスを撤去する

    経緯(深刻だったもの)

    #4034 で導入したエージェントのサンドボックスの構成で、次の問題が分かった。

    1. エージェントのコマンドが全く動かなくなった(2026-09-18)
      • Debian のセキュリティ更新で bubblewrap が 0.12.0 になり、setuid での起動が廃止された。[4] version を固定していなかったため、コンテナを作った日によって動作が不安定に。
      • 直そうとすると、コンテナのセキュリティ制限を次々に緩める必要があった(コンテナの中でさらにサンドボックスを動かす「入れ子」は、そもそも相性が悪い)。
    2. サンドボックスがあっても、秘密は守れていなかった
      • 本番の秘密 CONFIRM_API_URL は、compose がコンテナの環境変数として必須で注入していた。printenv を実行すれば誰でも読め、サンドボックスでは防げていなかった。
      • VS Code が GitHub のトークンをコンテナに共有しており、git credential fill で取り出せた。
    3. エージェントが何でもできる権限を持っていた
      • ベースイメージが node ユーザーにパスワードなしの sudo(管理者権限)を与えていた。エージェントも設定を書き換えたり、ファイアウォールを外したりできた。

    方針: コンテナに秘密を置かず、外向きの通信は許可リストで絞る

    コンテナ全体を境界とし、コンテナ内ではエージェントのサンドボックスを使わない。

    本番の値はローカルに置かない

    • CONFIRM_API_URL(AtCoder アカウント連携の確認にだけ使う)は、既定でコンテナに入れない。連携確認のボタンが失敗するだけで、ほかの機能には影響しない。
    • 連携済みユーザーは、シード(pnpm db:seed)で作る(admin、guest)。本物の値での確認が必要なときだけ、エージェントを使わずに行う。
    • 値そのものは変更しない。返すのは AtCoder の公開情報で、本人確認は URL の秘匿に依存しないため。大量アクセスへの対策は、エンドポイント側で別途行う。

    エージェントのサンドボックスはコンテナ内だけ無効にする

    • プロジェクトの設定は、コンテナを使わないホストでも効くため残す。
    • bubblewrap のパッケージを外すだけでは不十分だった。Codex は自前の bwrap を同梱しており、それを使うため。

    コンテナに残る秘密は、エージェント自身のログイン情報だけ

    • エージェントの push はルールで禁止し、staging / main はブランチ保護で守る。人間の push の手間は増やさない。ルールだけではエージェントによる push を技術的には防げない。
    • Claude と Codex のログイン情報はコンテナに残り、エージェントのコマンドから読める。この残余リスクを受け入れ、送信先を絞る。

    外部通信の許可リスト

    • Anthropic の公式リファレンスの init-firewall.sh を元にし、コンテナの起動のたびに適用する。[1]
    • 各ツールの任意機能の宛先 [2] と、api.openai.com(Codex を API キーで使うときだけ必要)[5] は入れない。
    • テレメトリは、各ツールの設定でも止める(遮断だけだと再送で遅くなる)。
    • CDN の IP 変更で許可先につながらなくなった場合は Rebuild する。稼働中の init-firewall.sh 再実行は、既存の OUTPUT DROP 規則を消した後に GitHub から IP 一覧を取得できず、通信不能になるため行わない。
    • 受け入れた制約:
      • Claude の Remote Control が使えない(テレメトリを止める設定が、その機能も止めるため)。[6]
      • Codex の利用状況の送信先は推論と同じ chatgpt.com で、ファイアウォールでは止められない。設定でだけ止めている。[3][7]

    却下した代替案

    案 却下理由
    エージェントを各自の OS で直接動かす Windows は Claude のサンドボックスに非対応。OS ごとに挙動が分かれる
    秘密管理ツール(dotenvx、Infisical など) 秘密 1 つには過剰。エージェントからも取得できてしまう
    push はホストから行う 作業環境が二つに分かれ、手間が増える

    Rebuild 後の確認(2026-09-19)

    ユーザーの確認結果:

    • curl による許可先・非許可先への接続と sudo -n true は、それぞれ期待どおりに成功・失敗した。
    • VS Code の拡張と Claude / Codex の CLI・拡張で会話とコマンドを実行できた。
    • pnpm install と pnpm test:e2e は成功した。

    実際の git commit / git push は、このレビュー前の確認項目から外した。coderabbit review --plain も今回の動作確認には含めない。

    残るリスク

    • Claude / Codex のログイン情報はエージェントのコマンドから読める。許可した宛先や DNS の問い合わせを経由した持ち出しは防げない。DNS の宛先だけを絞っても、このリスクは解消しない。
    • ファイアウォールは IP アドレスで判定するため、同じ IP を共有する他のサイトにも届く。
    • コンテナ作成直後のセットアップと、VS Code を使わない docker compose up では、ファイアウォールが効かない。この起動時の例外は、ローカル開発の運用として受け入れる。
    • ホストに CONFIRM_API_URL が残っていると再注入される(警告は出る)。GitHub のトークン共有の停止も各自のホスト設定に依存する。新しいメンバーにも確認を依頼する。

    教訓

    • セキュリティの設定を外すときは、それが裏で何を回避していたかを先に洗い出す。
    • エラーが連鎖しそうなら、既知の上流 issue をまとめて確認し、方針を 1 回で決める。
    • 守るもの(秘密とログイン情報)を先に固定し、それを削る案は候補にしない。

    出典

    1. Claude Code reference devcontainer: https://github.com/anthropics/claude-code/tree/main/.devcontainer
    2. Claude Code: Network access requirements: https://code.claude.com/docs/en/network-config
    3. Codex: Configuration reference: https://learn.chatgpt.com/docs/config-file/config-reference
    4. Debian: DSA-6472-1: https://security-tracker.debian.org/tracker/DSA-6472-1
    5. Codex: Authentication: https://developers.openai.com/codex/auth
    6. Claude Code: Data usage: https://code.claude.com/docs/en/data-usage
    7. openai/codex Discussion #8291(Client Analytics): Codex Client Analytics openai/codex#8291
  3. KATO-Hiro commented on Sep 19, 2026

    @KATO-Hiro
    CollaboratorAuthor

    追加

    CodeRabbit のレビューを受けて、DNS の許可を Docker の組み込み DNS(127.0.0.11)宛てに限定し、Claude の mount 元を ~/.claude-devcontainer/AtCoderNoviSteps に分離した。
    ホストの ~/.claude の memory・会話履歴は引き継がない。

  4. KATO-Hiro commented on Sep 19, 2026

    @KATO-Hiro
    CollaboratorAuthor

    ファイアウォール再実行の安全化

    概要

    devcontainer の起動待ちが長時間続き、コンテナ破棄後は起動できた。
    確認できた欠陥は、再実行時の iptables -F が許可規則を消して OUTPUT DROP だけを残し、GitHub メタデータ取得を妨げることである。ログがなく、実際の停止箇所は未特定。
    専用チェーン・一時 ipset の swap・全外部取得への期限・双方向の自己検証を導入した。

    判断

    • 稼働中の規則は、新しい宛先の取得と検証が終わるまで保持する。Docker の DNS を壊さないよう NAT と組み込みチェーンの flush には触れない。
    • このスクリプトは誤っていても症状が出ない(許可リストが無効でも通信は成功する)ため、起動時の自己検証で「許可先に届く」と「未許可先が塞がる」の両方を確かめる。片方だけでは全遮断でも合格する。
    • stub による挙動テストは全廃した。自己検証まで stub が答えるため、ファイアウォール無効化・全遮断・DNS 断・IPv6 素通し・freeze 再発の 5 点をどれも検出しなかった。実機で現れない「全 curl/dig に期限がある」ことだけを静的に検査する。
    • 個人環境で Rebuild により回復できるため、恒久的に壊れる経路だけを潰す。途中失敗で以後毎回 "Chain already exists" になる問題は -N || -F で直し、Rebuild で戻る不具合(host_network の固定、ジャンプ規則の重複、IPv4 ジャンプ追加後・IPv6 ジャンプ追加前の中断で再実行が完了済みと判定し IPv6 が素通しになること)は見送る。最後の件は該当区間が ip6tables 成功後の -I/-P だけで発生率が低い。コンテナ再起動で netns が作り直され規則も消えると想定しているが未検証。
    • 未許可先の検証は curl の終了コード 7(接続失敗)のみを遮断の証拠とする。DNS・TLS・タイムアウトによる失敗は遮断を証明しないため検証失敗とする。

    却下

    • 取得前に OUTPUT ACCEPT にする。一時的に許可範囲が広がる。
    • waitFor: postStartCommand を外す。設定失敗を隠す。
    • 別言語での書き直しや iptables-restore による宣言化。書き直し量に対し、要件(実態が分かること)に効かない。
    • 初回設定の fail-closed 化。2 経路を抱える複雑さが Rebuild で回復できる状況に見合わない。

    補足: チェーン

    NOVISTEPS_* は変数ではなく iptables のチェーン名である。チェーンは上から照合される規則のリストで、-N でカーネル内に作られスクリプト終了後も残る。組み込みの INPUT/OUTPUT からのジャンプで呼ばれる。
    中身はホスト側から docker compose exec -u root web iptables -L NOVISTEPS_OUTPUT -n -v --line-numbers で確認できる(node の sudo は init-firewall.sh のみ許可)。

    未解決

    • 反映には Rebuild が必要(image に焼き込んだコピーを sudo 実行する構成は、agent による改変を防ぐため維持)。
    • オフライン時や probe 先の障害時は自己検証が失敗する。
    • コンテナ起動から postStartCommand までの窓は無防備のまま。
    • コンテナ再起動で netns が作り直され規則も消える、という想定は未検証のまま(下記「残る任意確認」)。

    Rebuild 後の確認(2026-09-19 実施・合格)

    • 起動ログ: freeze せず、Firewall check OK 4 行と Firewall configured が出る。
    • コンテナ内:
      • diff /usr/local/bin/init-firewall.sh .devcontainer/init-firewall.sh が差分なし。
      • curl -sS -o /dev/null --max-time 8 https://example.com; echo $? が 7。
      • sudo /usr/local/bin/init-firewall.sh の再実行が Firewall configured まで進む。
      • ip -6 route にデフォルト経路がなければ、IPv6 素通しの影響は実質ない。
    • ホスト側(docker compose exec -u root web ...):
      • iptables -S OUTPUT に -P OUTPUT DROP と、再実行後も 1 行だけの -j NOVISTEPS_OUTPUT。
      • ip6tables -S OUTPUT に -P OUTPUT DROP と -j NOVISTEPS_IPV6。

    残る任意確認

    • iptables -N NETNS_MARKER 後にコンテナを再起動し、iptables -L NETNS_MARKER が失敗すれば netns 再作成の想定を確認済みとして「判断」の「未検証」を外す。残っていれば IPv6 素通しの見送り判断を見直す。
  5. KATO-Hiro commented on Sep 20, 2026

    @KATO-Hiro
    CollaboratorAuthor

    Devcontainer ファイアウォールの許可範囲と再実行の修正

    概要

    Compose の web から db:5432 への通信を残し、Docker ブリッジ全体への許可を削除する。IPv4 と IPv6 の設定状態を確認し、途中で止まった初期化を再実行できるようにする。

    設計

    ファイアウォール設定は .devcontainer/init-firewall.sh の単一責務とする。既存のユーティリティやサービスに同等の処理はない。Docker DNS で db の IPv4 アドレスを取得し、TCP 5432 だけ許可する。公開ポート 5173 と 5555 への着信のみ許可する。初期化済み判定は設けず、再実行時も両系統のチェーンを作り直して欠落を補う。再構築前に DROP ポリシーを設定し、途中失敗で許可的な IPv6 OUTPUT を残さない。

    完全な iptables-save / restore による状態保存は、ローカル開発コンテナには複雑すぎるため採用しない。ホスト側の宛先を許可する必要は Compose の通信契約から確認できないため採用しない。

    フェーズ

    1. テスト層: init-firewall.sh の許可範囲と再実行条件を検証するテストを先に追加する。
    2. 設定層: DB 宛て、着信ポート、IPv4/IPv6 の再実行処理を修正する。
    3. 文書: CONTRIBUTING.md の通信範囲、Rebuild、公開ポートの説明を現行設定に合わせる。
    4. 検証: pnpm test:unit、変更ファイルの Prettier、pnpm lint、pnpm check、git diff --check を実行する。

    Rebuild 後の確認

    以下のコマンドはホスト側のリポジトリで実行する。web と db が起動してから確認する。
    全項目を 2026-09-20 に確認した。手順 1・2 の再実行・5 の DB 接続・6 は web コンテナ内(node ユーザー)から、手順 2 の postStartCommand・3・4・5 のホストからの 5173 番ポートはホスト側から確認した。
    node ユーザーの sudo は /usr/local/bin/init-firewall.sh だけが NOPASSWD で許可されており、iptables の参照はできないため、残りはホスト側で実行する。

    1. docker compose exec web cmp -s .devcontainer/init-firewall.sh /usr/local/bin/init-firewall.sh が成功すること。イメージ内の実行ファイルが修正版であることを確かめる。
    2. Dev Container の postStartCommand が Firewall configured で終了すること。
      [x] 続けて docker compose exec web sudo /usr/local/bin/init-firewall.sh をもう一度実行し、再実行でも成功することを確かめる(自己チェック 4 件が通り Firewall configured で終了)。
    3. docker compose exec -u root web iptables -S NOVISTEPS_OUTPUT で、db の IPv4 アドレス宛て TCP 5432 の許可があり、Docker サブネット全体またはホスト側への無条件の許可がないことを確かめる。docker compose exec -u root web iptables -S NOVISTEPS_INPUT では、着信の許可が公開ポート 5173 と 5555、および既存接続とループバックに限られることを確かめる。
    4. docker compose exec -u root web iptables -S OUTPUT と docker compose exec -u root web ip6tables -S OUTPUT で、それぞれの NOVISTEPS_* チェーンへのジャンプが1つずつあり、両方の OUTPUT ポリシーが DROP であることを確かめる。再実行後もジャンプが増えないことを確認する。手順 2 の再実行を済ませてあるので、ここでジャンプが重複していなければ冪等性の確認を兼ねる。
    5. docker compose exec web node -e "const socket = require('node:net').connect(5432, 'db'); socket.on('connect', () => { console.log('DB TCP OK'); socket.destroy(); }); socket.on('error', (error) => { console.error(error); process.exitCode = 1; });" が DB TCP OK と表示することを確かめる(手順 2 の再実行の前後どちらも接続できた)。
      [x] アプリを起動している場合はホストから 5173 番ポートにアクセスできることも確認する。
    6. docker compose exec web curl -I --connect-timeout 5 --max-time 8 https://api.github.com が通信でき、docker compose exec web curl -sS --connect-timeout 5 --max-time 8 https://example.com は接続拒否で終了することを確かめる。これは初期化スクリプト自身の許可先・遮断先チェックの再確認である。
  6. added a commit that references this issue on Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions