CLOVER🍀

That was when it all began.

Clairで、Dockerイメージの脆弱性スキャンを試す

これは、なにをしたくて書いたもの?

  • Dockerイメージの脆弱性スキャンを実行できるツールについて、ちょっと調べてみて
  • Clairというものが良さそうだったので、まずはこちらを試してみようと

GitHub - coreos/clair: Vulnerability Static Analysis for Containers

Clairとは?

CoreOS社が開発している、コンテナイメージの脆弱性スキャンツールです。

GitHub - coreos/clair: Vulnerability Static Analysis for Containers

以下のような、特徴を持ちます。

  • Clairに設定された一連のデータソースから、脆弱性に関するメタデータをデータベースに取り込む
  • データソースは複数設定可能で、メタデータは定期的に更新される
  • APIでスキャンを実行でき、またWebhookの機能も備える(CI/CDで利用)

ビルトインされているデータソースには、以下のものがあります。

Data Sources for the built-in drivers

Debian Security Bug Tracker、Ubuntu CVE Tracker、Red Hat Security Data、Oracle Linux Security Data、SUSE OVAL Descriptions、
Alpine SecDB、NIST NVDですね。

基本的な用語は、こちらを参照。

clair/terminology.md at master · coreos/clair · GitHub

clair/drivers-and-data-sources.md at master · coreos/clair · GitHub

参考)

10+ top open-source tools for Docker security | TechBeacon

セキュアなDockerイメージを支援するClair

Dockerイメージの脆弱性診断をPortus & Clairで - Speaker Deck

Clair によるコンテナ・イメージの脆弱性検出 第1回 | オブジェクトの広場

Clair によるコンテナ・イメージの脆弱性検出 第2回 | オブジェクトの広場

Clair によるコンテナ・イメージの脆弱性検出 第3回 | オブジェクトの広場

Clairを起動してみる

では、まずはClairを起動してみましょう。

手順は、こちら。

clair/running-clair.md at master · coreos/clair · GitHub

Clair自体はDockerイメージが提供されているようなので、今回はDocker Composeを使って起動する方法を取ってみます。
なお、データベースとしてはPostgreSQLが必要です(設定ファイルの構文的には、他のものも書けそうな雰囲気があるものの、
現状はPostgreSQLに対する実装しかありません)。

手順に従って、docker-compose.ymlをダウンロード。

$ wget https://raw.githubusercontent.com/coreos/clair/master/contrib/compose/docker-compose.yml

中を見ると、割とlatest指定が多く…。

version: '2'
services:
  postgres:
    container_name: clair_postgres
    image: postgres:latest
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: password

  clair:
    container_name: clair_clair
    image: quay.io/coreos/clair-git:latest
    restart: unless-stopped
    depends_on:
      - postgres
    ports:
      - "6060-6061:6060-6061"
    links:
      - postgres
    volumes:
      - /tmp:/tmp
      - ./clair_config:/config
    command: [-config, /config/config.yaml]

というか、latest(とmaster)しかない?

quay.io / coreos / clair-git / tags

ここで、提供されているイメージの種類に関する説明を読むと、ちゃんと書いてありました。

Official Container Repositories

  • clair … Stable releases
  • clair-jwt … Stable releases with an embedded instance of jwtproxy
  • clair-git … Development releases

clair-gitは、開発中のものですと…。だから、latestしかないんですね…。

今回は、clair:v2.0.7を使うことにします。

quay.io / coreos / clair / tags

修正後のdocker-compose.yml

version: '2'
services:
  postgres:
    container_name: clair_postgres
    image: postgres:11.1
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: password

  clair:
    container_name: clair_clair
    image: quay.io/coreos/clair:v2.0.7
    restart: unless-stopped
    depends_on:
      - postgres
    ports:
      - "6060-6061:6060-6061"
    links:
      - postgres
    volumes:
      - ./:/etc/clair/
    # command: [-config, /etc/clair/config.yaml]  ## default "/etc/clair/config.yaml"

