Trivy DB に 優しい Github Actions を作成する

お久しぶりです、ANDPADのtomtwinkleです。 この記事はANDPADアドベントカレンダー2日目の記事になります。

みなさん、Trivy 使ってますか?

Trivyとはコンテナイメージの脆弱性をチェック出来るツールです。 Golang の net/http 辺りは度々DoS脆弱性が見つかることがあり、それに早めに気づけて更新出来ているため、大変助かっております。

おや? Trivy DB の様子が……?

そんなTrivyですが、数ヶ月前くらいからTrivy DBのダウンロードに失敗するケースが増えてきました。

OCI repository error: 1 error occurred:
* GET https://ghcr.io/v2/aquasecurity/trivy-db/manifests/2: TOOMANYREQUESTS: retry-after: 1.039236ms, allowed: 44000/minute"

原因はエラーを見ての通り GitHub Container Registry から Trivy DB を取得する際の rate-limit ですね。

そこで Githubのrate-limitに関するドキュメントを確認してみると 未認証TOKEN(GITHUB_TOKEN) の rate-limitは 60 requests per hour per repository だそうです。

はて、以前ドキュメントを見たときは 1,000 requests per hour per repository だったような……?

念の為 https://api.github.com/rate_limit で現在の rate limitを確認できるので確認して見ましょう。

❯ curl https://api.github.com/rate_limit
{
  /* 省略 */
  "rate": {
    "limit": 60,
    "remaining": 60,
    "reset": 1732764408,
    "used": 0,
    "resource": "core"
  }
}

ドキュメントの記載通り limit: 60 になっていそうですね。

Trivy の discussions でも同じ事象に遭遇したユーザーによる議論が繰り広げられています。

github.com

Trivy DB 取得時の rate-limit 対策

PAT(personal access token) を利用する

Github の rate-limit に引っかかっているので一番単純な対策としては rate-limit が 5,000 requests per hour per repository に増える PAT(personal access token) を利用することです。

      - name: Get image tag
        id: get-image-tag
        shell: bash
        run: |
          # 何らかの方法でimage tagを取得、この例だとcommit hash
          IMAGE_TAG=${{ github.sha }}
          echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"

      - name: Configured to use Github PAT
        shell: bash
        run: |
          git config --global url.https://${GITHUB_PAT_USER}:${GITHUB_PAT}@github.com/.insteadOf https://github.com/

      - name: Run Trivy vulnerability scanner
        shell: bash
        run: |
          trivy image \
            --exit-code 1 \
            --ignore-unfixed \
            --severity CRITICAL,HIGH \
            --pkg-types os,library \
            --no-progress \
            "${{ env.ecr-arn }}:${{ steps.get-image-tag.outputs.IMAGE_TAG }}"

めでたしめでたし〜

……ちょっと待ってください。

そもそも利用するPATが増えると管理コスト増えてしまうという問題があります。 PATの利用期限を No expiration (無制限) にするのは何らかの要因でtokenが漏洩した場合や、担当者不在で管理外のtokenが発生する場合にtokenを無効化されるタイミングがないなど、セキュリティリスクに繋がります。 そのためしっかり管理したいのですが数が増えると面倒です。なるべく増やしたくない。

Amazon ECR の imageを利用する

Trivyの README を眺めている と Amazon ECR の方にもaquasecurityが試験的に運用しているRepositoryがありました。

gallery.ecr.aws

こちらを利用すれば少なくとも GitHub の rate-limit に影響を受けずに利用できそうです。 更に Trivy v0.56.0 以降では db-repository Option に複数のRepositoryが指定できるようになっていました。 これは良さそう。

github.com

      - name: Get image tag
        id: get-image-tag
        shell: bash
        run: |
          # 何らかの方法でimage tagを取得、この例だとcommit hash
          IMAGE_TAG=${{ github.sha }}
          echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"

      - name: Run Trivy vulnerability scanner
        shell: bash
        run: |
          trivy image \
            --exit-code 1 \
            --ignore-unfixed \
            --severity CRITICAL,HIGH \
            --pkg-types os,library \
            --no-progress \
            --db-repository "ghcr.io/aquasecurity/trivy-db:2","public.ecr.aws/aquasecurity/trivy-db" \
            "${{ env.ecr-arn }}:${{ steps.get-image-tag.outputs.IMAGE_TAG }}"

