2
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?

コンテナビルド時に一時シークレットを安全に渡す

Posted at

はじめに

コンテナイメージのビルド時に、APIキーやクレデンシャルなどの何らかのシークレット情報が一時的に必要になるケースがあります。やり方を間違えるとシークレットが漏洩する場合があります。本稿ではイメージビルド時にホストからビルドコンテナへ一時シークレットを安全に渡す方法を説明します。

結論: シークレットマウントを使う

シークレットマウントを使うと、シークレット情報をイメージレイヤーやビルド履歴に残さずにホストからビルドコンテナへ安全に渡すことができます。

以下、具体的な使用方法です。題材例として、ビルド処理中にAWSへアクセスするために認証情報をホストからビルドコンテナに渡すことを取り上げます (Dockerfileは説明を目的としたものであり実用性はありません)。

クレデンシャルファイルを渡すケース

Dockerfile
FROM amazon/aws-cli
RUN --mount=type=secret,id=aws_credentials <<EOF
    export AWS_SHARED_CREDENTIALS_FILE=/run/secrets/aws_credentials
    aws s3 cp s3://Bucket/foo . || :
EOF
buildコマンド
docker image build . \
  --secret id=aws_credentials,src=${HOME}/.aws/credentials

buildコマンドラインから指定されたクレデンシャルファイルを、ビルドコンテナにてシークレットマウント経由で取得し、そのパスを環境変数 AWS_SHARED_CREDENTIALS_FILE に設定してビルド処理内部でAWSへアクセスしています。

以下ポイントです:

  • Dockerfile側:
    • RUN命令で--mount=type=secretオプション (シークレットマウントオプション) を指定することで秘密裡にホストからデータを受け取ることができる。データの中身はイメージレイヤーやビルド履歴に残らない。
    • 受け取るデータはid値で識別される (値は任意。例ではid=aws_credentials)。シークレットマウントオプションは複数指定可能。
    • デフォルトでシークレットデータは /run/secrets/{id値} のパスを通じてアクセスできる。
      • RUN --mount=type=secret,id=foo,target=/path/to/secret のようにtargetを指定すればデフォルト以外のパスも使用できる。
  • buildコマンド側:
    --secretオプションでidにシークレットマウントと同じid値を指定し、srcにホスト側のシークレットファイルのパスを指定する。

環境変数を渡すケース

シークレットマウントでは、ファイルではなく環境変数を渡すこともできます。以下は、ホスト側の環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY をビルドコンテナへ安全に渡す例です。

Dockerfile
FROM amazon/aws-cli
RUN --mount=type=secret,id=key_id,env=AWS_ACCESS_KEY_ID \
    --mount=type=secret,id=key,env=AWS_SECRET_ACCESS_KEY \
    aws s3 cp s3://Bucket/foo . || :
buildコマンド (各環境変数は設定済みとする)
docker image build . \
  --secret id=key_id,env=AWS_ACCESS_KEY_ID \
  --secret id=key,env=AWS_SECRET_ACCESS_KEY

先述のクレデンシャルファイルの例と比べると、おおよそ意味が把握できると思いますが、以下がポイントです。

  • Dockerfile側:
    • シークレットマウントオプションのenvに指定した名前の環境変数が作成され、ホストから渡された値が設定される。
      • ARG命令と異なり、ビルド履歴に残らない。
      • ENV命令と異なり、コンテナイメージに永続化されない。
  • buildコマンド側:
    • --secretオプションのenvに渡したい環境変数名を指定する。変数値ではなく変数名である点に注意 (これによりコマンドライン履歴に値を残してしまう心配がない)。
      • 省略記法として、id値に環境変数名をそのまま指定すればenvを省略できる (ただしDockerfile側は省略できない)。

ファイルを環境変数として / 環境変数をファイルとして渡すこともできる (おまけ)

使用頻度は比較的少ないかもしれないが、ホストのファイルをコンテナの環境変数として渡したり、反対にホストの環境変数をコンテナのファイルとして渡すこともできます。