volumesでカレントディレクトリを「/etc/clair」にマウントするようにしていますが、これはClairの起動には設定ファイルが
必要で、デフォルトの読み込み先は「/etc/clair」だからです(「-config」オプションで指定することもできます)。

$ docker container run -it --rm --name clair quay.io/coreos/clair:v2.0.7 -h
Usage of /clair:
  -config string
        Load configuration from the specified file. (default "/etc/clair/config.yaml")
  -cpu-profile string
        Write a CPU profile to the specified file before exiting.
  -insecure-tls
        Disable TLS server's certificate chain and hostname verification when pulling layers.
  -log-level string
        Define the logging level. (default "info")

というわけで、一緒に設定ファイルが必要なので、合わせて取得。

$ wget https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -O config.yaml

こちらも、ドキュメントに習って修正。PostgreSQLへの接続設定(「source」)を変える必要があります。
config.yaml

clair:
  database:
    # Database driver
    type: pgsql
    options:
      # PostgreSQL Connection string
      # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
      source: postgresql://postgres:password@postgres:5432?sslmode=disable&statement_timeout=60000
      cachesize: 16384
      paginationkey: 

  api:
    addr: "0.0.0.0:6060"

    # Health server address
    # This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
    healthaddr: "0.0.0.0:6061"

    # Deadline before an API request will respond with a 503
    timeout: 900s

    # Optional PKI configuration
    # If you want to easily generate client certificates and CAs, try the following projects:
    # https://github.com/coreos/etcd-ca
    # https://github.com/cloudflare/cfssl
    servername:
    cafile:
    keyfile:
    certfile:

  updater:
    # Frequency the database will be updated with vulnerabilities from the default data sources
    # The value 0 disables the updater entirely.
    interval: 2h
    enabledupdaters: 
      - debian
      - ubuntu
      - rhel
      - oracle
      - alpine

  notifier:
    # Number of attempts before the notification is marked as failed to be sent
    attempts: 3

    # Duration before a failed notification is retried
    renotifyinterval: 2h

    http:
      # Optional endpoint that will receive notifications via POST requests
      endpoint:

      # Optional PKI configuration
      # If you want to easily generate client certificates and CAs, try the following projects:
      # https://github.com/cloudflare/cfssl
      # https://github.com/coreos/etcd-ca
      servername:
      cafile: 
      keyfile: 
      certfile: 

      # Optional HTTP Proxy: must be a valid URL (including the scheme).
      proxy:

設定ファイルの内容は、以下の内容で構成されているようですね。

  • 脆弱性情報などを格納するデータベース(PostgreSQL)に関する設定
  • Clairとして提供するAPIに関する設定
  • 脆弱性情報のデータソースと、その更新頻度に関する設定
  • Webhookに関する設定

APIとしての設定は、こちらですね。Clairを、6060ポートをリッスンさせるように起動していることがわかります。

  api:
    addr: "0.0.0.0:6060"

    # Health server address
    # This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
    healthaddr: "0.0.0.0:6061"

    # Deadline before an API request will respond with a 503
    timeout: 900s

    # Optional PKI configuration
    # If you want to easily generate client certificates and CAs, try the following projects:
    # https://github.com/coreos/etcd-ca
    # https://github.com/cloudflare/cfssl
    servername:
    cafile:
    keyfile:
    certfile:

この「enabledupdaters」で指定されているのが脆弱性に関するデータソースなようですが、

  updater:
    # Frequency the database will be updated with vulnerabilities from the default data sources
    # The value 0 disables the updater entirely.
    interval: 2h
    enabledupdaters: 
      - debian
      - ubuntu
      - rhel
      - oracle
      - alpine

指定できそうなのはこのあたりでしょうかね。

https://github.com/coreos/clair/tree/master/ext/vulnsrc

NIST NVDがないのでは?と思うかもしれませんが、こちらはデータソースからの情報取得後、追加情報として収集されるようです…。

https://github.com/coreos/clair/tree/master/ext/vulnmdsrc

更新間隔は、今回はサンプルのままなので2時間…。

起動。

$ docker-compose up

起動すると、データソースからの情報の取得が始まります。