ログを見るとまず ghcr.io/aquasecurity/trivy-db:2 にアクセスし rate-limitに引っかかっている場合は public.ecr.aws/aquasecurity/trivy-db:2 をダウンロードに行っています。良いのでは?

...
2024-10-01T14:00:39+06:00       INFO    [db] Need to update DB
2024-10-01T14:00:39+06:00       INFO    Downloading vulnerability DB...
2024-10-01T14:00:39+06:00       INFO    Downloading  artifact...        repo="ghcr.io/aquasecurity/trivy-db:2"
2024-10-01T14:00:41+06:00       ERROR   Failed to download artifact     repo="ghcr.io/aquasecurity/trivy-db:2" err="oci download error: failed to fetch the layer: GET https://ghcr.io/v2/aquasecurity/trivy-db/blobs/sha256:23d1b901e7534020d5ac5f238b090ad66dcb78afef7301ed7d8ebe6b974ab5f1: TOOMANYREQUESTS: retry-after: 1.254886ms, allowed: 44000/minute"
2024-10-01T14:00:41+06:00       INFO    Trying to download artifact from other repository...
2024-10-01T14:00:41+06:00       INFO    Downloading  artifact...        repo="public.ecr.aws/aquasecurity/trivy-db:2"
53.85 MiB / 53.85 MiB [--------------------------------------------------------------------------------------] 100.00% 1.45 MiB p/s 37s
2024-10-01T14:01:22+06:00       INFO    Artifact successfully downloaded        repo="public.ecr.aws/aquasecurity/trivy-db:2"
...

これで暫くは大丈夫そうですね! めでたしめでたし!

Trivy DB を Cacheする

めでたしめでたし、と思いきやもう少し続くのじゃ

Amazon ECR を参照すれば今回の問題は一応は解決するのですが そもそもTrivy DB の Update Interval は 6時間毎 であり、imageのbuild毎にTrivyのチェックのためだけに毎回ダウンロードする必要性はありません。 今回はGithubのrate-limitの問題でしたが、Amazon ECRのrate-limitが厳しくならないとも言い切れないです。

ならば、普段はTrivy DBのcacheを利用して6時間毎に取りに行けば良いのでは?

というわけで営業時間内の6時間ごとにcacheするGithub Actionsを作ってみました。

Trivy DB をCacheする Github Actions

name: Update Trivy DB Cache
on:
  schedule:
    # At the start of 10:00 JST and 19:00 JST per 6 Hour on Monday through Friday
    # 10:00 JST and 16:00 JST
    - cron: '0 1-10/6 * * Mon-Fri'
  workflow_call: # Allow workflow triggering
  workflow_dispatch:  # Allow manual triggering

jobs:
  update-trivy-db-cache:
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Get current date
        id: date
        shell: bash
        run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
      - name: Check Trivy Cache
        id: trivy-cache
        uses: actions/cache@v4
        with:
          key: cache-trivy-${{ steps.date.outputs.date }}
          path: ${{ github.workspace }}/.cache/trivy
          lookup-only: true
      - name: Setup oras
        if: steps.trivy-cache.outputs.cache-hit != 'true' || github.event.schedule != ''
        uses: oras-project/setup-oras@v1
      - name: Download and extract the vulnerability DB
        if: steps.trivy-cache.outputs.cache-hit != 'true' || github.event.schedule != ''
        shell: bash
        run: |
          mkdir -p $GITHUB_WORKSPACE/.cache/trivy/db
          oras pull public.ecr.aws/aquasecurity/trivy-db
          tar -xzf db.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/db
          rm db.tar.gz
      - name: Download and extract the Java DB
        if: steps.trivy-cache.outputs.cache-hit != 'true' || github.event.schedule != ''
        shell: bash
        run: |
          mkdir -p $GITHUB_WORKSPACE/.cache/trivy/java-db
          oras pull ghcr.io/aquasecurity/trivy-java-db:1
          tar -xzf javadb.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/java-db
          rm javadb.tar.gz
      - name: Cache DBs
        if: steps.trivy-cache.outputs.cache-hit != 'true' || github.event.schedule != ''
        uses: actions/cache/save@v4
        with:
          path: ${{ github.workspace }}/.cache/trivy
          key: cache-trivy-${{ steps.date.outputs.date }}

