Docker Bench for SecurityでDockerコンテナのセキュリティ診断を行うでは運用しているコンテナのセキュリティをDocker社の提供するベンチマークツールで診断した。起動しているコンテナに関するセキュリティはこのようなベンチマークツールで診断することは可能だが、そもそもコンテナの元となるイメージは信頼できるものなのだろうか。そんな不安を覚えたエンジニアは少なくないと思う。この記事ではDocker 1.8から追加されているDockerイメージへのデジタル署名の仕組みDocker Content Trustを使い、イメージの信頼性の検証を行う。

【注】実際には最後のイメージの真正性の確認で想定した挙動とことなる挙動が起きている。現在問い合わせ中。備忘録として残しておく。

Docker Content Trustとは

Docker Content Trustはデジタル署名技術を利用したDocker イメージ発行者の真正性の検証とDocker イメージ自体の信頼性の検証を行う機能である。

MITM Attack、リプレイAttack、鍵の漏洩対策が施されており、信頼されないイメージに対して、docker pulldocker pushdocker builddocker createdocker runコマンドが使えなくなる。

詳細な仕組みを理解したい場合はIntroducing Docker Content Trust | Docker BlogContent trust in Docker を読んで頂きたい。

Notaryとは

Docker Content Trustを支えているツールにNotaryがある。Notaryは安全なイメージの公開と、イメージ内容を検証するためのDocker社のツールでOSSで公開されている。Notaryはイメージの信頼性の検証にはTUFが使っているとのこと。

実際に使ってみる

今回ははじめから構築するのではなく、Content Trustの検証をするためにDocker社が用意しているデモ用のコンテナを使う。

検証環境

➤  system_profiler SPSoftwareDataType
Software:

    System Software Overview:

      System Version: OS X 10.10.5 (14F27)
      Kernel Version: Darwin 14.5.0

➤  docker-machine -v
docker-machine version 0.5.1 (7e8e38e)

(Dockerホスト)
* Docker version 1.9.1

構成

20151223_content_trust_architecture

Dockerホストの構築

以下のVagrantfileを利用してDockerホストを構築する。

Vagrant.configure(2) do |config|
  config.vm.define "trust-content-vm" do |trust|
    trust.vm.provider :virtualbox do |virtualbox, override|
      virtualbox.name = "trust"
      virtualbox.customize ["modifyvm", :id, "--memory", "2048"]
      override.vm.box = "takanabe/ubuntu1504"
      override.vm.network "private_network", ip: "192.168.33.17"
    end

    trust.vm.provision "trigger" do |trigger|
      trigger.fire do
        puts "======= IN TRIGGER SECTION !!======="
        `docker-machine status #{@machine.name}`
        if $?.exitstatus != 0
          if @machine.provider_name == :virtualbox
            ip = `vagrant ssh #{@machine.name} -c "ip addr show dev eth1 | grep -w inet"`
            .match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)[0]
            port = 22
          else
            ip = @machine.ssh_info[:host]
            port = @machine.ssh_info[:port]
          end

          run "docker-machine -D create -d generic \
            --generic-ip-address #{ip} \
            --generic-ssh-key #{@machine.ssh_info[:private_key_path][0]} \
            --generic-ssh-port #{port} \
            --generic-ssh-user #{@machine.ssh_info[:username]} \
          #{@machine.name}"
        end
      end
    end

    trust.trigger.before :destroy do
      `docker-machine status #{@machine.name}`
      if $?.exitstatus == 0
        run "docker-machine rm #{@machine.name}"
      end
    end
  end
end
vagrant ssh
sudo sh -c 'echo "127.0.0.1 notaryserver" >> /etc/hosts'
sudo sh -c 'echo "127.0.0.1 registry" >> /etc/hosts'

Notary Serverの構築

Notary Server、Notary Signer、MySQLコンテナからなるNotaryシステムを構築する。必要なイメージをdocker-compose buildで取得する。

➤  eval "$(docker-machine env content-trust-vm)"
➤  git clone -b trust-sandbox https://github.com/docker/notary.git
➤  cd notary
➤  cat docker-compose.yml
notaryserver:
  build: .
  dockerfile: notary-server-Dockerfile
  links:
   - notarymysql
   - notarysigner
  ports:
   - "8080"
   - "4443:4443"
  environment:
    SERVICE_NAME: notary