これにはけっこう時間がかかり、次のようなログが出力されると完了、となります(update finished)。

clair_1     | {"Event":"finished fetching","Level":"info","Location":"updater.go:242","Time":"2019-01-12 09:54:32.348079","updater name":"alpine"}
clair_1     | {"Event":"finished fetching","Level":"info","Location":"updater.go:242","Time":"2019-01-12 09:54:33.349210","updater name":"debian"}
clair_1     | {"Event":"could not parse package version. skipping","Level":"warning","Location":"ubuntu.go:316","Time":"2019-01-12 09:55:47.918274","error":"invalid version","version":"0.27-1+deb7u1build0.12.04.1, 0.28-1+deb8u1"}
clair_1     | {"Event":"finished fetching","Level":"info","Location":"updater.go:242","Time":"2019-01-12 09:55:48.706536","updater name":"ubuntu"}
clair_1     | {"Event":"finished fetching","Level":"info","Location":"updater.go:242","Time":"2019-01-12 10:03:25.997211","updater name":"oracle"}
clair_1     | {"Event":"finished fetching","Level":"info","Location":"updater.go:242","Time":"2019-01-12 10:04:31.010574","updater name":"rhel"}
clair_1     | {"Event":"adding metadata to vulnerabilities","Level":"info","Location":"updater.go:268","Time":"2019-01-12 10:04:31.023454"}
clair_1     | {"Event":"update finished","Level":"info","Location":"updater.go:213","Time":"2019-01-12 10:27:41.691657"}

つまり、10分くらい待とう、と。

そこまで経過したら、準備は完了となります。

ところで、関連するGitHubリソースのリンク先にずっとmasterブランチを指定しているのが微妙なのですが、最近ディレクトリ構成が
変わったようで、最新安定版のタグだと今のmasterと全然違う形なので、まあ…参考程度に…くらいで…。

あと、ClairのDockerイメージが置かれていたQuay.io。こちらもDocker Hubと同じくコンテナのレジストリサービスなのですが、
CVEを元にしたセキュリティスキャンも行ってくれるサービスのようです。

Quay

無料プランでも、セキュリティスキャンは利用できるのだとか。

確認してみる

それでは、コンテナイメージをスキャンして確認してみたいと思います。

スキャン方法なのですが、以前は「analyze-local-images」というツールが使われていたようなのですが、今は非推奨と
なっています。

GitHub - coreos/analyze-local-images: deprecated tool for interacting with Clair locally

ではどうするか?ですが、今はclairctlかClair scannerを使うようです。

Clair Usage - deprecated · Issue #1 · coreos/analyze-local-images · GitHub

security - What tool is able to analyze images by connecting to clair? - DevOps Stack Exchange

今回は、Clair scannerを使ってみます。

GitHub - arminc/clair-scanner: Docker containers vulnerability scan

README.mdを見ていると、自分でビルドしなくてはいけない雰囲気を感じますが、releasesからビルド済みバイナリを取得
できるので、今回はこちらを使用します。

$ wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 -O clair-scanner
$ chmod a+x clair-scanner

Clair scannerは、ローカルのDockerイメージの情報をClairに送り、Clairから結果を受け取ることで脆弱性スキャン結果を
表示します。

Dockerイメージは、Clair scannerが動作するローカル環境にある必要があります。ちなみに、Clairが動作している環境には、
Dockerイメージが存在する必要はありません。

Clairから結果を受け取るという話に戻すと、ローカル環境はIPアドレスが192.168.0.2なのですが、こちらに対して
Docker Compose上で動作しているClairから脆弱性結果が送信されるイメージになります。

Clair scannerからClairに対して、イメージの情報を送信するのですが、このレスポンスにスキャン結果があるわけではなく、
あくまでClairからHTTPリクエストが送信されてきます。

このため、Clair scannerは短命ですが、Clairから脆弱性スキャン結果を受け取るサーバーにもなります。

ヘルプ。

$ ./clair-scanner
Error: incorrect usage

Usage: clair-scanner [OPTIONS] IMAGE

Scan local Docker images for vulnerabilities with Clair

