NFLabs. エンジニアブログ

セキュリティやソフトウェア開発に関する情報を発信する技術者向けのブログです。

Trivy を使ってみた

こんにちは。NFLabs. 事業推進部の齋藤です。この記事は NFLabs. アドベントカレンダー 14 日目の記事です。 普段は Blue Team の中で利用するシステムの開発をしています。

私は今のチームで DevSecOps の仕組みやツールを色々と試して導入を進めている最中なのですが、その内でも Trivy というコンテナイメージのスキャン機能を持ったツールについて紹介したいと思います。

github.com

Trivy の概要

Trivy はアプリケーションの依存ライブラリの脆弱性スキャン機能と IaC の設定ファイルのセキュリティ不備の検出機能を備えたセキュリティスキャンツールです。 アプリケーションを動かしてスキャンを行うのではなく、指定されたコンテナイメージやローカルのディレクトリに含まれる設定ファイルを自動で探索し、静的にバージョン情報や設定を抽出してチェックを行ってくれるため、高速なスキャンができるのが特徴です。

もともとはコンテナイメージの脆弱性スキャナとして開発されていましたが、開発が進むにつれ他ツールとの統合がなされ、設定ファイルの不備もチェックできるようになりました。

インストールは各 OS のパッケージマネージャ経由で行うか、 docker イメージを使うのが簡単です。

# Ubuntu/Debian
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy

# Mac
brew install aquasecurity/trivy/trivy

# docker
docker pull aquasec/trivy:0.21.1

Trivy を使った脆弱性スキャン

脆弱性スキャン機能は、言語毎の依存ライブラリの管理ファイル (yarn.lock, poetry.lock, go.sum など)や、 apt, yum, apk などでインストールしたOS のパッケージ情報を抽出し、こんな感じで脆弱なバージョンを使っていないかを検出してくれます。

f:id:keis-nflabs:20211130100156p:plain

対応しているパッケージマネージャーは下記のとおりです。

OS packages (Alpine Linux, Red Hat Universal Base Image, Red Hat Enterprise Linux, CentOS, Oracle Linux, Debian, Ubuntu, Amazon Linux, openSUSE Leap, SUSE Enterprise Linux, Photon OS and Distroless) Language-specific packages (Bundler, Composer, Pipenv, Poetry, npm, yarn, Cargo, NuGet, Maven, and Go)

使い方は簡単で、コンテナイメージをスキャンする場合 trivy image [image 名] と実行するだけです。 例として Trivy のオーナーの方が用意した検証用の脆弱なイメージをスキャンした結果がこちらです。 (出力結果が長いため、一部だけ切り取っています。)

パッケージマネージャ経由でインストールされた git や、 Cargo.lock に書かれた依存ライブラリが脆弱なバージョンであることが検出され、脆弱性の概要と修正バージョンが書かれています。

$ trivy image knqyf263/vuln-image:1.2.3
2021-11-28T23:08:22.930+0900    INFO    Detected OS: alpine
2021-11-28T23:08:22.930+0900    INFO    Detecting Alpine vulnerabilities...
2021-11-28T23:08:22.932+0900    INFO    Number of language-specific files: 2
2021-11-28T23:08:22.934+0900    INFO    Detecting composer vulnerabilities...
2021-11-28T23:08:22.935+0900    INFO    Detecting cargo vulnerabilities...
2021-11-28T23:08:22.935+0900    WARN    This OS version is no longer supported by the distribution: alpine 3.7.1
2021-11-28T23:08:22.935+0900    WARN    The vulnerability detection may be insufficient because security updates are not provided

knqyf263/vuln-image:1.2.3 (alpine 3.7.1)
========================================
Total: 64 (UNKNOWN: 0, LOW: 1, MEDIUM: 25, HIGH: 19, CRITICAL: 19)