以下は使い方の例です。

ファイルを環境変数として渡す

Dockerfile
FROM alpine
RUN --mount=type=secret,id=file_to_env,env=FILE_TO_ENV \
    echo "\$FILE_TO_ENV=$FILE_TO_ENV" && stop
$ echo 'This is file from host' > file         
$ docker image build --secret id=file_to_env,src=file .
(...略...)
 > [stage-0 2/2] RUN --mount=type=secret,id=file_to_env,env=FILE_TO_ENV     echo "$FILE_TO_ENV=****" && stop:                                                                   
0.635 $FILE_TO_ENV=This is file from host

環境変数をファイルとして渡す

Dockerfile
FROM alpine
RUN --mount=type=secret,id=ENV_TO_FILE \
    cat /run/secrets/ENV_TO_FILE; echo && stop
$ export ENV_TO_FILE='This is environment variable from host'
$ docker image build --secret id=ENV_TO_FILE .               
(...略...)
 > [stage-0 2/2] RUN --mount=type=secret,id=ENV_TO_FILE     cat /run/secrets/ENV_TO_FILE; echo && stop:
0.414 This is environment variable from host

シークレットの危険な渡し方

以下で取り上げる一時シークレットの渡し方は、意図に反してシークレットがイメージに残ってしまう危険な例です。情報漏洩リスクが極めて大きいので絶対にやってはいけません。

COPY-removeパターン (危険)

シークレットファイルをCOPY命令でコンテナにコピーし、使い終わったあとに RUN rm で削除する方法です。最終結果だけをみれば一見ファイルが削除できているように見えるので問題ないと誤解する方がいるかもしれませんが、コンテナイメージにはCOPYしたシークレットファイルがしっかり残っています。

Dockerfile
FROM amazon/aws-cli
COPY .aws /root/.aws
RUN <<EOF
    aws s3 cp s3://Bucket/foo . || :
    rm -rf /root/.aws
EOF
buildコマンド
docker image build ~ -f Dockerfile -t unsecret:copy-remove

シークレット窃取方法

Dockerfile内に記述した各命令の結果は、イメージの構成要素として、イメージ内部にレイヤー状に積み重ねられて保存されています。特にCOPYなどのファイル操作に関わる命令は、命令毎の処理結果が一つ一つtar形式でイメージ内部に残っており、それぞれ独立してイメージから抜き出すことができます。コンテナイメージは、ベースイメージのレイヤーを基底として、Dockerfileの各命令がレイヤーとして記録されているというわけです。

クレデンシャルファイルをコピーするレイヤー (COPY .aws /root/.aws) と削除するレイヤー (RUN ... rm -rf /root/.aws)はそれぞれ分離しており、コピーレイヤーが残っている以上は、そこからクレデンシャルファイルを抜き出して窃取できます。

では、実際に抜き出してみましょう。まずは、先ほどビルドしたイメージを docker image save コマンドでローカルに展開します。

本稿では、Docker 28.4.0を使用しています。Docker 24以前の場合は展開結果が異なります。