ざっくりと schedule と workflow_call で起動し、scheduleの場合は毎回cacheを更新 workflow_call で別のGithub Actionsから呼ばれた場合はcacheの有無を確認して無ければ更新するみたいな処理です。

弊社の場合、フレックス制なので勤務時間はバラバラなのですが、私の観測範囲では大体の開発メンバーは 10:00 - 19:00 付近を稼働時間としているようです。 その範囲で余計にworkflowが動きすぎないようにしています。

何らかの要因でずっとcacheを見てしまうという自体を避けるため、日付(YYYY-MM-DD) をcacheのKeyとしています。 こうしておくことで日付が変われば参照するcacheがなくなり、更新処理が走ります。

さらに、念の為手動でWorkflowを実行できるように workflow_dispatch も有効にしておくと吉でしょう。

今回は public.ecr.aws/aquasecurity/trivy-db から取得していますが、

oras pull ghcr.io/aquasecurity/trivy-db:2 で取得できなかったら oras pull public.ecr.aws/aquasecurity/trivy-db を参照する動作にするとより良さそうです。

Trivy DB の cacheを利用する Trivy Github Actions

name: Trivy Scan

on:
  push:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  # この例では Amazon ECR の arn を指定
  ecr-arn: '<image repository>'

permissions:
  id-token: write
  contents: read

jobs:
  trivy_db_update:
    name: Download and extract the vulnerability DB
    uses: ./.github/workflows/_trivy-db-update.yaml
  trivy_scan:
    needs: trivy_db_update
    runs-on: ubuntu-24.04
    steps:
      - name: Get current date
        id: date
        shell: bash
        run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          token: ${{secrets.GITHUB_TOKEN}}
          fetch-depth: 0
      - name: Get image tag
        id: get-image-tag
        shell: bash
        run: |
          # 何らかの方法でimage tagを取得、この例だとcommit hash
          IMAGE_TAG=${{ github.sha }}
          echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
     - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2
      - name: Manual Trivy Setup
        uses: aquasecurity/setup-trivy@v0.2.2
        with:
          cache: true
          version: v0.56.2
      - name: Set Up Trivy Cache
        id: trivy-cache
        uses: actions/cache@v4
        with:
          key: cache-trivy-${{ steps.date.outputs.date }}
          path: ${{ github.workspace }}/.cache/trivy
      - name: Echo Cache Hit
        shell: bash
        run: |
          echo "cache hit: ${{ steps.trivy-cache.outputs.cache-hit }}"
      - name: Run Trivy vulnerability scanner
        shell: bash
        run: |
          trivy image \
            --exit-code 1 \
            --ignore-unfixed \
            --severity CRITICAL,HIGH \
            --pkg-types os,library \
            --cache-dir ${{ github.workspace }}/.cache/trivy \
            --no-progress \
            --skip-db-update \
            --skip-java-db-update \
            --offline-scan \
            --skip-check-update \
            "${{ env.ecr-arn }}:${{ steps.get-image-tag.outputs.IMAGE_TAG }}"

Github Actionsとしてはちょい長いですが重要なのは以下の3点です。

  • 実行前にcacheがないと困るので先ほど作成した trivy-db-update の Github Actionsを呼び出す点
  • cache時のKeyと同じく日付(YYYY-MM-DD)の cache key で actions/cache からcacheを取得する点
  • Trivy の Option で DBの更新 を行わないでcacheを利用したOffline Scanするように設定する点

--cache-dir で cacheディレクトリを指定し

--skip-db-update --skip-java-db-update --offline-scan の指定でTrivy DBのダウンロードを抑制しながらcacheを利用するOffline Scanを指定しています。

--skip-check-update は cliの最新版チェックの無効化です。 aquasecurity/setup-trivy で事前に cli のBinary をダウンロードしcacheすることで毎回TrivyのBinaryをダウンロードしてこないようにしています。

ここまでやれば、Trivy DBのサーバー負荷を減らせるためTrivy DBに優しいGithub Actionsになったと思います!

めでたしめでたし!!!


アンドパッドでは CI/CDでピタゴラスイッチを書くのが楽しいGoエンジニアも絶賛募集中です!

hrmos.co