Docker官方镜像签名方案:Notary

Overview

1. TUF

1.1 背景

TUF是Tor项目设计出的一套安全分发软件更新的框架,Notary是TUF框架的go语言实现,而Docker Content Trust是TUF框架的应用。理清三者关系有助于后续理解。

1.2 TUF的角色

TUF框架包含五类角色,对应五把密钥,分别是Root, Target, Snapshot, Timestamp, Delegated target(可选),每个角色对应一个元数据文件及一把密钥。

TUF角色和密钥的理解与Notary介绍有重合,可以参考 "2.3 Notary的密钥管理"

1.2 TUF的工作流

TUF框架的定义在这里:https://github.com/theupdateframework/specification/blob/master/tuf-spec.md

step 0: 加载可信的root元数据文件

这一步可以认为是前奏步骤。一个初始的root元数据文件应该早已经随包管理器交付到客户端,或者通过带外流程放置进去了。此时的root元数据文件的超时不重要,因为下一步会更新它。

step 1: 更新root元数据文件

由于现在元数据文件可能是被完全不同的一组key签名的,客户端必须有办法能持续的更新到最新的密钥,通过不断的下载中间阶段的密钥一直到最新版。

step 1.1: 使用N代表可信root元数据文件的版本号
step 1.2: 下载root元数据文件的N+1版本

文件名是固定的格式:VERSION_NUMBER.FILENAME.EXT (e.g., 42.root.json)。不成功的话跳转到1.8步。

step 1.3: 检查签名

N+1版的root元数据必须被以下密钥签名:1)N版本root元数据内指定的达到阈值数量的密钥。2)新版本的root元数据里指定的达到阈值数量的密钥。
如果N+1版本的root元数据没有被正确签名,退出并报错。

step 1.4: 检查回滚攻击(rollback attach)

当前信任的root元数据文件版本(N)必须小于新的root元数据文件(N+1)。如果不是退出并报错。

step 1.5: 新版本元数据文件的超时时间可以忽略,1.8会检查
step 1.6: 设置受信任的root元数据文件为新的元数据文件
step 1.7: 重复step 1.1到1.7直到root元数据为最新
step 1.8: 检查冻结攻击(freeze attach)

检查当前的信任的root元数据文件是否过期,过期的报错退出。

step 1.9: 如果timestamp和/或snapshot密钥已经改变,删掉snapshot和timestamp元数据文件

这个是为了从快进攻击(fast-forward attach)中恢复出来。快进攻击是攻击者可以任意增加元数据的版本号:1)timestamp元数据。2)snapshot元数据。3)targets元数据。

step 2: 下载timestamp元数据文件

下载固定文件名timestamp.json

step 2.1:检查签名

使用可信的root元数据文件里包含的timestamp公钥验证签名。

step 2.2:检查回滚攻击

step 2.3:检查冻结攻击

step 3:下载snapshot元数据文件

如果使能了一致性snapshot,那么文件名就是VERSION_NUMBER.FILENAME.EXT (e.g., 42.snapshot.json)格式,否则就是固定的snapshot.json。

step 3.1:对照timestamp元数据检查

新的snapshot元数据文件的hash和版本号必须和timestamp元数据文件里的一致。否则报错并退出。

step 3.2:检查签名

snapshot元数据文件应该被root元数据文件里面指定的snapshot key正确签名。否则报错退出。

step 3.3:检查回滚攻击
step 3.4:检查冻结攻击

step 4:下载最顶层的targets元数据

下载targets.json

step 4.1:对照snapshot元数据检查

新的targets元数据文件的hash和版本号必须与可信的snapshot元数据文件内保存的一致。

step 4.2:检查“任意软件攻击”(arbitrary software attack)

新的targets元数据文件必须被root元数据文件里指定的targets密钥正确签名,否则报错退出。

step 4.3:检查回滚攻击
step 4.4:检查冻结攻击
step 4.5:前序遍历搜索对应的target,以最顶层的target角色为起始

Note: 前序遍历:按根节点-左子树-右子树的顺序遍历。实际上指的是targets及相应delegation角色构成的数

step 4.5.1 如果节点已经被访问过了,那么跳过这个节点以避免访问环路。如果角色包含了所需要的target元数据,那么跳到step 5。
step 4.5.2 递归访问delegation列表,直到找到相应的targets元数据。

step 5:对照target元数据验证target

step 5.1:找到对应的target元数据,报错退出
step 5.2:否则下载相应的target,验证hash是否与target元数据里的匹配。

2. Notary

2.1 介绍

Notary一个client和一个server组件。
它意图为用户创建一个易用的内容分发和验证系统,TLS本身可以用于加密同web server的安全通道,但是当server沦陷的时候,恶意用户可以轻易的将合法内容替换成非法内容。
使用notary,用户可以用自己妥善保管的线下密钥签名他们的内容,然后发布它的签民的可信内容到他的notary server。
内容的使用者,通常事先获取了内容发布者的公钥,可以与Notary server通信来验证内容的合法性和完整性。