$ docker image save unsecret:copy-remove | tar -x
$ tree
.
├── blobs
│   └── sha256
│       ├── 0727f841555e830a24054117b5d53ecc18438e2e82fc78dd3cc766ca6bb76cab
│       ├── 17ff68c4b9bea5f06b5856240c22968ea7874490485afe314bef7dfff288ddc0
│       ├── 359b48c19a7eda2dbe5f6a063f18327c83cb0791d93b228cdbc3dd3edfb73f65
│       ├── 448e1142ca40c043b07601eba30193c0c41f8870d5f8cbb85394c9aa937adad6
│       ├── 522e56c3db243ee4750373e5ed12559e6714340badabbc2d3bcc1de496b14825
│       ├── 67e026f94fdfc3c3b828b1ea304b409d3cbc7e064e82045fbc7600a38a7415f2
│       ├── 959de936080be6b138609d7a6a6ab382a16f0d36bbc73d5bc4fb10ad6318c8cd
│       ├── 9cad100601e54fcc5f37048337c64efcf9355dec98efaf8b5ea940a1aedcc41e
│       ├── ad4664a08041d4ac373f3ba80abb77c2afd98c47b9a2c9f25c53823a96c7e414
│       ├── b0f9eb6ae07dd2c61cb1613944d1e03bcf82c3d650daa435acd4f5229f48c24f
│       ├── dbe4eaa7fa780a581e2a9991697e94627111feb2246816e68602649faed4391b
│       ├── e5abad3e713a2c2bc69d660d95dae2404edc35470b23fc6f299f6262d1b17c32
│       └── ffa64c02a279b9594dfff2802cb29e5557796339797026b3fb198b03559637d9
├── index.json
├── manifest.json
└── oci-layout
$ tar -tzf blobs/sha256/ad4664a0*
root/
root/.aws/
root/.aws/config
root/.aws/credentials
$ tar -xOzf blobs/sha256/ad4664a0* root/.aws/credentials
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

クレデンシャルを削除したはずのイメージから、クレデンシャル窃取に成功しました。

イメージ内部構造に関する補足

イメージのレイヤーファイルは、blobs/sha256/ 配下に保存されています。レイヤー以外のファイルもあります。ファイル名はsha256ハッシュ値なので名前からはどれがレイヤーか判別できません。Dockerでビルドしたイメージを展開するとトップに manifest.json というファイルが得られ、ここに情報が載っています。

$ jq . manifest.json
[
  {
    "Config": "blobs/sha256/b0f9eb6ae07dd2c61cb1613944d1e03bcf82c3d650daa435acd4f5229f48c24f",
    "RepoTags": [
      "unsecret:copy-remove"
    ],
    "Layers": [
      "blobs/sha256/0727f841555e830a24054117b5d53ecc18438e2e82fc78dd3cc766ca6bb76cab",
      "blobs/sha256/448e1142ca40c043b07601eba30193c0c41f8870d5f8cbb85394c9aa937adad6",
      "blobs/sha256/67e026f94fdfc3c3b828b1ea304b409d3cbc7e064e82045fbc7600a38a7415f2",
      "blobs/sha256/e5abad3e713a2c2bc69d660d95dae2404edc35470b23fc6f299f6262d1b17c32",
      "blobs/sha256/959de936080be6b138609d7a6a6ab382a16f0d36bbc73d5bc4fb10ad6318c8cd",
      "blobs/sha256/ad4664a08041d4ac373f3ba80abb77c2afd98c47b9a2c9f25c53823a96c7e414",
      "blobs/sha256/ffa64c02a279b9594dfff2802cb29e5557796339797026b3fb198b03559637d9"
    ]
  }
]

Layers 配列が、各レイヤーファイルのパスの一覧です。ベースレイヤーから順に記載されています。中身はgzip圧縮のtarballです。それぞれ展開して、中身を取り出すことが可能です。

ARGパターン (危険)

buildコマンドの --build-arg を使って渡した引数は、ARG命令を通じてビルドコンテナ内では一時的な環境変数として使用できます。ARGは、ENVと異なりイメージに残らないと認識している方がいるかもしれませんが、ARG値はコンテナイメージ内部にビルド履歴情報としてしっかり記録されるので、イメージにアクセスできればアクターは誰でもARG値を窃取できます。

実際に、ARGを用いたシークレット情報のやりとりは、Dockerfile Reference にも危険であると明記されています。

Dockerfile
FROM amazon/aws-cli
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
RUN aws s3 cp s3://Bucket/foo . || :
buildコマンド
$ env | grep 'AWS_.*KEY'
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ docker image build -t unsecret:arg . \                  
  --build-arg AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  --build-arg AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}

シークレット窃取方法

ビルド履歴は、docker image historyコマンドで簡単に確認できます。見てみましょう。

