Harbor集成Clair镜像安全扫描原理探知

在这里插入图片描述

上一篇文章中我们简单了解了Harbor集成Clair的安装方案及内网模式下CVE漏洞数据的手动导入功能。本篇文章,我们再梳理下漏洞扫描的具体原理和实现。

关于clair

Clair是CoreOS 2016年发布的一款开源容器漏洞扫描工具。该工具可以交叉检查Docker镜像的操作系统以及上面安装的任何包是否与任何已知不安全的包版本相匹配。漏洞是从特定操作系统的通用漏洞披露(CVE)数据库获取。

通过从镜像文件系统中抽取静态信息以及维护一个组成镜像的不同层之间的差异列表,可以大大减少分析时间,而且不需要实际运行可能存在漏洞的容器。如果镜像所依赖的一个靠下的层存在漏洞,那么该镜像就会被识别为有漏洞,而且,通过使用图存储,可以避免重新分析镜像。

clair的目标是能够从一个更加透明的维度去看待基于容器化的基础框架的安全性。Clair=clear + bright + transparent

Clair总体工作原理

在这里插入图片描述
Clair主要包括以下模块:
获取器(Fetcher)- 从公共源收集漏洞数据
检测器(Detector)- 指出容器镜像中包含的Feature
容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI
通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器
数据库(Databases)- 存储容器中各个层以及漏洞
Worker - 每个Post Layer都会启动一个worker进行Layer Detect

Clair整体处理流程如下:

  • Clair定期从配置的源获取漏洞元数据然后存进数据库。
  • 客户端使用Clair API处理镜像,获取镜像的特征并存进数据库。
  • 客户端使用Clair API从数据库查询特定镜像的漏洞情况,为每个请求关联漏洞和特征,避免需要重新扫描镜像。
  • 当更新漏洞元数据时,将会有系统通知产生。另外,还有webhook用于配置将受影响的镜像记录起来或者拦截其部署。

Clair数据库一览
我们登陆clair-db容器,然后切换postgres账户,使用psql命令连接当前数据库,查看当前有的数据库和postgres库中所有的数据库表,我们查看keyvaule这张表,可以看到这张表记录了上一次在各开源社区同步CVE数据的状态点,比如key为alpine-secdbUpdater的数据记录的是上一次同步alpine漏洞状态, value为bc60eddeaadece7bc6014a0b01ac69ca176cb370指的是git commit,其中的updater/last记录的是系统上一次CVE全部同步完成时间,Clair 结合系统配置参数interval来控制同步CVE漏洞数据的执行时间。

[root@bogon lanjian]# docker exec -it clair-db /bin/bash
root [ / ]# su postgres
postgres [ / ]$ psql
psql (9.6.8)
Type "help" for help.

postgres=# \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

postgres=# \d
                             List of relations
 Schema |                    Name                     |   Type   |  Owner   
--------+---------------------------------------------+----------+----------
 public | feature                                     | table    | postgres
 public | feature_id_seq                              | sequence | postgres
 public | featureversion                              | table    | postgres
 public | featureversion_id_seq                       | sequence | postgres
 public | keyvalue                                    | table    | postgres
 public | keyvalue_id_seq                             | sequence | postgres
 public | layer                                       | table    | postgres
 public | layer_diff_featureversion                   | table    | postgres
 public | layer_diff_featureversion_id_seq            | sequence | postgres
 public | layer_id_seq                                | sequence | postgres
 public | lock                                        | table    | postgres
 public | lock_id_seq                                 | sequence | postgres
 public | namespace                                   | table    | postgres
 public | namespace_id_seq                            | sequence | postgres
 public | schema_migrations                           | table    | postgres
 public | vulnerability                               | table    | postgres
 public | vulnerability_affects_featureversion        | table    | postgres
 public | vulnerability_affects_featureversion_id_seq | sequence | postgres
 public | vulnerability_fixedin_feature               | table    | postgres
 public | vulnerability_fixedin_feature_id_seq        | sequence | postgres
 public | vulnerability_id_seq                        | sequence | postgres
 public | vulnerability_notification                  | table    | postgres
 public | vulnerability_notification_id_seq           | sequence | postgres
(23 rows)

postgres=# SELECT *     
postgres-# FROM keyvalue
postgres-# ;
 id |         key         |                  value                   
