5
3

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はDockerイメージに焼き込まれ、誰でも抜き出せる

5
Last updated at Posted at 2026-06-26

その.envは焼き込まれている:Dockerイメージは読み取り専用レイヤーの積み重ねで、COPY .envした秘密はRUN rmしても下のレイヤーに残り、docker saveで展開すれば抜き出せる。BuildKit secret mountで渡せという図

.env.gitignore に書いてある。Gitには上げていない。だから秘密は守れている。

そう考えている人は多いはずです。私もそうでした。ですが、Dockerでコンテナ化しているなら、その安心は半分しか正しくありません。.env をGitに上げていなくても、COPY .env した瞬間に、秘密はDockerイメージのレイヤーにそのままコピーされます。そして docker history や、イメージを展開するだけで、誰でも読めます。

この記事では、ビルドしたイメージから秘密を抜き出す手順を実演し、なぜマルチステージでも消えないのかを説明し、BuildKitのsecret mountで正しく守るところまで書き切ります。Gitの履歴とは別の、コンテナ特有の漏れ方の話です。

この記事が扱うのは「コンテナイメージに焼き込まれた秘密」です

最初に、似たテーマとの違いを切り分けます。秘密の漏れ方は経路ごとに対策が違うので、混ぜると守り損ねます。

どこに残るか この記事との関係
git履歴の残存 VCSのコミットオブジェクト 別軸(削除しても残る)
npm等の依存攻撃 第三者パッケージ 別軸
ローカルの窃取 開発機の .env 別軸
イメージへの焼き込み ビルドで生成したレイヤー 本記事

本記事が扱うのは一番下です。Gitに何を上げたかは関係ありません。ビルドという行為そのものが、秘密をイメージのレイヤーに固定してしまう、という話です。

他人のイメージから鍵が出てきた話

具体例から入ります。以前、あるOSSのDockerイメージをベースにしようとしてDocker Hubから引いてきたとき、中身を確認するために docker history を流しました。

そこに、外部サービスのAPIキーらしき文字列が、伏字なしで表示されました。ENV API_KEY=... の形で焼き込まれていたのです。作者は本番では使っていない検証用の鍵だと思いますが、世界中の誰でもpullして読める状態でした。

このとき、ひやりとしたのは自分のことでした。私も昔、検証用のイメージで COPY .env . をやったことがある。あのイメージ、レジストリに残っていないだろうか、と。確認したら、幸いプライベートでしたが、もし公開していたら同じことになっていました。他人の事故は、たいてい自分にも起こりえます。

なぜマルチステージでも秘密は消えないのか

そもそも、なぜイメージに秘密が残るのでしょうか。理由はDockerイメージのデータ構造にあります。

Dockerイメージは、読み取り専用のレイヤーを積み重ねた構造です。RUNCOPYADD の命令ひとつごとに、変更分だけを記録した新しいレイヤーが1枚作られます。各レイヤーはtarballとして保存され、内容ハッシュで管理されます。地層のようなもので、上に土を盛っても、下に埋めたものは下の地層に残り続けます。

ここに落とし穴があります。COPY .env で秘密を入れたあと、後続のレイヤーで RUN rm .env しても、秘密は消えません。削除という操作も「上に積む新しいレイヤー」でしかなく、.env を含む下のレイヤーはそのまま残るからです。最終的なファイル一覧から見えなくなるだけで、レイヤーを1枚ずつ展開すれば中身は出てきます。

ARGENV も同じです。ビルド時に渡した値はイメージのメタデータに記録され、docker history で丸見えになります。

「レイヤーをまとめれば消えるのでは」と考える人もいます。--squash で全レイヤーを1枚に潰す方法です。これは当てになりません。squashしても、ビルド途中のレイヤーがキャッシュやレジストリに残ることがあり、ENV の値はメタデータに残ります。レイヤーを潰すのは、焼き込んだ秘密を隠す対症療法であって、根本対策にはなりません。秘密は、そもそも焼き込まないのが唯一の正解です。

ARGENV は秘密の置き場所ではありません。渡した値はイメージのレイヤー履歴に保存され、docker history で誰でも読めます。認証情報をこの2つで渡してはいけません。

実演:ビルドしたイメージから秘密を抜く

仕組みだけでは実感が湧きません。実際に抜いてみます。

まず、よくある「うっかり」を含むDockerfileです。

FROM python:3.12-slim
WORKDIR /app
COPY .env .          # 秘密ごとレイヤーに焼き込まれる
COPY . .
RUN rm .env          # 消したつもり。だが下のレイヤーには残る
CMD ["python", "main.py"]

これをビルドします。最終的なコンテナ内に .env は存在しません。RUN rm したからです。ですが、秘密はイメージに残っています。

docker history で見る

docker history --no-trunc myapp:latest

--no-trunc を付けると、各レイヤーを作った命令が省略なしで表示されます。ARG で渡した値や、RUN 内にベタ書きした認証情報は、ここに伏字なしで出ます。出力はこんな具合です。

CREATED BY                                          SIZE
ENV API_KEY=sk-live-abcd1234efgh5678                0B
COPY .env . # buildkit                              112B
RUN /bin/sh -c rm .env # buildkit                   0B

ENV の値が平文で見え、COPY .env のレイヤーも RUN rm のレイヤーも別々に記録されています。削除した事実は記録されますが、焼き込んだ事実は取り消されません。

レイヤーを展開して読む

docker history に出ない COPY .env の中身も、イメージを展開すれば取り出せます。

