Dockerイメージレイヤから「削除したはずの認証情報ファイル」を復元できた話

例えば、Docker コンテナ内で**認証情報を必要とするコマンド**（例：AWS CLI など）を実行したい場合に、コンテナ内に認証情報ファイルをコピーして、コマンドを実行したらすぐに削除したとします。

```dockerfile:Dockerfile
FROM alpine:3.21.3

# 認証情報ファイルをコピー
COPY credentials /tmp/credentials

# コマンド実行後に認証情報ファイルを削除
RUN 認証情報が必要なコマンド && rm -f /tmp/credentials
```

コンテナを起動してコンテナ内に入ると`/tmp/credentials`は存在していません。
`RUN`でファイルを削除しているためです。

一見するとセキュリティ面では問題なさそうに思えますが、`RUN`のイメージレイヤで認証情報ファイルは削除されたものの、`COPY`のレイヤにはファイルが**残ったまま**になっているため危険であると言われています。

この事は知識としては知っていたのですが、ふと、このファイルを復元（取り出し）できるのか？が気になり、検証を行ってみました。

先に結論から書きますと、ファイルの復元が**できました**。

## 検証環境

- Ubuntu 22.04.5 LTS (ホスト機)
- Docker version 28.0.1

## 認証情報ファイルが存在しないことを確認する

まずは、作業用のディレクトリを作成します。

```shell
mkdir ~/work
cd ~/work
```

`Dockerfile` を作成します。中身は先ほど登場したものと同じです。
`認証情報が必要なコマンド`の箇所は（AWS CLI のコマンドでも良かったのですが）簡単に使える`touch /tmp/test.txt`で代替します。

```dockerfile:Dockerfile
FROM alpine:3.21.3

# 認証情報ファイルをコピー
COPY credentials /tmp/credentials

# コマンド実行後に認証情報ファイルを削除
RUN touch /tmp/test.txt && rm -f /tmp/credentials
```

`credentials`ファイルを作成します。検証用なので中身は適当です。

```shell
$ echo "abcd1234" > credentials
```

ディレクトリ構造を確認します。

```shell
work
├── credentials
└── Dockerfile
```

Docker コンテナイメージをビルドします。

```shell
$ docker build -t test-credentials:v1 .
```

コンテナを起動します。

```shell
$ docker run -it --rm test-credentials:v1 /bin/sh
```

コンテナ内で`ls`コマンドを実行します。
`/tmp/credentials`が存在しないことを確認できました。

```shell
$ docker run -it --rm test-credentials:v1 /bin/sh
/ # ls /tmp
test.txt
/ # 
```

## 認証情報ファイルを復元する

では、本題の復元作業に入ります。

まずは、コンテナイメージの履歴を確認します。
上から２つ目が認証情報ファイルをコピーした履歴のようです。

```shell
$ docker history test-credentials:v1 
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
baaa7f5efecb   3 hours ago    RUN /bin/sh -c touch /tmp/test.txt && rm -f …   0B        buildkit.dockerfile.v0
<missing>      3 hours ago    COPY credentials /tmp/credentials # buildkit    10B       buildkit.dockerfile.v0
<missing>      2 months ago   CMD ["/bin/sh"]                                 0B        buildkit.dockerfile.v0
<missing>      2 months ago   ADD alpine-minirootfs-3.21.3-x86_64.tar.gz /…   7.83MB    buildkit.dockerfile.v0
```

次に、コンテナイメージを tar ファイルに保存します。

```shell
$ docker image save -o image.tar test-credentials:v1
```

tar ファイルを展開します。

```shell
mkdir my_image
tar -xf image.tar -C my_image
```

```shell
$ tree ./my_image
./my_image
├── blobs
│   └── sha256
│       ├── 08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350
│       ├── 43f60cf296e3de5b33a7e40bd413fb1b6bef27cdfd6511a9c62ec1d67dfa9282
│       ├── 45323140c02695850874d0d9d6b5604630e1db83771f2fe6217a510b36831e87
│       ├── 546e58a06b0be2997519913950b91459021da27c98a02aed826ab6fd0fb45e33
│       ├── a7b3e0a86c7b50efa82ae1162d4ef1c10164eaf17c3bc2886790a10ab1fb0b2f
│       ├── baaa7f5efecb76eb5857eed6791c35191cdcdea97081bf9a43c4fcc61ad0a7dd
│       ├── cf9c58598bc7725d655275a489b8c78fa54e9b386ce05806dd3d92dee05764bf
│       └── d60bba0bcb4bd87500469f4d9a40c42401516589cb0c3fdb311ea5feb1062b4e
├── index.json
├── manifest.json
├── oci-layout
└── repositories
```

`manifest.json` ファイルを確認します。
`Layers`にはレイヤの実行順序が定義されているようなので、上から２つ目の`546e58a0...`がコピーのレイヤだと当たりを付けます。

```json:manifest.json
[
  {
    "Config": "blobs/sha256/baaa7f5efecb76eb5857eed6791c35191cdcdea97081bf9a43c4fcc61ad0a7dd",
    "RepoTags": [
      "test-credentials:v1"
    ],
    "Layers": [
      "blobs/sha256/08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350",
      "blobs/sha256/546e58a06b0be2997519913950b91459021da27c98a02aed826ab6fd0fb45e33",
      "blobs/sha256/a7b3e0a86c7b50efa82ae1162d4ef1c10164eaf17c3bc2886790a10ab1fb0b2f"
    ],
    "LayerSources": {
      "sha256:08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350": {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",
        "size": 8120832,
        "digest": "sha256:08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350"
      },
      "sha256:546e58a06b0be2997519913950b91459021da27c98a02aed826ab6fd0fb45e33": {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",
        "size": 2560,
        "digest": "sha256:546e58a06b0be2997519913950b91459021da27c98a02aed826ab6fd0fb45e33"
      },
      "sha256:a7b3e0a86c7b50efa82ae1162d4ef1c10164eaf17c3bc2886790a10ab1fb0b2f": {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",
        "size": 3072,
        "digest": "sha256:a7b3e0a86c7b50efa82ae1162d4ef1c10164eaf17c3bc2886790a10ab1fb0b2f"
      }
    }
  }
]
```

`blobs/sha256/546e58a0...`の正体は tar ファイルなので、これを展開します。
※`LayerSources`の`mediaType`に`tar`と書かれてある。

```shell
cd my_image
mkdir layer
tar -xf blobs/sha256/546e58a06b0be2997519913950b91459021da27c98a02aed826ab6fd0fb45e33 -C layer
```

展開先ディレクトリの中身を確認します。
ファイルを復元できました。

```shell
$ tree ./layer
./layer
└── tmp
    └── credentials # 認証情報ファイル

2 directories, 1 file

$ cat ./layer/tmp/credentials 
abcd1234
```

## さいごに

筆者は使ったことがありませんが、[dive](https://github.com/wagoodman/dive) という Docker イメージレイヤを視覚的に調査できるツールがあるようで、これを使えばもっと簡単にできたりするのかも知れません。

## 参考記事

- [コンテナイメージの実体を見てみる](https://qiita.com/tettsu__/items/acfd93fe292e02f42850)
- [Dockerイメージをちゃんと理解する](https://techblog.recochoku.jp/10885)