----+---------------------+------------------------------------------
  1 | alpine-secdbUpdater | bc60eddeaadece7bc6014a0b01ac69ca176cb370
  4 | rhelUpdater         | 20193136
  5 | updater/last        | 1571570148
  3 | oracleUpdater       | 20194823
  2 | debianUpdater       | b99e9a950e549d1d150f1218b722ca69cc250348
(5 rows)

从各开源社区获取CVE数据之后,Clair会进行数据解析,将各社区CVE数据统一结构,放到数据库中,每一个漏洞都是一个database.Vulnerability对象(涉及多张数据库表)

v := &database.Vulnerability{
  Severity: database.UnknownSeverity,
  Name: "CVE-2018-19044",
  Link: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19044",
  FixedIn: []database.FeatureVersion{
    {
      Feature: database.Feature{
        Namespace: database.Namespace{
          Name:          "alpine:v3.9"
          VersionFormat: "pkg",
        },
        Name: "keepalived",
      },
      Version: "2.0.11-r0",
    },
  },
}

其中Severity为漏洞风险等级,Vuluerability.Name为CVE-ID,Link为CVE详情链接,Namespace.Name为操作系统,VersionFormat软件管理包, 如centos为rpm,alpine为pkg,Feature.Name为软件包名字,Version为软件包版本。这个结构涉及的几张表结构及数据一览:

postgres=# \d vulnerability
                                     Table "public.vulnerability"
    Column    |           Type           |                         Modifiers                          
--------------+--------------------------+------------------------------------------------------------
 id           | integer                  | not null default nextval('vulnerability_id_seq'::regclass)
 namespace_id | integer                  | not null
 name         | character varying(128)   | not null
 description  | text                     | 
 link         | character varying(128)   | 
 severity     | severity                 | not null
 metadata     | text                     | 
 created_at   | timestamp with time zone | 
 deleted_at   | timestamp with time zone | 
Indexes:
    "vulnerability_pkey" PRIMARY KEY, btree (id)
    "vulnerability_name_idx" btree (name)
    "vulnerability_namespace_id_name_idx" btree (namespace_id, name)
Foreign-key constraints:
    "vulnerability_namespace_id_fkey" FOREIGN KEY (namespace_id) REFERENCES namespace(id)
Referenced by:
    TABLE "vulnerability_affects_featureversion" CONSTRAINT "vulnerability_affects_featureversion_vulnerability_id_fkey" FOREIGN KEY (vulnerability_id) REFERENCES vulnerability(id) ON DELETE CASCADE
    TABLE "vulnerability_fixedin_feature" CONSTRAINT "vulnerability_fixedin_feature_vulnerability_id_fkey" FOREIGN KEY (vulnerability_id) REFERENCES vulnerability(id) ON DELETE CASCADE
    TABLE "vulnerability_notification" CONSTRAINT "vulnerability_notification_new_vulnerability_id_fkey" FOREIGN KEY (new_vulnerability_id) REFERENCES vulnerability(id) ON DELETE CASCADE
    TABLE "vulnerability_notification" CONSTRAINT "vulnerability_notification_old_vulnerability_id_fkey" FOREIGN KEY (old_vulnerability_id) REFERENCES vulnerability(id) ON DELETE CASCADE

postgres=# SELECT * FROM vulnerability LIMIT 5 ;
 id | namespace_id |      name      | description |                             link                              | severity | metadata |          created
_at           | deleted_at 
----+--------------+----------------+-------------+---------------------------------------------------------------+----------+----------+-----------------
--------------+------------
  1 |            1 | CVE-2016-9941  |             | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-9941  | Unknown  | null     | 2019-10-20 11:04
:07.292302+00 | 
  2 |            2 | CVE-2017-15186 |             | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15186 | Unknown  | null     | 2019-10-20 11:04
:07.316191+00 | 
  3 |            3 | CVE-2017-14107 |             | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14107 | Unknown  | null     | 2019-10-20 11:04
:07.324011+00 | 
  4 |            3 | CVE-2017-7529  |             | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7529  | Unknown  | null     | 2019-10-20 11:04
:07.329613+00 | 
  5 |            4 | CVE-2017-2615  |             | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-2615  | Unknown  | null     | 2019-10-20 11:04
:07.332934+00 | 

(5 rows)