notarysigner:
  volumes:
   - /dev/bus/usb/003/010:/dev/bus/usb/002/010
   - /var/run/pcscd/pcscd.comm:/var/run/pcscd/pcscd.comm
  build: .
  dockerfile: notary-signer-Dockerfile
  links:
   - notarymysql
notarymysql:
  build: ./notarymysql/
  ports:
    - "3306:3306"
➤  docker-compose build
➤  docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
notary_notaryserver           latest              7caf51d0db26        14 minutes ago      771.5 MB
notary_notarysigner           latest              a896b6e24c4f        16 minutes ago      616.1 MB
notary_notarymysql            latest              08a49db35bcd        18 minutes ago      316.9 MB
ubuntu                        14.04               89d5d8e8bafb        2 weeks ago         187.9 MB
golang                        latest              08ff0c215f8f        2 weeks ago         703.8 MB
diogomonica/golang-softhsm2   latest              b2faa32d5624        5 months ago        550.3 MB

取得したイメージを使ってNotaryシステムを構築する。

➤  docker-compose up -d
➤  docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                             NAMES
95e0b84348e7        notary_notaryserver   "notary-server -confi"   8 seconds ago       Up 8 seconds        0.0.0.0:4443->4443/tcp, 0.0.0.0:32768->8080/tcp   notary_notaryserver_1
2f053325fa03        notary_notarysigner   "notary-signer -confi"   9 seconds ago       Up 8 seconds        4443/tcp                                          notary_notarysigner_1
1afbb6c9d7cc        notary_notarymysql    "/start"                 9 seconds ago       Up 9 seconds        0.0.0.0:3306->3306/tcp                            notary_notarymysql_1

Registryの構築

続いて、コンテナイメージを登録するRegistryコンテナを立てる。こちらもお手軽簡単にできる。

➤  cd .. 
➤  git clone https://github.com/docker/distribution.git
➤  cd distribution
➤  docker build -t registry .
➤  docker run -p 5000:5000 --name registry registry &
➤  docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
registry                      latest              7746865674c6        3 minutes ago       812.7 MB
notary_notaryserver           latest              7caf51d0db26        26 minutes ago      771.5 MB
notary_notarysigner           latest              a896b6e24c4f        28 minutes ago      616.1 MB
notary_notarymysql            latest              08a49db35bcd        29 minutes ago      316.9 MB
ubuntu                        14.04               89d5d8e8bafb        2 weeks ago         187.9 MB
golang                        1.5.2               08ff0c215f8f        2 weeks ago         703.8 MB
golang                        latest              08ff0c215f8f        2 weeks ago         703.8 MB
diogomonica/golang-softhsm2   latest              b2faa32d5624        5 months ago        550.3 MB

Operation用コンテナイメージの作成

Notary ServerやRegistryと接続してContent Trustの仕組みを検証するためのOperationコンテナをつくる。ディレクトリを移動する。

cd ..

以下の内容でDockerfileを作成する。

FROM debian:jessie


ADD https://master.dockerproject.org/linux/amd64/docker /usr/bin/docker
RUN chmod +x /usr/bin/docker \
  && apt-get update \
  && apt-get install -y \
  tree \
  vim \
  git \
  ca-certificates \
  --no-install-recommends


WORKDIR /root
RUN git clone -b trust-sandbox https://github.com/docker/notary.git
RUN cp /root/notary/fixtures/root-ca.crt /usr/local/share/ca-certificates/root-ca.crt
RUN update-ca-certificates


ENTRYPOINT ["bash"]

続いてビルドしてイメージを作成する。

➤  docker build -t notary_ops .
➤  docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
notary_ops                    latest              14880b4ca74f        3 minutes ago       333.7 MB
registry                      latest              7746865674c6        10 minutes ago      812.7 MB
notary_notaryserver           latest              7caf51d0db26        33 minutes ago      771.5 MB
notary_notarysigner           latest              a896b6e24c4f        35 minutes ago      616.1 MB
notary_notarymysql            latest              08a49db35bcd        36 minutes ago      316.9 MB
ubuntu                        14.04               89d5d8e8bafb        2 weeks ago         187.9 MB
golang                        1.5.2               08ff0c215f8f        2 weeks ago         703.8 MB
golang                        latest              08ff0c215f8f        2 weeks ago         703.8 MB
debian                        jessie              23cb15b0fcec        2 weeks ago         125.1 MB
diogomonica/golang-softhsm2   latest              b2faa32d5624        5 months ago        550.3 MB

