Docker Notaryで署名したイメージをDockerHubで公開する

はじめに

ソフトウェアをインストールするさいにHashの検証や署名の検証などで、ダウンロードしてきたバイナリの信頼性を検証するみたいなのはよくやると思います。Dockerのイメージに対してこれがどのように解決されるかというところに理解が浅かったので、Dockerのコンテントトラストのドキュメントを読みつつ、自分で作成したイメージに署名をするところまでやってみようかと思います。

どのようにDocker のコンテントトラストが実現されるか

DockerではDCT(Docker Content Trust)と呼ばれる機能でデジタル署名を利用してデータの整合性と公開者情報を検証できる仕組みを提供しているようです。
この機能を使うと特定のイメージタグに対して検証を行えるようになります。
DCTでは、タグ毎にサインを行いどのタグにサインを行なうかはイメージの公開者が決める必要があります。
また、1つのリポジトリで1つのイメージに対してサインされているタグは1つだけ存在するようです。

クライアント目線で言うと、DCTを有効にした場合実行できるイメージはサインされたイメージのみで、ほかは利用できなくなります。フィルターの概念が近いようです。

Notaryについて

Docker DCTの仕組みはNotrayという機能の上で実装されているみたいです。
Notrayはサーバサイドとクライアントサイドで提供されており、サーバサイドが利用するDockerリポジトリにアタッチされている必要があるようです。
このブログでは独自にリポジトリを用意してアタッチすることなどは行いません(そのやり方に付いてはこちらを確認ください)。 今回はDocker Hubを使います。

また、NotrayはTOFU(Trust On First Use)というモデルを採用しており、最初にダウンロードしたものを信じるという仕様になってます。
V2ではこれを改善するような議論がコミュニティで行われているようです(ソースを見つけられなかった...)

自分で作成したイメージに署名する

環境

今回は以下の環境で諸々を動かしてみます。

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

$ uname -srvmpio
Linux 5.4.0-80-generic #90-Ubuntu SMP Fri Jul 9 22:49:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ docker version
Client: Docker Engine - Community
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:54:27 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:52:33 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 runc:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

イメージの作成

まずは署名を行なうイメージを作っておきます。
今回はnginxの公式イメージを使って作ります。

$ docker create nginx

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS    PORTS     NAMES
896fa7872413   nginx     "/docker-entrypoint.…"   27 seconds ago   Created             cranky_dewdney

$ docker commit 896fa7872413 hirohiroyuya/nginx:singed
sha256:ffcb0c4915f34a5d68f4eb6e8452db191d4c19e6ecfc4bc17a90b16689e0dfaa

$ docker commit 896fa7872413 hirohiroyuya/nginx:non-singed
sha256:8e321e701fd6f0f5bf730dfd55c464804f85570e0dcb31326f6e50e4c289b8a4


$ docker images
REPOSITORY           TAG          IMAGE ID       CREATED              SIZE
hirohiroyuya/nginx   non-singed   8e321e701fd6   About a minute ago   133MB
hirohiroyuya/nginx   singed       ffcb0c4915f3   About a minute ago   133MB
nginx                latest       08b152afcfae   3 weeks ago          133MB

今回は2つのタグを作り片方は署名しもう片方は署名せずにPushしようと思います。 下準備は完了です。

鍵の作成とNotaryサーバへの設定

署名をする前に鍵を作る必要があります。
鍵を作る場合には以下のコマンドを実行します。

$ docker trust key generate henoheno
Generating key for henoheno...
Enter passphrase for new henoheno key with ID 3eb42d4: 
Repeat passphrase for new henoheno key with ID 3eb42d4: 
Successfully generated and loaded private key. Corresponding public key available: /home/someone/henoheno.pub

今回は試しませんがすでに鍵がある場合は以下のようにして既存のロードできるみたいです。

$ docker trust key load key.pem --name jeff

次に作成された公開鍵を公開鍵を Notary サーバーへ追加します。 今回はDockerHubを使うので特にドメインなどは指定してませんが、必要な場合は指定してください。

$ docker trust signer add --key /home/yuya-hirooka/henoheno.pub henoheno hirohiroyuya/nginx
Adding signer "henoheno" to hirohiroyuya/nginx...
Enter passphrase for repository key with ID 7d993ef: 
Successfully added signer: henoheno to hirohiroyuya/nginx

ここまでで鍵の生成とサーバへの設定は終了です。

署名したイメージとしてないイメージをリポジトリにPushする

署名は以下のコマンドで行なうことができます。

