0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.envは.gitignoreに書いても漏れる:私が実演した3経路

0
Last updated at Posted at 2026-07-14

先に結論を書きます。.env.gitignore に書いてリポジトリから追い出しても、シークレットは3つの別経路で普通に漏れます。私が今回検証したのは、Shell履歴、CI/CDのログ、そしてIDE(VSCode)のワークスペース保存領域の3つです。すべて、.env を git に触らせないつもりで運用している私自身のマシンから、実際に APIキー相当の文字列を拾えました。

「Docker イメージに焼き込まれる」話 — つまり .env をビルド時に COPY してしまう問題 — は以前から言われてきました。もう一段手前、.gitignore だけを頼りにしている運用にも、同じ深さの穴があります。この記事はその実演記録です。

この記事の位置づけ

線を引いておきます。世の中には「.env が Docker イメージに焼き込まれ、誰でも抜き出せる」という有名な話があります。あれはビルドコンテキストと COPY の話でした。この記事はそこには触れません。

扱うのは、Docker とは無関係な、開発者のローカルマシンとCIランナー、そしてエディタです。

  • 経路1:Shell履歴 (~/.zsh_history / ~/.bash_history)
  • 経路2:CIログ (GitHub Actionsのジョブ出力)
  • 経路3:IDEのワークスペース保存領域 (VSCodeの workspaceStorage)

どれも .gitignore の範囲外です。守っているつもりの防御線が、シークレットの実体をどこで踏み外すかを見ていきます。

以下の実演はすべて 自分の環境 で確認したものです。他人のマシンや業務環境で同じ操作をするのは避けてください。grep の対象と検索文字列は、ご自身のダミー環境で置き換えて実行してください。

経路1:Shell履歴に残る export

一番よく使うシークレットの渡し方は、たぶん export です。ローカルで手早く試すとき、私もこう書きます。

export OPENAI_API_KEY="sk-proj-xxxxxxxxxxxxxxxxxxxx"
python run_eval.py

.env ファイルは触っていない、git にも入っていない。これで安心と思いがちですが、実際にはこの1行が丸ごと Shell の履歴ファイルに書き出されます。zsh の私の環境で試すとこうなります。

$ tail -5 ~/.zsh_history
: 1782090011:0;export OPENAI_API_KEY="sk-proj-xxxxxxxxxxxxxxxxxxxx"
: 1782090035:0;python run_eval.py
: 1782090201:0;curl -H "Authorization: Bearer $STRIPE_KEY" https://api.stripe.com/v1/charges
: 1782091188:0;AWS_ACCESS_KEY_ID=AKIA... aws s3 ls
: 1782091502:0;env | grep -i token

秘密の文字列が、日付付きでそのまま残っています。しかもこのファイルはユーザ権限で読めるので、あとでこのマシンにマルウェアが1つ入っただけで、過去に叩いたシークレットが全部持って行かれます。

抽出は grep 1本で足ります。

grep -E "sk-|AKIA|ghp_|Bearer " ~/.zsh_history

このコマンドの怖いところは、正規表現を少し足せば、他人の環境でも Slack トークンやGitHub PAT、AWS のアクセスキーIDまで一気に釣れる点です。私は自分のマシンで試して、想定より多くの当たりが出て青くなりました。

対策の骨子は3つに絞れます。

  1. HISTIGNORE (bash) や zsh の HIST_IGNORE_SPACE を使い、シークレットを含む行を履歴から外す。行頭にスペースを入れると履歴に残らない設定にしておく
  2. .envdirenv1Password CLI (op run --env-file) 経由で読み込み、export を手で打たない
  3. 履歴ファイル自体を定期的に洗う。zsh なら fc -RI で読み直したうえで sensitive 行を消す

2026年時点で、1Password CLI の op run.env を仮想的に注入して子プロセスにだけ渡してくれます。Shell履歴には環境変数の中身が残りません。私はローカル開発をこれに寄せてから、履歴からシークレットが完全に消えました。

経路2:CI/CDのログに写る echoset -x

次はサーバサイドの話です。GitHub Actions のワークフローで、こう書きたくなる瞬間があります。

- name: Debug env
  run: |
    echo "API_KEY=$OPENAI_API_KEY"
    curl -X POST -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/models

GitHub Actions は Secrets を扱う値を出力からマスクしてくれます。ただしマスクは 完全一致 に近い挙動で、ちょっと加工されると崩れます。私が実際にログで漏れを確認した典型パターンはこの3つでした。

  • base64 エンコードしたキーをそのまま echo (元の値と一致しないのでマスクされない)
  • キーを jq で JSON に変換した中間結果を出力 (改行が挟まると片方が素通りする)
  • set -x を有効にしたスクリプト内で、シークレットを引数に取る curl を実行 (+ curl -H 'Authorization: Bearer sk-...' がそのまま出る)

再現手順は誰でも組めます。ダミーのワークフローを一度走らせて、生ログを保存された成果物 (Artifact) からダウンロードすれば、ワーカーが吐いた行が全部読めます。GitHub の Web UI 上ではマスクされた *** に見えても、Artifact 側のログにはマスクが効いていないケースが実在します。

対策は「シークレットを stdout に流さない」を徹底することに尽きます。

  • set -x を使うスクリプトでは、シークレットを含む変数を先に別変数に退避してから set +x の区間に隠す
  • curl-H はテンプレート文字列で組み立てず、--config ファイル経由で渡す
  • CI 上で envprintenv を吐くデバッグを PR のまま残さない
  • Artifact のアップロード対象に生ログを含めない (.gitignore ではなく paths-ignoreretention-days で守る)