postgres=# \d featureversion
                                   Table "public.featureversion"
   Column   |          Type          |                          Modifiers                          
------------+------------------------+-------------------------------------------------------------
 id         | integer                | not null default nextval('featureversion_id_seq'::regclass)
 feature_id | integer                | not null
 version    | character varying(128) | not null
 
postgres=# SELECT * FROM featureversion LIMIT 5 ;
 id | feature_id |      version      
----+------------+-------------------
  1 |      27164 | 0.5.10.2-5
  2 |      27165 | 2.7-2build2
  3 |      27166 | 4.7-1
  4 |      11517 | 1.19.0.5ubuntu2.1
  5 |       5575 | 8.3.0-6
(5 rows)

postgres=# \d feature                            
                                    Table "public.feature"
    Column    |          Type          |                      Modifiers                       
--------------+------------------------+------------------------------------------------------
 id           | integer                | not null default nextval('feature_id_seq'::regclass)
 namespace_id | integer                | not null
 name         | character varying(128) | not null


postgres=# SELECT * FROM feature LIMIT 5 ;       
 id | namespace_id |     name     
----+--------------+--------------
  1 |            1 | libvncserver
  2 |            2 | ffmpeg
  3 |            3 | libzip
  4 |            3 | nginx
  5 |            4 | qemu
(5 rows)

postgres=# \d namespace
                                     Table "public.namespace"
     Column     |          Type          |                       Modifiers                        
----------------+------------------------+--------------------------------------------------------
 id             | integer                | not null default nextval('namespace_id_seq'::regclass)
 name           | character varying(128) | 
 version_format | character varying(128) | 

postgres=# SELECT * FROM namespace LIMIT 5 ;       
 id |     name     | version_format 
----+--------------+----------------
  1 | alpine:v3.10 | dpkg
  2 | alpine:v3.5  | dpkg
  3 | alpine:v3.6  | dpkg
  4 | alpine:v3.8  | dpkg
  5 | alpine:v3.4  | dpkg
(5 rows)

用通俗的一句话概括,就是找到每个镜像文件系统中已经安装的软件包与版本,然后跟官方系统公布的信息比对,官方已经给出了在哪个系统版本上哪个软件版本有哪些漏洞,比如Debian 7系统上,nginx 1.12.1有哪些CVE漏洞,通过对逐个安装的软件包比对,就能知道当前这个镜像一共有多少CVE。1

理解docker镜像

我们知道,容器是一个动态的环境,每一层镜像中的文件属于静态内 容,然而 Dockerfile 中的 ENV、VOLUME、CMD 等内容最终都需要落实到容器的运行环境中,而这些内容均不可能直接坐落到每一层镜像所包含的文件系统内容中,那此时每一个Docker镜像还会包含 json文件记录与容器之间的关系。可以通过docker history命令查看镜像的每一层。2

在这里插入图片描述
插个题外话,关于docker镜像和docker容器的关系,这段描述很清晰:

那么作为静态的镜像,如何才有能力转化为一个动态的Docker容器呢?此时,我们可以想象:第一,转化的依据是什么;第二,由谁来执行这个转化操作。
转化的依据就是每个镜像的json文件,Docker可以通过解析Docker镜像的json的文件,获知应该在这个镜像之上运行什么样的进程,应该为进程配置怎么样的环境变量,此时也就实现了静态向动态的转变。
谁来执行这个转化工作?答案是Docker守护进程。也许大家早就理解这样一句 话:Docker容器实质上就是一个或者多个进程,而容器的父进程就是Docker守护进程。这样的,转化工作的执行就不难理解了:Docker守护进程 手握Docker镜像的json文件,为容器配置相应的环境,并真正运行Docker镜像所指定的进程,完成Docker容器的真正创建。
Docker容器运行起来之后,Docker镜像json文件就失去作用了。此时Docker镜像的绝大部分作用就是:为Docker容器提供一个文件系统的视角,供容器内部的进程访问文件资源

总的来说,docker镜像就是由许多Layer层组成的文件系统,重要的是每个镜像有一个manifest。也就相当于文件清单。一个镜像需要这个manifest文件来记录下到底由哪几个层联合组成的。

Harbor+Clair扫描原理

