Docker の BuildKit を使ってセキュアなビルドを試す

# はじめに

Docker 18.09 で `BuildKit` が正式に採用されました。`BuildKit` によって、パフォーマンスの向上、ビルドキャッシュの改良、 鍵ファイルや SSH 秘密鍵の安全なマウントなどの改良がされました。今回は、`BuildKit` の鍵ファイル、SSH 秘密鍵の安全なマウントに着目して、セキュアなビルドを試してみます。

# 環境

- macOS High Sierra Version 10.13.6
- Docker for Mac 2.0.0.3 (Engine: 18.09.2)

# BuildKit を有効にする

環境変数から `BuildKit` を有効にするには、`DOCKER_BUILDKIT=1` を実行して有効にします。

```bash
$ export DOCKER_BUILDKIT=1
$ docker build .
```

デフォルトで有効にする場合は、`/etc/docker/daemon.json` に以下の設定を記述して再起動することで有効になります。
本記事での環境は Docker for Mac なので、Docker for Mac の設定から `Daemon - Advanced` を開いて設定を記述します。

```json
{
  "features": {
    "buildkit": true
  }
}
```

以降は、`BuildKit` が有効である前提で進めます。

# 鍵ファイルのマウント

今まで Dockerfile 内からプライベートな Git リポジトリーや AWS などのクラウドサービスに接続するにはちょっとした工夫をする必要がありました。

まずは単純に鍵ファイルを `COPY` 命令でビルドした例をみてみましょう。
以下のような Dockerfile を用意します。

```Dockerfile:Dockerfile
FROM ubuntu:18.04

COPY id_rsa /root/.ssh/

RUN apt-get update && apt-get install -y git

RUN ssh-keyscan -H github.com >> /root/.ssh/known_hosts \
    && git clone git@github.com:[ユーザー名]/[プライベートリポジトリ名].git

RUN rm -rf /root/.ssh/id_rsa
```

鍵ファイルを用意したディレクトリでビルドしてみます。

```bash
$ docker build -t sample .
```

ビルドした Docker イメージの履歴を表示してみます。

```bash
$ docker history sample
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
4a841c8c9d87        2 minutes ago       /bin/sh -c rm -rf /root/.ssh/id_rsa                 0B
de2dc4ca7089        2 minutes ago       /bin/sh -c ssh-keyscan -H github.com >> /roo…   22.6kB
16ced59e0844        2 minutes ago       /bin/sh -c apt-get update && apt-get install…   118MB
207d964e6f0b        3 minutes ago       /bin/sh -c #(nop) COPY file:53ba8782ad73011c…   1.68kB
47b19964fb50        3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           3 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:529264c6593975a61…   88.1MB
```

履歴を見ると鍵ファイルを削除する前のレイヤーが残っていることが分かります。
削除する前のレイヤーにアクセスできるということはパブリックなレジストリにプッシュしてしまうと、第三者が鍵ファイルにアクセスできてしまうことになります。

以前のバージョンでこのレイヤーへアクセスできないようにするには、マルチステージビルドや `docker build --squash` を用いて不要なレイヤを削除する必要があります。
マルチステージビルドを試した場合の Dockerfile は以下のとおりです。

```Dockerfile:Dockerfile.multi
FROM ubuntu:18.04 as builder

COPY id_rsa /root/.ssh/

RUN apt-get update && apt-get install -y git

RUN ssh-keyscan -H github.com >> /root/.ssh/known_hosts \
    && git clone git@github.com:[ユーザー名]/[プライベートリポジトリ名].git

RUN rm -rf /root/.ssh/id_rsa

FROM ubuntu:18.04

COPY --from=builder /private-test /private-test
```

マルチステージビルドの Dockerfile をビルドします。

```bash
$ docker build -f Dockerfile.multi -t sample-multi .
```

マルチステージビルドした Docker イメージの履歴を表示してみます。

```bash
$ docker history sample-multi
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
8ec225a84208        3 minutes ago       /bin/sh -c #(nop) COPY dir:681f0d3bca9cd752e…   21.7kB
47b19964fb50        3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           3 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:529264c6593975a61…   88.1MB
```

`COPY` したレイヤだけが表示されているので鍵ファイルにアクセスできないことが分かります。

次に Docker の `experimental` を有効にして、 `docker build --squash` を試した場合の Dockerfile は以下のとおりです。

```Dockerfile:Dockerfile.squash
FROM ubuntu:18.04

COPY id_rsa /root/.ssh/

RUN apt-get update && apt-get install -y git

RUN ssh-keyscan -H github.com >> /root/.ssh/known_hosts \
    && git clone git@github.com:[ユーザー名]/[プライベートリポジトリ名].git

RUN rm -rf /root/.ssh/id_rsa
```

`docker build --squash` でビルドしてみます。

```bash
$ docker build --squash -f Dockerfile.squash -t sample-squash .
```

`docker build --squash` した Docker イメージの履歴を表示してみます。

```bash
$ docker history sample-squash
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
d447056e816f        About a minute ago                                                   118MB               merge sha256:4a841c8c9d871dbe8f44c3624cb41bb3c0f65ff76b1b6ea29516d3cb945f0f4d to sha256:47b19964fb500f3158ae57f20d16d8784cc4af37c52c49d3b4f5bc5eede49541
<missing>           3 hours ago          /bin/sh -c rm -rf /root/.ssh/id_rsa                 0B
<missing>           3 hours ago          /bin/sh -c ssh-keyscan -H github.com >> /roo…   0B
<missing>           3 hours ago          /bin/sh -c apt-get update && apt-get install…   0B
<missing>           3 hours ago          /bin/sh -c #(nop) COPY file:53ba8782ad73011c…   0B
<missing>           3 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           3 weeks ago          /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           3 weeks ago          /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           3 weeks ago          /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           3 weeks ago          /bin/sh -c #(nop) ADD file:529264c6593975a61…   88.1MB
```