notary_opsを起動する

作成したイメージを利用してRegistryとNotaryに接続した状態でコンテナを起動する。

➤  docker run -it -v /var/run/docker.sock:/var/run/docker.sock --link notary_notaryserver_1:notaryserver --link registry:registry notary_ops

Trust contentテスト用イメージをプルしようとしたが以下のエラーが出た。

root@9685575d7a50:~# docker pull hello-world
Using default tag: latest
Error response from daemon: client is newer than server (client API version: 1.22, server API version: 1.21)

ホストとコンテナ内のクライアントのAPIのバージョンが合っていないのが原因らしい。 DockerホストのAPIバージョンの確認。

$  docker version
Client:
 Version:      1.9.1
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   a34a1d5
 Built:        Fri Nov 20 13:16:54 UTC 2015
 OS/Arch:      linux/amd64

sandbox containerのAPIバージョンの確認。どのコマンドを実行してもエラーが出る状態のようだ。

$  sudo docker exec eb85b4290804 docker version
Client:
 Version:      1.10.0-dev
 API version:  1.22
 Go version:   go1.5.2
 Git commit:   7ff61dd
 Built:        Sat Dec 19 02:35:52 2015
 OS/Arch:      linux/amd64
Error response from daemon: client is newer than server (client API version: 1.22, server API version: 1.21)

ホストのバージョンとコンテナのAPIのバージョンがずれていることが確認された。この辺をみるとdocker-machine upgrade machine_nameコマンドを使ってAPIのバージョンを上げる可能らしいが、この方法はワークしなかった。(APIのバージョンが1.21までしか上げられない)。そこで、Installation from binaries を参考にしてコンテナ側のDockerバイナリを古い1.9.1に入れ替える。

# apt-get install wget
# wget https://get.docker.com/builds/Linux/x86_64/docker-1.9.1
# cp docker-1.9.1 $(which docker)
# docker version
Client:
 Version:      1.9.1
 API version:  1.21
 Go version:   go1.4.3
 Git commit:   a34a1d5
 Built:        Fri Nov 20 17:56:04 UTC 2015
 OS/Arch:      linux/amd64

Server:
 Version:      1.9.1
 API version:  1.21
 Go version:   go1.4.3
 Git commit:   a34a1d5
 Built:        Fri Nov 20 17:56:04 UTC 2015
 OS/Arch:      linux/amd64

APIのバージョンが一致してエラーが出なくなったので、もう一度Trust contentのテスト用イメージをプルする。またエラーが出る。

# docker pull hello-world
# docker tag hello-world registry:5000/test/hello-world:latest
# export DOCKER_CONTENT_TRUST=1
# export DOCKER_CONTENT_TRUST_SERVER=https://notaryserver:4443

レジストリにhello-worldのイメージが登録されてい無いことを確認する。

# docker pull registry:5000/test/hello-world
Using default tag: latest
no trust data available

レジストリにイメージを登録する。

docker push registry:5000/test/hello-world:latest

The push refers to a repository registry:5000/test/hello-world 0a6ba66e537a: Pushed b901d36b6f2f: Pushed latest: digest: sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 size: 2749 Signing and pushing trust metadata You are about to create a new root signing key passphrase. This passphrase will be used to protect the most sensitive key in your signing system. Please choose a long, complex passphrase and be careful to keep the password and the key file itself secure and backed up. It is highly recommended that you use a password manager to generate the passphrase and keep it safe. There will be no way to recover this key. You can find the key in your config directory. Enter passphrase for new root key with id 347da56: Passphrase is too short. Please use a password manager to generate and store a good random passphrase. Enter passphrase for new root key with id 347da56: Repeat passphrase for new root key with id 347da56: Enter passphrase for new repository key with id registry:5000/test/hello-world (4d2561e): Repeat passphrase for new repository key with id registry:5000/test/hello-world (4d2561e): Finished initializing “registry:5000/test/hello-world”

docker pull registry:5000/test/hello-world Using default tag: latest Pull (1 of 1): registry:5000/test/hello-world:latest@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858: Pulling from test/hello-world

Digest: sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 Status: Downloaded newer image for registry:5000/test/hello-world@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 Tagging registry:5000/test/hello-world@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 as registry:5000/test/hello-world:latest

一度削除してpull出来ることを確認する