以下部分总体参考Harbor仓库镜像扫描原理(作者:kingfsen的)这篇文章的每一步来写,因为作者已经将总体步骤已经说明了,我会再细化每一步来探知具体的实现细节。

要扫描分析一个镜像,首先你就必须获取到这个镜像的manifest文件,通过manifest文件获取到镜像所有的Layer的地址digest,digest在docker镜像存储系统中代表的是一个地址,类似操作系统中的一个内存地址概念,通过这个地址,可以找到文件的内容,这种可寻址的设计是v2版本的重大改变。在docker hub储存系统中,所有文件都是有地址的,这个digest就是由某种高效的sha算法通过对文件内容计算出来的。

在这里插入图片描述
上图中虚线框中的模块是harbor自身功能,Clair是coreos开源的一个系统,镜像扫描分析工作主要由Clair完成,它具体的结构在下面再分析,这里先侧重分析harbor这块流程。箭头方向大致描述的是请求方向,系统之间交互可能产生多次请求。

1.UI向Job发起镜像扫描请求,参数中包含了仓库名称以及tag

我们看下ui容器的日志,可以看到UI请求了一个扫描的API:/api/repositories/library/nginx/tags/latest/scan,仓库名称:library,tag是latest。

[root@localhost harbor]# cat ui.log|tail -n 100 | grep 'scan'
Oct 21 18:57:22 172.19.0.1 ui[7151]: 2019/10/21 10:57:22 #033[1;44m[D] [server.go:2619] |  192.168.137.1|#033[42m 200 #033[0m| 246.416022ms|   match|#033[46m POST    #033[0m /api/repositories/library/nginx/tags/latest/scan   r:/api/repositories/*/tags/:tag/scan#033[0m
Oct 21 18:57:23 172.19.0.1 ui[7151]: 2019/10/21 10:57:23 #033[1;44m[D] [server.go:2619] |     172.19.0.8|#033[42m 200 #033[0m|  19.943236ms|   match|#033[46m POST    #033[0m /service/notifications/jobs/scan/28   r:/service/notifications/jobs/scan/:id([0-9]+)#033[0m
Oct 21 18:57:23 172.19.0.1 ui[7151]: 2019/10/21 10:57:23 #033[1;44m[D] [server.go:2619] |     172.19.0.8|#033[42m 200 #033[0m|  10.103443ms|   match|#033[46m POST    #033[0m /service/notifications/jobs/scan/28   r:/service/notifications/jobs/scan/:id([0-9]+)#033[0m

2.Job收到请求之后,向registry发起一个Head请求(/v2/nginx/manifest/v1.12.1),判断当前镜像的manifest是否存在,取出当前manifest的digest,这个digest是存放在响应头中的Docker-Content-Digest。

我们来看registry的日志中的请求数据,

[root@localhost harbor]# cat registry.log |tail -n 100|grep 'manifest' 
time = "2019-10-21T10:57:14.830778194Z"
level = info
msg = "response completed"
go.version = go1.7.3
http.request.host = "registry:5000"
http.request.id = 78e58185-7ee4-42b3-b0e5-f8801c27b45b
http.request.method = GET
http.request.remoteaddr = "172.19.0.7:59678"
http.request.uri = "/v2/library/nginx/manifests/latest"
http.request.useragent = "Go-http-client/1.1"
http.response.contenttype = "application/vnd.docker.distribution.manifest.v2+json"
http.response.duration = 9.624286 ms
http.response.status = 200
http.response.written = 948
instance.id = 61406ee1-9106-4ac2-86f1-1ea6a864a5ee 
service = registry
version = v2 .6 .2

可以看到拿到了digest值:sha256:5a9061639d0aeca4b13f8e18b985eea79e55168969d069febdb6723993ebba7d

Oct 21 18:57:24 172.19.0.1 registry[7151]: time="2019-10-21T10:57:24.139675734Z" level=info msg="response completed" go.version=go1.7.3 http.request.host="registry:5000" http.request.id=238b5122-b269-400e-a60c-b91e546ab01c http.request.method=GET http.request.remoteaddr="172.19.0.7:59678" http.request.uri="/v2/library/nginx/blobs/sha256:5a9061639d0aeca4b13f8e18b985eea79e55168969d069febdb6723993ebba7d" http.request.useragent="Go-http-client/1.1" http.response.contenttype="application/octet-stream" http.response.duration=3.054639ms http.response.status=200 http.response.written=6669 instance.id=61406ee1-9106-4ac2-86f1-1ea6a864a5ee service=registry version=v2.6.2 
Oct 21 18:57:24 172.19.0.1 registry[7151]: 172.19.0.7 - - [21/Oct/2019:10:57:24 +0000] "GET /v2/library/nginx/blobs/sha256:5a9061639d0aeca4b13f8e18b985eea79e55168969d069febdb6723993ebba7d HTTP/1.1" 200 6669 "" "Go-http-client/1.1"