CIのログはリポジトリの .gitignore の管轄外です。git に入っていないから安全、という直感がここでは通用しません。

経路3:VSCode の workspaceStorage にキャッシュされる

一番私が驚いたのがこれでした。VSCode は、開いたワークスペースに関する各種メタデータをローカルに保存します。パスは Linux ならこう。

~/.config/Code/User/workspaceStorage/

拡張機能によっては、ここに .env の中身の一部や、環境変数の名前と値のペアが残ることがあります。私が試したのは以下の3種の拡張です。

  • Python 拡張:launch.jsonenvFile パスと、直近のデバッグセッションの一部
  • REST Client 系:.http ファイルに書いた {{apiKey}} の解決済み値がキャッシュされることがある
  • Copilot 系:ワークスペース検索インデックスの中に .env の内容が入り、後続の候補生成の材料になる

再現の grep はこう書きました。

grep -rE "sk-|AKIA|ghp_" ~/.config/Code/User/workspaceStorage/ 2>/dev/null | head

自分の環境で当たりが出たときは、対象の拡張の設定を見直しました。特に「ワークスペースを開くたびに .env を読ませる」タイプの拡張は、キャッシュを持つ確率が高いです。

対策は次のとおりです。

  • .env を直接読ませる拡張を最小化する。代替として、envFile に指すのはローカル専用のダミーだけにする
  • ワークスペースを閉じたら workspaceStorage/ の該当フォルダを削除する運用にする
  • 機密プロジェクトは別プロファイルで開き、拡張の共有を切る
  • 業務PCとプライベートPCを分けるだけでは足りない。同じPC上のワークスペース間でも漏れる前提で運用する

「拡張がキャッシュを持たないはず」という前提でシークレットを扱うと、後から拡張の仕様変更で漏れます。実装は外部リポジトリのコードなので、私たちには制御できません。「持たれても困らない値だけを渡す」設計に寄せるのが安全です。

3経路の共通点:git の外側で守る発想が必要

3経路を並べると、共通の構造が見えます。

経路 保存場所 .gitignore の効果 対策の軸
Shell履歴 ~/.zsh_history なし シークレットを手で打たない
CIログ GitHub Actions Artifact なし シークレットを stdout に流さない
IDEインデックス workspaceStorage/ なし シークレットをファイルに書かない

3つとも、gitリポジトリの 外側 です。.gitignore はコミット対象を絞る仕組みなので、コミット対象になったことがないファイルには一切効きません。

守るべき境界は「gitに入るかどうか」ではなく、「シークレット文字列がプレーンテキストで残る場所を作らないこと」です。ここが直感と一番ずれる点でした。私自身、以前は .gitignore にさえ入れれば安心だと素朴に思っていました。今回の3経路で、その思い込みは撤回しました。

2026年時点のシークレット管理ツールとの噛み合わせ

3経路を全部塞ぐには、単発の対策ではなく仕組みで守る必要があります。2026年時点で私が実際に使っているのは次の組み合わせです。

  • 1Password CLI (op run):ローカル開発。Shell履歴に値が残らない
  • HashiCorp Vault または AWS Secrets Manager:サーバサイド。CIから直接引く
  • GitHub Actions の OIDC + AWS IAM Role:短寿命クレデンシャルで、そもそも長寿命の秘密をCIに渡さない
  • gitleaks + trufflehog:コミット前と過去履歴のスキャン。追加のフックとして走らせる

.env を扱わない設計に寄せると、上の3経路のほとんどが自動で塞がります。手元で export を打たなくなり、CI に長期のキーを渡さなくなり、IDEに読ませる .env そのものが縮みます。単発の tips ではなく、この構造を作れるかが分かれ目でした。

この記事のスコープ外

正直に書いておきます。今回検証していないのは次の経路です。

  • Docker イメージへの焼き込み (別記事の範囲)
  • Slack や Chatwork にキーを貼ってしまうケース
  • dotfiles のリポジトリに .zshrc ごと push してしまうケース
  • クラウドサービス側で保存されるログ (Sentry, Datadog 等)

これらはこれからの記事で順に扱う予定です。読者の側で「うちだとどこが漏れそうか」を1つ思い浮かべたら、それは高確率でクリティカルです。手を動かして自分の環境で grep してみてください。私はそこで青くなりました。

まとめ

  • .env.gitignore に入れても、Shell履歴・CIログ・IDEインデックスの3経路で漏れる
  • 3経路とも git の外側に保存されるので、.gitignore の効果はゼロ
  • Shell履歴は 1Password CLI などで export を手打ちしない設計に寄せる
  • CIログは set -xecho を封じ、OIDC + IAM Role で長期キーを渡さない
  • IDEは workspaceStorage/ を「値が残る前提」で扱い、機密プロジェクトは分離する
  • 単発のtipsではなく、「シークレット文字列がプレーンテキストで残る場所を作らない」構造で守る

私は今回の検証で、自分のマシンから想定より多くの秘密文字列を釣り上げました。.gitignore の内側だけを気にする運用は、そろそろ卒業したほうがよさそうです。手元で grep -E "sk-|AKIA|ghp_" ~/.zsh_history を1回叩くだけで、話が変わります。面白くいきましょう。

MCP経由でLLMに外部ツールをつなぐと、シークレットの通り道はさらに増えます。設計側の考え方は拙著にまとめてあります。
MCPセキュリティ実践

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?