# イメージをtarに書き出す
docker save myapp:latest -o myapp.tar
mkdir extract && tar -xf myapp.tar -C extract

# レイヤーのtarを片端から展開して .env を探す
find extract -name "*.tar" -exec tar -tf {} \; | grep -i env

# 見つけたレイヤーを展開すれば中身が読める

RUN rm .env をしていても、.env を焼き込んだレイヤーのtarballは残っています。そこを展開すれば、秘密はそのまま出てきます。攻撃者がやることは、これだけです。

公開レジストリにpushしたイメージは、誰でもpullして展開できます。Docker Hub上で1万件を超えるイメージから認証情報が見つかったという調査もあります。「コンテナの中から見えないから安全」は、レイヤーを展開する相手には通用しません。

公開イメージは実際に漏れている

これは理論上のリスクではありません。

セキュリティ各社の調査では、Docker Hubなどの公開イメージから、APIキー・データベース認証情報・秘密鍵などが大量に検出されています。1万件超のイメージで認証情報の混入が確認されたという報告もあります。多くは COPY .env やbuild ARG経由の、ごくありふれたミスです。

なぜこれほど起きるのか。理由は、COPY .env . がいちばん手っ取り早く動くからです。ローカルで .env を読んでいたアプリを、そのままコンテナでも動かそうとすると、.env をイメージに入れるのが最短ルートに見えます。動いてしまうので、問題に気づきません。秘密がレイヤーに焼かれていることは、docker history を叩くまで表に出てこないからです。

つまり、これは「一部の不注意な人」の問題ではありません。Dockerでアプリをコンテナ化する人なら誰でも踏みうる、構造的な罠です。動くコードほど見直されないので、むしろ真面目に動かした人ほどはまります。だからこそ、人の注意ではなく仕組みで防ぐ必要があります。

スキャンはCIに組み込むのが効きます。trivy image --scanners secret をpush前のパイプラインに入れておけば、秘密入りのイメージがレジストリに出る前に止められます。人間のレビューは見落としますが、機械は毎回同じ精度で全レイヤーを走査します。ここは自動化に任せる場所です。

正しく守る:BuildKit secret mount とマルチステージ

ここからが対策です。ビルド時に秘密を使いつつ、レイヤーに焼き込まない方法があります。

BuildKit の secret mount

RUN --mount=type=secret は、秘密を「そのRUN命令の実行中だけ」ファイルとしてマウントします。マウントされた秘密はどのレイヤーにも書き込まれず、docker history にも出ません。これが本命の解決策です。

# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY . .
# 秘密はこのRUNの間だけマウントされ、レイヤーに残らない
RUN --mount=type=secret,id=pipconf,target=/root/.pip/pip.conf \
    pip install -r requirements.txt
# ビルド時に秘密ファイルを渡す
docker build --secret id=pipconf,src=$HOME/.pip/pip.conf -t myapp .

その他の必須対策

  • .dockerignore: .env やキー類を .dockerignore に書き、COPY . . で巻き込まないようにする。.gitignore とは別ファイルなので、両方に書く
  • マルチステージビルド: ビルド専用ステージで秘密を使い、最終ステージには成果物だけをコピーする。ただしビルドステージのレイヤーに秘密を焼くと無意味なので、secret mountと併用する
  • 実行時に注入する: 認証情報はイメージに入れず、起動時に環境変数やマウントで渡す。イメージは「鍵の入っていない箱」に保つ
  • スキャンする: trivy image --scanners secret myapp:latest でpush前にレイヤーを走査し、秘密の混入を検出する

安全なDockerfileの全体像

ここまでの対策をまとめると、最終的なDockerfileはこういう形になります。

# syntax=docker/dockerfile:1
# --- ビルドステージ:秘密はsecret mountで一時利用 ---
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=pipconf,target=/root/.pip/pip.conf \
    pip install --prefix=/install -r requirements.txt

# --- 実行ステージ:成果物だけをコピー、秘密は一切焼かない ---
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
# 認証情報はイメージに入れず、起動時に渡す
CMD ["python", "main.py"]

ポイントは、ビルドステージで使った秘密が最終イメージに一切渡らないことです。実行時の認証情報は、docker run --env-file やオーケストレータのsecret機能で起動時に注入します。イメージ自体は「鍵の入っていない箱」のまま保てます。

対策の優先順位はこうです。(1).dockerignoreで秘密をビルドコンテキストから外す → (2)ビルド時に要る秘密はBuildKit secret mountで渡す → (3)実行時の認証情報はimageに入れず起動時に注入する → (4)push前にTrivyでスキャンする。COPY .envは、そもそも選択肢に入れません。

まとめ

  • Dockerイメージはレイヤーの積み重ね。COPY .env した秘密は RUN rm しても下のレイヤーに残る
  • ARGENV で渡した値は docker history で誰でも読める。秘密の置き場所にしない
  • docker save でイメージを展開すれば、焼き込まれた秘密はそのまま取り出せる
  • 公開イメージから1万件超の認証情報漏れが実際に確認されている
  • 守り方はBuildKit secret mount + .dockerignore + 実行時注入 + Trivyスキャン

知識は、人生の難易度を下げます。「レイヤーは積み重ね」というデータ構造をひとつ知っているだけで、COPY .env という1行が一生書けなくなります。仕組みを知る側に回りましょう。

git履歴側の「削除しても残る秘密」については別記事で扱っています。VCSとコンテナイメージは、秘密の残り方がまったく違うので、両方を押さえておくと守りが固くなります。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?