3.Job把第2步获取到的digest以及仓库名、tag作为一条记录插入job表中,job的状态为pending。 这个时候Job系统则会新建一个扫描任务的job进行调度,这里则涉及到一个状态机处理流程。

可以看下jobservice的日志,可以看到创建了一个job并入队调度

[root@localhost harbor]# cat jobservice.log |tail -n 50
Oct 21 18:57:22 172.19.0.1 jobservice[7151]: 2019-10-21T10:57:22Z [INFO] Receive event 'register_hook' with data(unformatted): map[string]interface {}{"job_id":"db08dd8de71797608ac5cbf7", "hook_url":"http://ui:8080/service/notifications/jobs/scan/28"}
Oct 21 18:57:23 172.19.0.1 jobservice[7151]: 2019-10-21T10:57:23Z [INFO] Job incoming: IMAGE_SCAN:db08dd8de71797608ac5cbf7
Oct 21 18:57:23 172.19.0.1 jobservice[7151]: [mysql] 2019/10/21 10:57:23 packets.go:130: write tcp 172.19.0.8:54724->172.19.0.6:3306: write: broken pipe
Oct 21 18:57:23 172.19.0.1 jobservice[7151]: [mysql] 2019/10/21 10:57:23 packets.go:130: write tcp 172.19.0.8:54724->172.19.0.6:3306: write: broken pipe
Oct 21 18:57:23 172.19.0.1 jobservice[7151]: 2019-10-21T10:57:23Z [INFO] Job 'IMAGE_SCAN:db08dd8de71797608ac5cbf7' exit with success

4.Job系统通过manifest文件获取镜像的所有Layer digest,针对每一层,封装一个ClairLayer参数对象,然后根据层的数量,循环请求Clair系统(即调用Clair的API),ClairLayer参数结构如下:

Name:    sha256:7d99455a045a6c89c0dbee6e1fe659eb83bd3a19e171606bc0fd10eb0e34a7dc
Headers: tokenHeader,
Format:  "Docker",
Path:    http://registry:5000/v2/nginx/blobs/7d99455a045a6c89c0dbee6e1fe659eb83bd3a19e171606bc0fd10eb0e34a7dc
ParentName: a55bba68cd4925f13c34562c891c8c0b5d446c7e3d65bf06a360e81b993902e1

这里补充说明下Clair的API,具体可以参考Clair的API文档
示例请求:将指定镜像的每一层按照顺序push到clair中

POST http://localhost:6060/v1/layers HTTP/1.1
{
 "Layer": {
   "Name": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6",
   "Path": "https://mystorage.com/layers/523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6/layer.tar",
   "Headers": {
     "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiY
     WRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGu
     ERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE"
   },
   "ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2",
   "Format": "Docker"
 }
}

官网说明:
为了分析容器图像,必须按照正确的顺序push某个镜像的所有层。例如,要分析由三层A-> B-> C组成的图像,其中A是基础层,必须对此路径进行三次API调用,从A到C.此外,在分析B时,必须将A设置为父级,然后在分析C时,必须将B定义为父级。

上述ClairLayer参数结构中字段的解释说明如下3

Name:层id,或为一个不与其他图层id重复的字符串
Path:位图层的下载地址
Authorization:可选值,可选,其内容将在通过HTTP请求图层时填充Authorization HTTP Header。
ParentName:父级id
Format:固定为Docker

所以根据层数循环调用Clair的API的方法我们也就明确了Habor的Job的执行步骤:
(1)查看镜像的层级关系