2.2 目标

Notary的实现基于TUF(The Update Framework),TUF是软件安全发布与升级的通用设计。借助TUF,Notary可以获得一些关键优势:

  1. 抗密钥泄漏。内容发布者必须使用密钥来签名内容,镜像签名系统必须保证密钥泄露之后系统可以恢复。TUF使用分层次的多把密钥,可以保证密钥泄漏不会影响。
  2. 新鲜性保证。重放攻击是常见的攻击手段,攻击者可以将老的拥有合法签名的软件包伪装成最新的软件包发布给客户,这些旧软件包可能包含漏洞。Notary使用时间戳来保证内容使用者收到的是最新的内容。
  3. 可配置的信任阈值。经常有一种情形是允许多个发布者发布同一份内容,比如某个项目的多个maintainers。使用信任阈值可以保证只有一定数量的发布者同时签名一份文件他才可以被信任,这样可以保证单独的一份密钥泄漏不会允许恶意内容被发布出去。(Q: 听上去很好,怎么用?一把用户key,一把ci key?)
  4. 签名授权。内容发布者可以将自己的部分可信内容集合授权给另一个签名者。
  5. 使用现有的发布渠道。Notary不需要和任何特殊的发布通道绑定。
  6. 不信任的Mirrors和传输。Notary的元数据可以通过任意的镜像或者通道传输。

2.3 Notary的密钥管理

TUF的密钥是分角色的,不同的密钥有不同的特性和功能。

  • Root key: 用来签名root元数据,root元数据存储了root,targets,snapshot和timestamp公钥的ID。客户可以用这些公钥来验证所有的元数据文件的签名。root key极其重要,建议离线存放,比如存在yubi key硬件里面。它的过期时间应该也是最长的,比如10年。
  • Snapshot key:用来签名snapshot元数据,snapshot元数据列举了集合[注1]的root,targets和delegation元数据文件的文件名大小和hash,它用于验证其他元数据文件的完整性。可以给集合的拥有者保存,也可以给notary service保存。
  • timestamp key: 签名timestamp元数据,timestamp元数据通过给元数据指定最小超时时间,以及指定最新的snapshot的文件名大小hash值等来保证时效性。它用来验证snapshot文件的完整性。timestamp密钥由notary service保存,它可以自动重新生成而不需要集合的拥有者参与。有效期应该最短,比如14天。
  • target key:签名target元数据,target元数据列举了集合内的文件名,大小及相应的hash,这个文件用来验证repository的实际内容的完整性。也用来给其他的合作者授权。这个key由拥有者持有。有效期中等,比如3年。
  • Delegation key:用于签名delegation元数据,delegation元数据列举了集合文件名,大小及hash。这个key与target key实际上是相似的,也可以授权给下一级合作者。

注:
[1] .集合:docker content trust里面实际上是image名字,集合是tag的集合,

3. Docker Content Trust本地测试

3.1 环境准备

  1. 启动本地registry
1$ docker run -d -p 5000:5000 registry:2.4.1
  1. 启动notary
1$ cd notary-src-dir
2$ docker-compose up -d
  1. 导出环境变量
1$ export REGISTRY=localhost:5000
2$ export DOCKER_CONTENT_TRUST=1
3$ export DOCKER_CONTENT_TRUST_SERVER=https://localhost:4443

3.2 push的镜像无签名的情况

先pull一个测试用的镜像:

1$ docker pull ubuntu:18.04
2$ docker images ubuntu:18.04
3REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
4ubuntu              18.04               7698f282e524        12 days ago         69.9MB
5$ docker tag ubuntu:18.04 $REGISTRY/test/ubuntu:18.04

先把不使用notary签名的版本搞上去。

1$ export DOCKER_CONTENT_TRUST=0
2$ docker push $REGISTRY/test/ubuntu:18.04

本地删除镜像,然后分别用支持Notary和不支持notary的方式pull一下试试:

 1$ docker rmi -f 7698f282e524
 2$ export DOCKER_CONTENT_TRUST=1
 3$ docker pull $REGISTRY/test/ubuntu:18.04
 4Error: remote trust data does not exist for localhost:5000/test/ubuntu: localhost:4443 does not have trust data for localhost:5000/test/ubuntu
 5$ export DOCKER_CONTENT_TRUST=0
 6$ docker pull $REGISTRY/test/ubuntu:18.04
 718.04: Pulling from test/ubuntu
 86abc03819f3e: Pull complete 
 905731e63f211: Pull complete 
100bd67c50d6be: Pull complete 
11Digest: sha256:b36667c98cf8f68d4b7f1fb8e01f742c2ed26b5f0c965a788e98dfe589a4b3e4
12Status: Downloaded newer image for localhost:5000/test/ubuntu:18.04