Arguments:
  IMAGE=""     Name of the Docker image to scan

Options:
  -w, --whitelist=""                    Path to the whitelist file
  -t, --threshold="Unknown"             CVE severity threshold. Valid values; 'Defcon1', 'Critical', 'High', 'Medium', 'Low', 'Negligible', 'Unknown'
  -c, --clair="http://127.0.0.1:6060"   Clair URL
  --ip="localhost"                      IP address where clair-scanner is running on
  -l, --log=""                          Log to a file
  --all, --reportAll=true               Display all vulnerabilities, even if they are approved
  -r, --report=""                       Report output file, as JSON

「-c」でClairに対するURL、「--ip」でClair scannerがどのIPアドレスでバインドさせるかを指定します。

「--ip」で指定されたIPアドレスに対して、Clairから結果が送信されてきます。デフォルトは「localhost」なので、今回のように
ClairをDockerコンテナ上で動かしていると、結果の受け取り先が見つからずに実行に失敗します。

今回は、こんな感じの指定になります。「-c」で指定しているのは、Docker Compose上のClairのIPアドレスです。

$ ./clair-scanner -c http://172.19.0.3:6060 --ip=192.168.0.2 [Dockerイメージ]

あと、「-w」でホワイトリストの指定もできるようですが、今回はパス…。

サンプルは、このあたりです。

https://github.com/arminc/clair-scanner/blob/master/example-alpine.yaml

https://github.com/arminc/clair-scanner/blob/master/example-whitelist.yaml

試しに、ApacheのDockerイメージに対して、スキャンを行ってみましょう。

まずは、イメージをpull。

$ docker pull httpd:2.4.37

実行。

$ ./clair-scanner -c http://172.19.0.3:6060 --ip=192.168.0.2 httpd:2.4.37
2019/01/12 22:26:16 [INFO] ▶ Start clair-scanner
2019/01/12 22:26:23 [INFO] ▶ Server listening on port 9279
2019/01/12 22:26:23 [INFO] ▶ Analyzing 4101911ad4bd7142bc97f3d255c3e5a2ad202246ae5a9f0e3b712865198e2fe3
2019/01/12 22:26:23 [INFO] ▶ Analyzing d7cffd99b16a032223434d27ac88ef1425ef6d86067d81768e78964168782a29
2019/01/12 22:26:23 [INFO] ▶ Analyzing f7081d1ca73a53dc13351dcb12539a49fd97713e8647780734b31150d315d293
2019/01/12 22:26:23 [INFO] ▶ Analyzing 0eadcd43b848bf964f4d0166ab7a96b8f9d740f2b6a96249a48565f162d740ca
2019/01/12 22:26:23 [INFO] ▶ Analyzing e109fadbe58ded0d1013edfa38040d50d0078a7ec1d47ea016e3c8222e7f1b0f
2019/01/12 22:26:23 [WARN] ▶ Image [httpd:2.4.37] contains 146 total vulnerabilities

結果は、こんな感じで表示されます(大量に…)。