# docker images | grep hello
registry:5000/test/hello-world   latest              0a6ba66e537a        10 weeks ago        960 B
hello-world                      latest              0a6ba66e537a        10 weeks ago        960 B
registry:5000/test/hello-world                 0a6ba66e537a        10 weeks ago        960 B

# docker rmi -f 0a6ba66e537a
Untagged: hello-world:latest
Untagged: registry:5000/test/hello-world:latest
Untagged: registry:5000/test/hello-world@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
Deleted: 0a6ba66e537a53a5ea94f7c6a99c534c6adb12e3ed09326d4bf3b38f7c3ba4e7
Deleted: b901d36b6f2fd759c362819c595301ca981622654e7ea8a1aac893fbd2740b4c

# docker pull registry:5000/test/hello-world
Using default tag: latest
Pull (1 of 1): registry:5000/test/hello-world:latest@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858: Pulling from test/hello-world

b901d36b6f2f: Pull complete
0a6ba66e537a: Pull complete
Digest: sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
Status: Downloaded newer image for registry:5000/test/hello-world@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
Tagging registry:5000/test/hello-world@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858 as registry:5000/test/hello-world:latest

イメージを偽装する

登録したイメージを偽装するためにRegistryコンテナに入る。

➤  docker exec -it registry bash

pushした時にsha256の値が表示されるが、その値がパスに含まれていないパス使ってイメージのデータがあるディレクトリまで移動し偽装する。

# cd /var/lib/registry/docker/registry/v2/blobs/sha256/a3/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
# echo "Malicious data" > data

イメージの偽装が完了したら、Opeartion用コンテナからローカルに保存されている偽装前のイメージを削除する。

# docker images | grep hello
hello-world                      latest              0a6ba66e537a        10 weeks ago        960 B
registry:5000/test/hello-world   latest              0a6ba66e537a        10 weeks ago        960 B

# docker rmi -f 0a6ba66e537a
Untagged: hello-world:latest
Untagged: registry:5000/test/hello-world:latest
Deleted: 0a6ba66e537a53a5ea94f7c6a99c534c6adb12e3ed09326d4bf3b38f7c3ba4e7
Deleted: b901d36b6f2fd759c362819c595301ca981622654e7ea8a1aac893fbd2740b4c

pullして失敗すればContent Trustの仕組みが有効であることが確認できる。

# docker pull registry:5000/test/hello-world
Using default tag: latest
Pull (1 of 1): registry:5000/test/hello-world:latest@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858: Pulling from test/hello-world

b901d36b6f2f: Pull complete
0a6ba66e537a: Verifying Checksum
filesystem layer verification failed for digest sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4

Content Trustによる真正性の検証により、偽装されたイメージが取得できなくなった。

余談:暗号系の基礎知識について

暗号の公開鍵暗号とかデジタル署名とかPKIとかSSL/TLSとか聞いてピンと来ない人は暗号の勉強が必要かもしれない。その際はこの本が圧倒的にわかりやすいのでお勧め。僕は第2版しか読んでないが、第3版は時代の流れを汲みとって大幅にアップデートされてるようなので読み直したい。

暗号技術入門 第3版
  • Author: 結城 浩
  • Manufacturer: SBクリエイティブ
  • Publish date: 2015-08-26
  • 古典的な暗号理論ではなく、もう少し先端の暗号理論について知りたい人はこんな本もある。

    クラウドを支えるこれからの暗号技術
  • Author: 光成 滋生
  • Manufacturer: 秀和システム
  • Publish date: 2015-06-24
  • 余談2:

    Notary Opretionコンテナ起動時に/var/run/docker.sockをマウントしているわけだがこれセキュリティ的に大丈夫だったんだっけ?。

    余談3

    コンテナイメージを偽装するときに、pushの時に表示されたsha256以下のblobを変更すると失敗する。

    cd /var/lib/registry/docker/registry/v2/blobs/sha256/bb/bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
    echo "Malicious data" > data
    

    こんな感じのエラーメッセージが表示される。

    # docker pull registry:5000/test/hello-world
    Using default tag: latest
    Pull (1 of 1): registry:5000/test/hello-world:latest@sha256:bb39087fb4d12a5d9f2d179759a8599fa943cdc1922e1088215be5cd838b6858
    Error response from daemon: manifest unknown: manifest unknown
    

    ちなみに、blobとはBinary large objectのことである。

    参考

    公式

    Docker Content Trustについて

    TUFについて