$ docker trust sign hirohiroyuya/nginx:singed
Signing and pushing trust data for local image hirohiroyuya/nginx:singed, may overwrite remote trust data
The push refers to repository [docker.io/hirohiroyuya/nginx]
e3135447ca3e: Mounted from library/nginx 
b85734705991: Mounted from library/nginx 
988d9a3509bb: Mounted from library/nginx 
59b01b87c9e7: Mounted from library/nginx 
7c0b223167b9: Mounted from library/nginx 
814bff734324: Mounted from library/nginx 
singed: digest: sha256:505db062138c1e3dd094c9e5811c6cd9baae8c7beb77b1c010db809f2e0d8fd3 size: 1570
Signing and pushing trust metadata
Enter passphrase for henoheno key with ID 3eb42d4: 
Successfully signed docker.io/hirohiroyuya/nginx:singed

イメージをPushします。 この際にDOCKER_CONTENT_TRUST=1環境変数に指定してコンテントトラストを有効にする必要があるようです。

$ DOCKER_CONTENT_TRUST=1 docker push hirohiroyuya/nginx:singed
The push refers to repository [docker.io/hirohiroyuya/nginx]
e3135447ca3e: Layer already exists 
b85734705991: Layer already exists 
988d9a3509bb: Layer already exists 
59b01b87c9e7: Layer already exists 
7c0b223167b9: Layer already exists 
814bff734324: Layer already exists 
singed: digest: sha256:505db062138c1e3dd094c9e5811c6cd9baae8c7beb77b1c010db809f2e0d8fd3 size: 1570
Signing and pushing trust metadata
Enter passphrase for henoheno key with ID 3eb42d4: 
Successfully signed docker.io/hirohiroyuya/nginx:singed

これでDockerHubでの公開が完了しました。

f:id:yuya_hirooka:20210814160814p:plain

Pushしたイメージの署名の情報を見るためには以下のコマンドを実行します。

$ docker trust inspect --pretty hirohiroyuya/nginx:singed

Signatures for hirohiroyuya/nginx:singed

SIGNED TAG   DIGEST                                                             SIGNERS
singed       12d3e6084e8af99509bd65b1d4583953cfb0791ddd66c4db199b725f6463327c   henoheno

List of signers and their keys for hirohiroyuya/nginx:singed

SIGNER     KEYS
henoheno   3eb42d4ad775

Administrative keys for hirohiroyuya/nginx:singed

  Repository Key:   7d993ef2d41d5473aa8556e987cf8449bda7edd07b34856a13788a495bf70e3c
  Root Key: 9f0717638ac4ef0e113004354e2946c8010e2f6cb5b425af4d0779986ad45c74

Pushしたイメージを利用する

まずは、比較を行なうために先程のhirohiroyuya/nginx:non-singedの方もPushしておきます。
また、ローカルのイメージも綺麗にしておきます。

$ docker push hirohiroyuya/nginx:non-singed
The push refers to repository [docker.io/hirohiroyuya/nginx]
e3135447ca3e: Layer already exists 
b85734705991: Layer already exists 
988d9a3509bb: Layer already exists 
59b01b87c9e7: Layer already exists 
7c0b223167b9: Layer already exists 
814bff734324: Layer already exists 
non-singed: digest: sha256:1ab4fc461a4c9028fa375aefec46c862d9317a2b2009321273c0135f7bdcb6ec size: 1570

$ docker rmi -f $(docker images -a -q)

$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

f:id:yuya_hirooka:20210814161604p:plain

これでOK。

DockerクライアントではDCTはデフォルトで無効になっているようです。
これを有効にするためにはDOCKER_CONTENT_TRUST=1環境変数に指定してコマンドを実行する必要があるようです。

署名が行われていない状態のnginx:non-signeddocker runしてみます。

$ DOCKER_CONTENT_TRUST=1 docker run hirohiroyuya/nginx:non-singed
docker: No valid trust data for non-singed.

実行できないようになってますね。
今度は、署名がされているものをdocker runしてみます。

$ DOCKER_CONTENT_TRUST=1 docker run hirohiroyuya/nginx:singed
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2021/08/14 07:41:54 [notice] 1#1: using the "epoll" event method
2021/08/14 07:41:54 [notice] 1#1: nginx/1.21.1
2021/08/14 07:41:54 [notice] 1#1: built by gcc 8.3.0 (Debian 8.3.0-6) 
2021/08/14 07:41:54 [notice] 1#1: OS: Linux 5.4.0-80-generic
2021/08/14 07:41:54 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2021/08/14 07:41:54 [notice] 1#1: start worker processes
2021/08/14 07:41:54 [notice] 1#1: start worker process 36
2021/08/14 07:41:54 [notice] 1#1: start worker process 37
2021/08/14 07:41:54 [notice] 1#1: start worker process 38
2021/08/14 07:41:54 [notice] 1#1: start worker process 39
2021/08/14 07:41:54 [notice] 1#1: start worker process 40
2021/08/14 07:41:54 [notice] 1#1: start worker process 41
2021/08/14 07:41:54 [notice] 1#1: start worker process 42
2021/08/14 07:41:54 [notice] 1#1: start worker process 43

きちんと実行ができました。
この通り、クライアントサイドではDCTを有効にしている場合、署名されたものしか実行されず更に検証も行われているようです。