2019/01/12 22:26:23 [ERRO] ▶ Image [httpd:2.4.37] contains 146 unapproved vulnerabilities
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| STATUS     | CVE SEVERITY                | PACKAGE NAME | PACKAGE VERSION        | CVE DESCRIPTION                                              |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| Unapproved | High CVE-2018-14614         | linux        | 4.9.130-2              | An issue was discovered in the Linux kernel                  |
|            |                             |              |                        | through 4.17.10. There is an out-of-bounds                   |
|            |                             |              |                        | access in __remove_dirty_segment() in                        |
|            |                             |              |                        | fs/f2fs/segment.c when mounting an f2fs image.               |
|            |                             |              |                        | https://security-tracker.debian.org/tracker/CVE-2018-14614   |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| Unapproved | High CVE-2018-14610         | linux        | 4.9.130-2              | An issue was discovered in the Linux kernel                  |
|            |                             |              |                        | through 4.17.10. There is out-of-bounds access               |
|            |                             |              |                        | in write_extent_buffer() when mounting and                   |
|            |                             |              |                        | operating a crafted btrfs image, because of a                |
|            |                             |              |                        | lack of verification that each block group has               |
|            |                             |              |                        | a corresponding chunk at mount time, within                  |
|            |                             |              |                        | btrfs_read_block_groups in fs/btrfs/extent-tree.c.           |
|            |                             |              |                        | https://security-tracker.debian.org/tracker/CVE-2018-14610   |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| Unapproved | High CVE-2018-9517          | linux        | 4.9.130-2              | In pppol2tp_connect, there is possible memory                |
|            |                             |              |                        | corruption due to a use after free. This could               |
|            |                             |              |                        | lead to local escalation of privilege with System            |
|            |                             |              |                        | execution privileges needed. User interaction is             |
|            |                             |              |                        | not needed for exploitation. Product: Android.               |
|            |                             |              |                        | Versions: Android kernel. Android ID: A-38159931.            |
|            |                             |              |                        | https://security-tracker.debian.org/tracker/CVE-2018-9517    |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| Unapproved | High CVE-2017-1000379       | linux        | 4.9.130-2              | The Linux Kernel running on AMD64 systems will               |
|            |                             |              |                        | sometimes map the contents of PIE executable,                |
|            |                             |              |                        | the heap or ld.so to where the stack is mapped               |
|            |                             |              |                        | allowing attackers to more easily manipulate the             |
|            |                             |              |                        | stack. Linux Kernel version 4.11.5 is affected.              |
|            |                             |              |                        | https://security-tracker.debian.org/tracker/CVE-2017-1000379 |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+
| Unapproved | High CVE-2018-14611         | linux        | 4.9.130-2              | An issue was discovered in the Linux kernel                  |
|            |                             |              |                        | through 4.17.10. There is a use-after-free in                |
|            |                             |              |                        | try_merge_free_space() when mounting a crafted btrfs         |
|            |                             |              |                        | image, because of a lack of chunk type flag checks           |
|            |                             |              |                        | in btrfs_check_chunk_valid in fs/btrfs/volumes.c.            |
|            |                             |              |                        | https://security-tracker.debian.org/tracker/CVE-2018-14611   |
+------------+-----------------------------+--------------+------------------------+--------------------------------------------------------------+

〜省略〜

Severity(重大度)は、以下のように表示されます。

High CVE-2018-14614

先頭に付いているのがそうで、収集したデータソースからの脆弱性情報は、以下のようにマッピングされます。

  • Defcon1(使われてなさそう)
  • Critical
  • High
  • Medium
  • Low
  • Negligible
  • Unknown

マッピングしているのは、例えば以下あたり。

https://github.com/coreos/clair/blob/master/ext/vulnsrc/ubuntu/ubuntu.go

func SeverityFromPriority(priority string) database.Severity {
    switch priority {
    case "untriaged":
        return database.UnknownSeverity
    case "negligible":
        return database.NegligibleSeverity
    case "low":
        return database.LowSeverity
    case "medium":
        return database.MediumSeverity
    case "high":
        return database.HighSeverity
    case "critical":
        return database.CriticalSeverity
    default:
        log.Warningf("could not determine a vulnerability severity from: %s", priority)
        return database.UnknownSeverity
    }
}

https://github.com/coreos/clair/blob/master/ext/vulnsrc/rhel/rhel.go

func severity(sev string) database.Severity {
    switch strings.Title(sev) {
    case "Low":
        return database.LowSeverity
    case "Moderate":
        return database.MediumSeverity
    case "Important":
        return database.HighSeverity
    case "Critical":
        return database.CriticalSeverity
    default:
        log.Warningf("could not determine vulnerability severity from: %s.", sev)
        return database.UnknownSeverity
    }
}

確認したい、対処したい重大度に合わせて、このあたりを見ていくのでしょうねぇ。

まとめ

Dockerイメージの脆弱性スキャンに使えるツール、Clairを試してみました。

とりあえず動かしてみただけなので、CI/CDに組み込むとかそういう高尚なところは全然調べられていませんが…。

コンテナを使うなら、こういうところも気にしていかないとなーという気がします。