vi manifest.json
[{
	"Config": "e445ab08b2be8b178655b714f89e5db9504f67defd5c7408a00bade679a50d44.json",
	"RepoTags": ["nginx:latest"],
	"Layers": [
	  			"08029cfa8b0dcfe678e23255feef9c9f1a08c39ebf6faffc4e5ee8b6ec63ff1f/layer.tar", 
			  	"656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81/layer.tar", 
			  	"22e7e6fdc1e671b8f4d55efa2e707cf4ac35e34d757efc2b43c219be7a800ae4/layer.tar"
			  ]
}]

其中08029为最底层,656e为中间层,22e7为最上层。
(2)发起API调用请求

  • Push最底层
post

http://192.168.2.186:6060/v1/layers

{
  "Layer": {
    "Name": "08029cfa8b0dcfe678e23255feef9c9f1a08c39ebf6faffc4e5ee8b6ec63ff1f",
    "Path": "http://localhost:8080/08029cfa8b0dcfe678e23255feef9c9f1a08c39ebf6faffc4e5ee8b6ec63ff1f/layer.tar",
    "Format": "Docker"
  }
}
  • Push中间层
post

http://192.168.2.186:6060/v1/layers

{
  "Layer": {
    "Name": "656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81",
    "Path": "http://localhost:8080/656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81/layer.tar",
    "ParentName": "08029cfa8b0dcfe678e23255feef9c9f1a08c39ebf6faffc4e5ee8b6ec63ff1f",
    "Format": "Docker"
  }
}
  • Push最上层
post

http://192.168.2.186:6060/v1/layers

{
  "Layer": {
    "Name": "22e7e6fdc1e671b8f4d55efa2e707cf4ac35e34d757efc2b43c219be7a800ae4",
    "Path": "http://localhost:8080/22e7e6fdc1e671b8f4d55efa2e707cf4ac35e34d757efc2b43c219be7a800ae4/layer.tar",
    "ParentName": "656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81",
    "Format": "Docker"
  }
}

5.Clair系统收到请求之后,根据ParentName首先校验父Layer是否存在,不存在则报错。存在则开始探测layer的操作系统和软件包的版本信息

(1)下载镜像层文件

携带必要的权限headers发起对path的Get请求,得到的则是一个tar归档文件,然后进行解压

(2)探测镜像操作系统

遍历解压后的文件目录,探测操作系统文件路径。 首先要了解各Linux发行版的一些基础文件,比如系统版本、安装的软件包版本记录等文件。

centos:etc/os-release,usr/lib/os-release

查看文件/etc/os-release

NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

clair逐行解析该文件,提取ID以及VERSION_ID字段,最终把centos:7作为clair中的一个namespace概念。

(3)探测镜像已安装的软件包

上一步已经探测到了操作系统,自然可以知道系统的软件管理包是rpm还是dpkg。

debian, ubuntu : dpkg
centos, rhel, fedora, amzn, ol, oracle : rpm

centos系统的软件管理包是rpm,而debain系统的软件管理是dpkg。下面是不同软件管理包的目录:

rpm:var/lib/rpm/Packages
dpkg:var/lib/dpkg/status
apk:lib/apk/db/installed

比如debian系统,从文件/var/lib/dpkg/status文件则可以探测到当前系统安装了哪些版本的软件。

Package: sed
Essential: yes
Status: install ok installed
Priority: required
Section: utils
Installed-Size: 799
Maintainer: Clint Adams <[email protected]>
Architecture: amd64
Multi-Arch: foreign
Version: 4.4-1
Pre-Depends: libc6 (>= 2.14), libselinux1 (>= 1.32)
Description: GNU stream editor for filtering/transforming text
 sed reads the specified files or the standard input if no
 files are specified, makes editing changes according to a
 list of commands, and writes the results to the standard
 output.
Homepage: https://www.gnu.org/software/sed/

Package: libsmartcols1
Status: install ok installed
Priority: required
Section: libs
Installed-Size: 257
Maintainer: Debian util-linux Maintainers <[email protected]>
Architecture: amd64
Multi-Arch: same
Source: util-linux
Version: 2.29.2-1+deb9u1
Depends: libc6 (>= 2.17)
Description: smart column output alignment library
 This smart column output alignment library is used by fdisk utilities.

逐行解析文件,提取Package以及Version字段,最终获取 libsmartcols1 2.29-1+deb9u1以及sed等

(4)保存信息

把上面探测到的系统版本、以及系统上安装的各种软件包版本都存入数据库。