$ docker image history --format '{{.CreatedBy}}' --no-trunc unsecret:arg       
RUN |2 AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY /bin/sh -c aws s3 cp s3://Bucket/foo . || : # buildkit
ARG AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
ARG AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
/bin/sh -c #(nop)  ENTRYPOINT ["/usr/local/bin/aws"]
/bin/sh -c #(nop) WORKDIR /aws
/bin/sh -c #(nop) COPY dir:ac700318f39dde8c8b7f4798e4a165435175eaf9334e07b42b201e97471c26fb in /usr/local/bin/ 
/bin/sh -c #(nop) COPY dir:1a110cdca7ad9228f567c289816cffad531e6fab3823e6ce54bb2b6d6f2994b0 in /usr/local/aws-cli/ 
/bin/sh -c dnf update -y   && dnf install -y less groff findutils jq   && dnf clean all
CMD ["/bin/bash"]
ADD al2023-container-raw-2023.8.20250908.0-amd64.tar.xz / # buildkit

ARGで渡したクレデンシャルがばっちり取得できました。ビルド履歴には、ベースイメージも含めて最終イメージ作成までに実行されたDockerファイルの命令が記録されています。新しいものから順に表示されます。ARG AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE 行よりも下のエントリはベースイメージ由来のものです。

なお、ビルド履歴の情報は、イメージ内部のイメージコンフィグファイルに記録されてます。イメージを展開して直接確認することも可能です。イメージコンフィグはJSON形式になっており、その history 配列にビルド履歴の各エントリが記載されています。イメージコンフィグの構造については、OCIイメージフォーマット仕様のOCI Image Configurationを参照してください。

コンフィグファイルのパスは、Dockerの場合 manifest.jsonConfig キーから取得できます。

以下が生のビルド履歴を読み出すコマンドです。

$ docker image save unsecret:arg | tar -x
$ jq -r '.[0].Config' manifest.json | xargs jq '.history'
[
  {
    "created": "2025-09-06T00:39:38.427489338Z",
    "created_by": "ADD al2023-container-raw-2023.8.20250908.0-amd64.tar.xz / # buildkit",
    "comment": "buildkit.dockerfile.v0"
  },
  {
    "created": "2025-09-06T00:39:38.427489338Z",
    "created_by": "CMD [\"/bin/bash\"]",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2025-09-12T18:16:36.062344603Z",
    "created_by": "/bin/sh -c dnf update -y   && dnf install -y less groff findutils jq   && dnf clean all"
  },
  {
    "created": "2025-09-12T18:16:41.181526869Z",
    "created_by": "/bin/sh -c #(nop) COPY dir:1a110cdca7ad9228f567c289816cffad531e6fab3823e6ce54bb2b6d6f2994b0 in /usr/local/aws-cli/ "
  },
  {
    "created": "2025-09-12T18:16:42.451523886Z",
    "created_by": "/bin/sh -c #(nop) COPY dir:ac700318f39dde8c8b7f4798e4a165435175eaf9334e07b42b201e97471c26fb in /usr/local/bin/ "
  },
  {
    "created": "2025-09-12T18:16:42.639014418Z",
    "created_by": "/bin/sh -c #(nop) WORKDIR /aws"
  },
  {
    "created": "2025-09-12T18:16:42.76272951Z",
    "created_by": "/bin/sh -c #(nop)  ENTRYPOINT [\"/usr/local/bin/aws\"]",
    "empty_layer": true
  },
  {
    "created": "2025-09-18T09:54:49.762964635Z",
    "created_by": "ARG AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2025-09-18T09:54:49.762964635Z",
    "created_by": "ARG AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2025-09-18T09:54:49.762964635Z",
    "created_by": "RUN |2 AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY /bin/sh -c aws s3 cp s3://Bucket/foo . || : # buildkit",
    "comment": "buildkit.dockerfile.v0"
  }
]

さいごに

ビルド時の一時シークレットは、シークレットマウントを使用して渡してください。自分で思いついた独自のやり方は、セキュアではないかもしれないと考えた方が無難でしょう。

参考資料

2
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
2
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?