`docker build --squash` を利用するとマージされ不要なレイヤが削除されていることが分かります。

以上のように以前のバージョンでは注意して対応する必要がありました。

そこで `BuildKit` では、`RUN --mount=type=secret` 命令を使用することでマウントできるようになりました。
試した例が以下の Dockerfile です。

```Dockerfile:Dockerfile.secret
# syntax = docker/dockerfile:experimental
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y git

RUN --mount=type=secret,id=ssh,dst=/root/.ssh/id_rsa \
    ssh-keyscan -H github.com >> /root/.ssh/known_hosts \
    && git clone git@github.com:[ユーザー名]/[プライベートリポジトリ名].git
```

Dockerfile の新しい構文を利用するため、1 行目に `# syntax = docker/dockerfile:experimental` を指定する必要があります。
そして、`RUN --mount=type=secret` 命令を用いて鍵ファイルをイメージに残す危険性を考えずにマウントできます。
`id` には docker build 時に渡すキーとなる任意の同じ ID 、`dst` には　 Docker コンテナ内のマウント先を指定します。`dst` は、`target`・`destination` のいずれかでも動作します。
以下のコマンドでビルドします。

```bash
$ docker build --secret id=ssh,src=$HOME/.ssh/i d_rsa -f Dockerfile.secret -t sample-secret .
```

`--secret` を有効にし、`id` に Dockerfile の `RUN --mount=type=secret` 命令でも使用した同じ ID、`src` にマウント元を指定します。`src` は `source` でも動作します。

`RUN --mount=type=secret` 命令を使用した Docker イメージの履歴を表示してみます。

```bash
$ docker history sample-secret
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
f2f83f34ae1c        4 minutes ago       RUN /bin/sh -c ssh-keyscan -H github.com >> …   22.6kB              buildkit.dockerfile.v0
<missing>           30 hours ago        RUN /bin/sh -c apt-get update && apt-get ins…   118MB               buildkit.dockerfile.v0
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           4 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           4 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           4 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:529264c6593975a61…   88.1MB
```

鍵ファイルが残っていないことが見て取れます。

実際にコンテナを起動して確認して見ると、コンテナの中には空のファイルが残っていることが分かります。

```bash
$ docker run --rm --name sample-secret -it -d sample-secret bash
$ docker attach sample-secret
root@9d4b16c1ba71:/# ls /root/.ssh/
id_rsa  known_hosts
root@9d4b16c1ba71:/# cat /root/.ssh/id_rsa
root@9d4b16c1ba71:/#
```

# SSH 秘密鍵のマウント

`RUN --mount=type=secret` 命令では、パスフレーズ付きの SSH 秘密鍵には対応できません。
そこで代わりに `RUN --mount=type=ssh` 命令が対応しています。
試した例が以下の Dockerfile です。

```Dockerfile:Dockerfile.ssh
# syntax = docker/dockerfile:experimental
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y git

RUN --mount=type=ssh \
    mkdir -p -m 0600 ~/.ssh \
    && ssh-keyscan -H github.com >> ~/.ssh/known_hosts \
    && git clone git@github.com:[ユーザー名]/[プライベートリポジトリ名].git
```

1 行目に `# syntax = docker/dockerfile:experimental` を指定し、`RUN --mount=type=ssh` 命令を用いて SSH 秘密鍵をマウントします。

事前にパスフレーズ入力するため、`ssh-agent` を実行して SSH 秘密鍵を登録します。

```bash
$ ssh-add ~/.ssh/id_rsa
Enter passphrase for /Users/[ユーザー名]/.ssh/id_rsa: [パスフレーズを入力する]
Identity added: /Users/[ユーザー名]/.ssh/id_rsa (/Users/[ユーザー名]/.ssh/id_rsa)
```

以下のコマンドでビルドします。

```bash
$ docker build --ssh default -f Dockerfile.ssh -t sample-ssh .
```

`--ssh` を有効にし、`default` を渡すことで `ssh-agent` に接続し解決してくれます。

# さいごに

`BuildKit` を用いて鍵ファイルや SSH 秘密鍵をマウントしてイメージに認証情報を残さずビルドできることが分かりました。
リリースサイクルを考えると Docker 19.03 が控えているので次のリリースが楽しみです。

# 参考

[Build Enhancements for Docker | Docker Documentation](https://docs.docker.com/develop/develop-images/build_enhancements/)
[Docker v18.09 新機能 (イメージビルド&セキュリティ) – nttlabs – Medium](https://medium.com/nttlabs/docker-v18-09-%E6%96%B0%E6%A9%9F%E8%83%BD-%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%93%E3%83%AB%E3%83%89-%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3-9534714c26e2)
[Docker セキュリティ: 今すぐ役に立つテクニックから，次世代技術まで - SlideShare](https://www.slideshare.net/AkihiroSuda/docker-125002128)
[Access Private Repositories from Your Dockerfile Without Leaving Behind Your SSH Keys - vsupalov](https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/)
[Docker Engine 18.09 から使える Build-time secrets を試してみた - はったりエンジニアの備忘録](https://blog.manabusakai.com/2018/12/docker-build-secret/)
[GitHub - moby/buildkit: concurrent, cache-efficient, and Dockerfile-agnostic builder toolkit](https://github.com/moby/buildkit)