6.Clair将layer的版本特征feature数据和已经获取的公开漏洞CVE数据进行比对匹配

Clair系统已经获取了各linux版本操作系统软件版本,以及对应软件版本存在的CVE。官方公布了某个软件在某个版本修复了哪个CVE,Clair只需要将当前镜像中的软件的版本与官方公布的版本进行比较。比如官方维护的CVE信息中公布了nginx 1.13.1修复了漏洞CVE-2015-10203,那么当前镜像中包含的版本为1.12.1的nginx必然存在漏洞CVE-2015-10203,这些版本比较都是基于同一个版本的操作系统之上比较的。

7.返回镜像漏洞扫描结果

Harbor的Job系统发送完最后一层的请求之后,则会发起一个CVE分析结果的GET请求查询,生成一个特征feature和漏洞vulnerabilities扫描结果的概览保存在数据库中,主要是记录当前镜像发现了高风险漏洞多少个,中度风险多少个等。同时把job表中的状态设置为finished,如果请求Clair发生任何错误,则会把job记录设置为error。harbor页面具体的漏洞详细数据展示,还是通过UI系统调用Clair系统实时查询。

get

http://192.168.2.186:6060/v1/layers/22e7e6fdc1e671b8f4d55efa2e707cf4ac35e34d757efc2b43c219be7a800ae4?features&vulnerabilities

获得的响应如下:

{
    "Layer": {
        "Name": "22e7e6fdc1e671b8f4d55efa2e707cf4ac35e34d757efc2b43c219be7a800ae4",
        "NamespaceName": "debian:10",
        "ParentName": "656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81",
        "IndexedByVersion": 3,
        "Features": [
            {
                "Name": "libgd2",
                "NamespaceName": "debian:10",
                "VersionFormat": "dpkg",
                "Version": "2.2.5-5.2",
                "AddedBy": "656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81"
            },
            {
                "Name": "openssl",
                "NamespaceName": "debian:10",
                "VersionFormat": "dpkg",
                "Version": "1.1.1c-1",
                "AddedBy": "656e37151781143bab1be21edfbe2de2251ffe28e8d2004e83cd79a7a76d0b81"
            },
            {
                "Name": "util-linux",
                "NamespaceName": "debian:10",
                "VersionFormat": "dpkg",
                "Version": "2.33.1-0.1",
                "AddedBy": "08029cfa8b0dcfe678e23255feef9c9f1a08c39ebf6faffc4e5ee8b6ec63ff1f"
            },
            ...........
        ]
    }
}

在这里插入图片描述
Clair漏洞等级说明如下:

  • Unknown
    社区暂未给出优先级或者Clair当前未同步支持

  • Negligible
    几乎无任何影响的漏洞,不会单独给出一个针对性的修复更新版本 Low 低风险等级,一般会在更高风险等级漏洞修复版本中修复

  • Medium
    中风险等级,如跨域、用户权限暴露等,会有针对性版本修复

  • High
    高风险等级,漏洞在大多数人默认的安装版本中出现,如服务出错、数据丢失、root权限恶意获取等

  • Critical
    最高风险等级,几乎所有版本都存在某漏洞,如造成用户大量数据丢失

注:
1.Clair可以确定是否存在可能有漏洞的包,但不能验证它们实际上是否已被利用。另外,它们也无法检测运行实例中的动态行为,比如在运行时安装有漏洞的包版本。
2.由于Clair会根据CVE库扫是Docker镜像使用的内核,但是实际上容器使用的是宿主的内核,这样可能产生大量无用漏洞或者误报,但是根据Clair开发组的意思,他们把决定权交给用户,默认不提供白名单机制,也不对此做区分。

clair-db获取CVE源策略4

直接翻阅君无止境的另外一篇文章的内容:http://youendless.com/post/cve/
介绍的很详细。

参考资料:

  1. Harbor仓库镜像扫描原理,作者:君无止境 kingfsen ↩︎

  2. Docker的镜像理解以及容器的备份、恢复和迁移操作 ↩︎

  3. 【Docker】镜像安全扫描工具clair与clairctl,作者:SunAlwaysOnline ↩︎

  4. Docker镜像扫描之CVE漏洞数据源,作者:君无止境 kingfsen ↩︎

发布了25 篇原创文章 · 获赞 24 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/arnolan/article/details/102666831