+-----------------------+------------------+----------+-------------------+---------------+-----------------------------------------+
|        LIBRARY        | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION |                  TITLE                  |
+-----------------------+------------------+----------+-------------------+---------------+-----------------------------------------+
| git                   | CVE-2018-17456   | CRITICAL | 2.15.2-r0         | 2.15.3-r0     | git: arbitrary code                     |
|                       |                  |          |                   |               | execution via .gitmodules               |
|                       |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2018-17456   |
+                       +------------------+----------+                   +---------------+-----------------------------------------+
|                       | CVE-2019-1349    | HIGH     |                   | 2.15.4-r0     | git: Recursive submodule cloning        |
|                       |                  |          |                   |               | allows using git directory twice        |
|                       |                  |          |                   |               | with synonymous directory...            |
|                       |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2019-1349    |
+                       +------------------+----------+                   +               +-----------------------------------------+
|                       | CVE-2019-1348    | LOW      |                   |               | git: Arbitrary path                     |
|                       |                  |          |                   |               | overwriting via export-marks            |
|                       |                  |          |                   |               | in-stream command feature               |
|                       |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2019-1348    |
+-----------------------+------------------+----------+-------------------+---------------+-----------------------------------------+

rust-app/Cargo.lock (cargo)
===========================
Total: 9 (UNKNOWN: 9, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

+-----------+-------------------+----------+-------------------+------------------------------+---------------------------------------------+
|  LIBRARY  | VULNERABILITY ID  | SEVERITY | INSTALLED VERSION |        FIXED VERSION         |                    TITLE                    |
+-----------+-------------------+----------+-------------------+------------------------------+---------------------------------------------+
| ammonia   | RUSTSEC-2021-0074 | UNKNOWN  | 1.9.0             | >= 3.1.0, >= 2.1.3, < 3.0.0  | Incorrect handling of embedded SVG          |
|           |                   |          |                   |                              | and MathML leads to mutation XSS            |
|           |                   |          |                   |                              | -->rustsec.org/advisories/RUSTSEC-2021-0074 |
+-----------+-------------------+----------+-------------------+------------------------------+---------------------------------------------+

Trivy は脆弱性情報を格納した DB を使ってスキャンを行っていて、 DB が最後に更新されて 12 時間以上経過していれば Trivy の起動時に更新が行われます。 DB のサイズは 10~30MB と小さいため、スキャンの前に更新がかかってもスキャン完了までの時間が大きく延びることもありません。

Trivy downloads its vulnerability database every 12 hours when it starts operating. This is usually fast, as the size of the DB is only 10~30MB.

また Trivy はコンテナイメージだけでなく、 trivy fs [スキャン対象のパス] でローカルディレクトリを指定してスキャンすることも可能です。 先程の例と同じイメージを使ってスキャンをしてみます。 ただし、今度はコンテナ内に trivy をインストールしてコンテナ内の package-lock.json が置かれたディレクトリをスキャンします。 ついでに --severity オプションを指定して、深刻度が CRITICAL,HIGH のもののみ表示するようにしてみます。

$ docker run -it --rm knqyf263/vuln-image:1.2.3 bash
bash-4.4# curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.21.1
bash-4.4# trivy fs --severity CRITICAL,HIGH /node-app/
2021-11-28T10:11:19.327Z        INFO    Number of language-specific files: 1
2021-11-28T10:11:19.327Z        INFO    Detecting npm vulnerabilities...

package-lock.json (npm)
=======================
Total: 4 (HIGH: 3, CRITICAL: 1)

+---------+------------------+----------+-------------------+---------------+---------------------------------------+
| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION |                 TITLE                 |
+---------+------------------+----------+-------------------+---------------+---------------------------------------+
| lodash  | CVE-2019-10744   | CRITICAL | 4.17.4            | 4.17.12       | nodejs-lodash: prototype              |
|         |                  |          |                   |               | pollution in defaultsDeep function    |
|         |                  |          |                   |               | leading to modifying properties       |
|         |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2019-10744 |
+         +------------------+----------+                   +---------------+---------------------------------------+
|         | CVE-2018-16487   | HIGH     |                   | >=4.17.11     | lodash: Prototype pollution           |
|         |                  |          |                   |               | in utilities function                 |
|         |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2018-16487 |
+         +------------------+          +                   +---------------+---------------------------------------+
|         | CVE-2020-8203    |          |                   | 4.17.19       | nodejs-lodash: prototype pollution    |
|         |                  |          |                   |               | in zipObjectDeep function             |
|         |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2020-8203  |
+         +------------------+          +                   +---------------+---------------------------------------+
|         | CVE-2021-23337   |          |                   | 4.17.21       | nodejs-lodash: command                |
|         |                  |          |                   |               | injection via template                |
|         |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2021-23337 |
+---------+------------------+----------+-------------------+---------------+---------------------------------------+

CRITICAL と HIGH の脆弱性のみが出力されました。 スキャンで検出される脆弱性には攻撃の実現可能性や影響度が低いものも含まれるため、このように --severity オプションなどのフィルタリング機能を使って、すぐに対応が必要な危険度の高い脆弱性に絞って結果を表示する機能も備わっています。

また、重大な脆弱性であっても自分たちの利用方法では影響度を受けないとわかっているものなど、対応が必要ない脆弱性の CVE を .trivyignore というファイルに記述しておくことでスキャン結果から除外することも可能です。 CVE や severity だけでなく、 Rego を使って、もっと柔軟に CWE やライブラリ名などの組み合わせで除外設定を記述する機能も実装されています (ただし、 unstable な機能という位置づけ らしい)。 紹介した以外で Trivy で使えるフィルタ機能の詳細についてはドキュメントを参照してください。

ちなみに先程紹介した trivy fs とよく似た、 trivy rootfs [rootfs のパス] というコマンドも用意されています。 こちらは各種ライブラリの脆弱性に加え、ホストマシンや VM などの rootfs 内にインストールされている OS パッケージの脆弱性を検出できます。 また、 trivy repo [リポジトリ URL] でリポジトリをスキャンすることも可能です。

他にも開発環境からも CLI やVSCode の拡張機能を使ってスキャンが可能なので、開発プロセスの中のより早い段階で素早く問題を検出できます。

Dependabot を使っていたら使わなくても良い?

脆弱性スキャン機能の説明を読んでいて、「これって Depandabot とやっていること変わらなくない? (Dependabot でよくない?)」と思われた方がいたかも知れません。 確かに、依存ライブラリを解析して脆弱なバージョンが存在するかをチェックするという点ではどちらも同じことをしていそうです。

しかし、個人的には「基本的には Trivy を使い、チームに合わせて Dependabot も併用するのもあり」という考え方をすると良いかなと思います。 というのも、両ツール間で確認できる対象や機能が異なっているからです。

Dependabot はリポジトリを見てアップデート用の PR を投げてくれますが、その修正がプロダクション環境に反映されているかをチェックすることはできません (GitOps のような CI/CD フローを採用することで適用漏れのリスクや修正が反映されるまでのリードタイムを極小化することは可能ですが)。 その点、 Trivy はコンテナイメージを指定して対してスキャンをすることができるので、 今まさに動作している環境 (コンテナイメージ) に脆弱性があるか を確認することができます。

このユースケースについて少し掘り下げるため、ある namespace 上で動作している全ての Pod のイメージを定期的にスキャンしたいというケースを考えてみます。 実は trivy にはプラグイン機能があり、簡単に機能を拡張できるようになっています。 そのプラグインの一つにtrivy-plugin-kubectlがあります。 trivy-plugin-kubectl は kubectl get で取得した Pod や Deployment の情報からイメージ名を抽出してスキャンをかけてくれるので、これを使って下記のように Pod を全て検索することができます。

github.com

# k8s のドキュメントに載っている nginx の Deployment を apply します (あまり面白くないですが。。)
# https://kubernetes.io/ja/docs/tasks/run-application/run-stateless-application-deployment/#nginx-deployment%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E6%8E%A2%E6%A4%9C
# これで nginx の Pod がレプリカ数 2 で立ち上がります
$ cat <<EOF | kubectl -n test apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
EOF
$ kubectl -n test get pods
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-66b6c48dd5-rr6b6   1/1     Running   0          21m
nginx-deployment-66b6c48dd5-zlzp8   1/1     Running   0          21m

# kubectl 用の plugin をインストール
$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl
# test namaspace 内の全 Pod に対してスキャン
# 2 回実行されているのが確認できる
$ for name in $(kubectl -n test get pods -o jsonpath='{.items[*].metadata.name}'); do
    trivy kubectl -n test pods $name
done
2021-11-29T00:26:03.463+0900    INFO    Detected OS: debian
2021-11-29T00:26:03.463+0900    INFO    Detecting Debian vulnerabilities...
2021-11-29T00:26:03.471+0900    INFO    Number of language-specific files: 0

nginx:1.14.2 (debian 9.8)
=========================
Total: 321 (UNKNOWN: 4, LOW: 148, MEDIUM: 61, HIGH: 73, CRITICAL: 35)
...
+-------------------+------------------+----------+------------------------+------------------------+-----------------------------------------+
2021-11-29T00:26:40.992+0900    INFO    Detected OS: debian
2021-11-29T00:26:40.992+0900    INFO    Detecting Debian vulnerabilities...
2021-11-29T00:26:41.000+0900    INFO    Number of language-specific files: 0

nginx:1.14.2 (debian 9.8)
=========================
Total: 321 (UNKNOWN: 4, LOW: 148, MEDIUM: 61, HIGH: 73, CRITICAL: 35)
...
# Deployment を指定してのスキャンも可能
$ trivy kubectl -n test deployment nginx-deployment

このように実際にデプロイされているイメージを簡単にスキャンすることが可能です。複数バージョンを別々のイメージで動かしているときなどは特に重宝しそうです。 これを定期実行しておき、脆弱性が見つかったのをトリガーに webhook や issue を起票するような仕組みを作れば、イメージに脆弱性が含まれていないことを常時監視することも可能です。

また Dependabot は記事執筆時点で OS パッケージのチェックは issue が立てられているもののまだ未対応です。 ビルド時にキャッシュを使うために OS パッケージが中々更新されずに放置されてしまう、なんていう状況は結構あるあるなのではないでしょうか? これは Trivy でないと検出できないため、コンテナを使ってデプロイを行っている場合は Dependabot を入れていたとしても Trivy を使ってチェックをするべきです。

じゃあ Dependabot じゃなくて Trivy だけでいいのでは?となってしまいそうですが、 Trivy にも苦手なことはあります。

Trivy は定期スキャンをさせるだけなら簡単に設定ができますが、 Dependabot のように自動で PR を作成する機能はありません。 実現したければ自分で機能を作ってやる必要があります。 ただし、 Trivy の検知結果を元に issue を起票する仕組みであれば、Trivy 公式の GitHub ActionsGitHub Actions 以外の CI 設定例を提供している他、オーナーによる Trivy のスキャン結果に基づいて issue を自動起票する Actions も公開されているため、これらをベースに CI を設定すれば割とお手軽に実現が可能です。

一方で Dependabot は GitHub 上で数クリックするだけで簡単に定期スキャン + 自動 PR 作成まで行ってくれるため、セットアップや毎度のバージョンアップ作業にかかる負担は Trivy に比べて圧倒的に楽です。

以上をまとめると、 Dependabot は持続可能なバージョンアップ対応を実現するために、 Trivy は開発プロセスの早期の段階 (最速ならローカルで開発している時点) で OS パッケージも含めた脆弱性の検出や、デプロイされた環境の脆弱性チェックをするために、といった具合にそれぞれ得意なユースケースが分かれています。

そのため、チームの状況に合わせて

  • Dependabot で各種言語のライブラリのチェックを行い、 Trivy では OS パッケージだけデプロイ時に検査
  • Trivy でライブラリも OS パッケージの両方をチェックし、 issue 起票まで自動化 (or スキャン結果を元に PR を作れるように自前で設定・機能を作る)

といった具合に、場合によってはうまく両ツールを組み合わせるのが良いのではないか、というのが私の考えです。

設定ファイルのセキュリティ不備の検出

続いて、 Trivy の設定ファイルのスキャン機能についても紹介します。 この機能は kubernetes や docker, Terraform といった IaC の設定ファイルのセキュリティ上まずい部分を検出してくれます。 設定ファイルのスキャンもイメージのスキャンと同じく、静的解析によるスキャンをおこなっているため、非常に高速に検証が可能なのがメリットです。

Trivy は各種設定ファイルの不備を Rego で記述したポリシーとして管理しており、一覧はこちらから確認できます。 これらのポリシーは OSS として公開され、継続的にアップデートがされていくようになっています。

設定ファイルをチェックする例として、 kubernetes の manifest をスキャンしてみます。 Trivy が利用する kubernetes のポリシーには PodSecurityStandard に沿っているかをチェックするものを始めとして、リソース制限を設定しているかなど、一般的なお作法に沿っているかをチェックするものも入っています。

脆弱性チェックの際に使用した nginx の deployment を使ってチェックをしてみます。

$ cat deployment/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

$ trivy config deployment
2021-11-30T07:28:37.799+0900    INFO    Detected config files: 1

nginx-deployment.yaml (kubernetes)
==================================
Tests: 28 (SUCCESSES: 17, FAILURES: 11, EXCEPTIONS: 0)
Failures: 11 (UNKNOWN: 0, LOW: 6, MEDIUM: 5, HIGH: 0, CRITICAL: 0)

+---------------------------+------------+----------------------------------------+----------+--------------------------------------------+
|           TYPE            | MISCONF ID |                 CHECK                  | SEVERITY |                  MESSAGE                   |
+---------------------------+------------+----------------------------------------+----------+--------------------------------------------+
| Kubernetes Security Check |   KSV001   | Process can elevate its own privileges |  MEDIUM  | Container 'nginx' of Deployment            |
|                           |            |                                        |          | 'nginx-deployment' should set              |
|                           |            |                                        |          | 'securityContext.allowPrivilegeEscalation' |
|                           |            |                                        |          | to false                                   |
|                           |            |                                        |          | -->avd.aquasec.com/appshield/ksv001        |
+                           +------------+----------------------------------------+----------+--------------------------------------------+
|                           |   KSV003   | Default capabilities not dropped       |   LOW    | Container 'nginx' of Deployment            |
|                           |            |                                        |          | 'nginx-deployment' should add 'ALL'        |
|                           |            |                                        |          | to 'securityContext.capabilities.drop'     |
|                           |            |                                        |          | -->avd.aquasec.com/appshield/ksv003        |
+                           +------------+----------------------------------------+          +--------------------------------------------+
|                           |   KSV011   | CPU not limited                        |          | Container 'nginx' of Deployment            |
|                           |            |                                        |          | 'nginx-deployment' should                  |
|                           |            |                                        |          | set 'resources.limits.cpu'                 |
|                           |            |                                        |          | -->avd.aquasec.com/appshield/ksv011        |
+                           +------------+----------------------------------------+----------+--------------------------------------------+
|                           |   KSV012   | Runs as root user                      |  MEDIUM  | Container 'nginx' of Deployment            |
|                           |            |                                        |          | 'nginx-deployment' should set              |
|                           |            |                                        |          | 'securityContext.runAsNonRoot' to true     |
|                           |            |                                        |          | -->avd.aquasec.com/appshield/ksv012        |
+                           +------------+----------------------------------------+----------+--------------------------------------------+
...

セキュリティ上怪しい点が複数検出されました。(一部省略) 少し中身を見てみると、今回の manifest には securityContext を一切設定していなかったため、 securityContext.allowPrivilegeEscalationsecurityContext.runAsNonRoot の設定を追加して最低限の権限で動作させるように提案を出してくれています。

こうしたチェックをサクッと実行できるため、 deprecated となった PodSecurityPolicy の後釜である Pod Security Admission (PSA) が正式に使えるようになるまでのつなぎとしても有用ですし、自分のチームの運用ルールに合わせてカスタムの Rego ファイルを用意すれば、運用ルールのコード化を実現することもできます。 Trivy で使う Rego ファイルのフォーマットについてはドキュメントに詳しく書いてあるため、ビルトインのルールを参考にしながら書いてみるのが良さそうです。

ところで、各種設定ファイルを Rego で書いたポリシーで検証すると聞くと、 Conftest を連想する方がいるかもしれませんが、実は Trivy の設定ファイルのスキャンは Conftest の影響を大きく受けて作られています。どちらもポリシーチェックツールとして非常に強力です。 あえて Conftest になくて Trivy にある大きなアドバンテージを挙げるとするならば、ビルトインのルールが備わっていることです。

Conftest も公開されている Rego ルールを集めてきたり、自分たちで作成することでさまざまなファイルを検証することができますが、 Trivy は自動で最新のルールを取得して検証ができるため、ポリシーを専門として管理できるリソースがないチームでも取り入れやすいというメリットがあります。

逆に自分たちのチームでポリシーをゴリゴリ書いて運用するぞ!という方ならば両ツールでやれることに大きな違いはないため、細かい機能の違いを見てどちらを使うか選ぶといいと思います。

まとめ

Trivy の脆弱性スキャン機能と設定ファイルのセキュリティ不備の検出方法について紹介しました。開発フローの中に簡単に取り入れられるので、 DevSecOps 的なことををチームの中に取り入れたいと思っている方の取っ掛かりとして導入してみるのもいいかもしれません。開発も活発で、 Red Hat などでも導入実績のある将来性が高いツールなのでぜひ一度触ってみてください!