可以看到如果用户上次的image没有被notary签名过,那么当客户端指定enable了content trust之后,是无法pull不可信的image的。

3.3 push有签名的版本

 1$ export DOCKER_CONTENT_TRUST=1
 2$ docker push $REGISTRY/test/ubuntu:18.04
 3The push refers to repository [localhost:5000/test/ubuntu]
 48d267010480f: Layer already exists 
 5270f934787ed: Layer already exists 
 602571d034293: Layer already exists 
 718.04: digest: sha256:b36667c98cf8f68d4b7f1fb8e01f742c2ed26b5f0c965a788e98dfe589a4b3e4 size: 943
 8Signing and pushing trust metadata
 9You are about to create a new root signing key passphrase. This passphrase
10will be used to protect the most sensitive key in your signing system. Please
11choose a long, complex passphrase and be careful to keep the password and the
12key file itself secure and backed up. It is highly recommended that you use a
13password manager to generate the passphrase and keep it safe. There will be no
14way to recover this key. You can find the key in your config directory.
15Enter passphrase for new root key with ID 74088e3: 
16Repeat passphrase for new root key with ID 74088e3: 
17Enter passphrase for new repository key with ID 5e28dca: 
18Repeat passphrase for new repository key with ID 5e28dca: 
19Finished initializing "localhost:5000/test/ubuntu"
20Successfully signed localhost:5000/test/ubuntu:18.04

可以看到push成功了之后会在给notary做一次签名。

此时客户端可以成功pull,不论CONTENT_TRUST是否enble,签名检查对用户是透明的。

使用以下命令可以撤销对image的签名:

1$ docker trust revoke $REGISTRY/test/ubuntu:18.04

3.4 恶意用户上传恶意镜像,覆盖ubuntu:18.04

先使用如下Dockerfile制作一个恶意image:

1FROM localhost:5000/test/ubuntu:18.04
2
3MAINTAINER black-hat
4RUN apt update && apt install -y sl
5CMD ["/usr/games/sl"]

制作image的命令:

1$ docker build -t ubuntu:evil .

这个镜像run起来之后会有一个小火车跑过:-)

我们切换一个账户,尝试用普通账户签名并且覆盖原先的ubuntu:18.04

 1$ su - test
 2$ export REGISTRY=localhost:5000
 3$ export DOCKER_CONTENT_TRUST=1
 4$ export DOCKER_CONTENT_TRUST_SERVER=https://localhost:4443
 5$ docker tag ubuntu:evil $REGISTRY/test/ubuntu:18.04
 6$ docker push $REGISTRY/test/ubuntu:18.04
 7The push refers to repository [localhost:5000/test/ubuntu]
 80f5d6ef7110f: Pushed 
 98d267010480f: Layer already exists 
10270f934787ed: Layer already exists 
1102571d034293: Layer already exists 
1218.04: digest: sha256:8ff78797f8ce02027d187f3a0c27502e134aa18163c62e110001e11b1a95b36d size: 1155
13Signing and pushing trust metadata
14ERRO[0002] couldn't add target to targets: could not find necessary signing keys, at least one of these keys must be available: 5e28dcaf218a140b6eeb8af239c2c44a2fbc7103d41759b5bc7aeaa3b7a5ec4e 
15failed to sign localhost:5000/test/ubuntu:18.04: could not find necessary signing keys, at least one of these keys must be available: 5e28dcaf218a140b6eeb8af239c2c44a2fbc7103d41759b5bc7aeaa3b7a5ec4e

可以看到由于这个用户由于没有合法的密钥,是无法给image做签名的。但是镜像上传成功了。这部分理论上应该有registry的身份认证拦截。

我们切换回root用户,把本地镜像删除了,禁用content trust之后再来试一次。

1$ sudo su
2$ docker rmi -f `docker images ubuntu:evil -q`
3$ docker rmi -f `docker images $REGISTRY/test/ubuntu -q`
4$ export DOCKER_CONTENT_TRUST=0
5$ docker run -ti $REGISTRY/test/ubuntu:18.04

可以看到小火车跑过,证明本地的image下载的是恶意的。
重新删除,然后再带Content Trust试一下。

1$ docker rmi -f `docker images $REGISTRY/test/ubuntu -q`
2$ export DOCKER_CONTENT_TRUST=1
3$ docker pull $REGISTRY/test/ubuntu:18.04
4## docker pull $REGISTRY/test/ubuntu:18.04
5No valid trust data for 18.04

可见这个image已经不被信任了,无法pull和运行非法image。

3.5 docker content trust的问题

  1. 镜像是先与Registry打交道后存储签名,如果是恶意image覆盖的问题,会导致恶意image上传上去了,但是签名没有更新/被删除的问题,导致image和notary签名数据不一致。需要加事务来解决?
  2. 密钥的存储必须与KMS结合起来,这样就不能直接使用docker+notary的解决方案了,预计要重新实现docker client的签名和验证功能?