TECHSTEP

ITインフラ関連の記事を公開してます。

GitLab Container Scanningを実施する

今回はGitLabの提供するコンテナイメージのスキャン機能を検証します。

docs.gitlab.com

背景

GitLabのコンテナスキャン機能は、GitLab CI/CDでコンテナイメージスキャンのJobを実行し、レジストリに配置したイメージを検査して結果を出力します。

GitLabのコンテナスキャン機能は Trivy / Grype と統合しており、どちらかを利用、あるいはその他のツールを統合することも可能です。

※GrypeのサポートはGitLab 17.0で廃止されます

本機能を利用する方法は2通りあります。今回は1つ目の方法を検証します。

  • .gitlab-ci.ymlinclude:template を追加し、Jobを呼び出す
  • Auto DevOpsの提供するコンテナスキャンを使用する

コンテナスキャン機能を利用するには、いくつかの前提条件があります。

  • GitLab CI/CD上で実行する場合は test ステージで実行するため、 test ステージを含める必要があります。
  • GitLab Runnerは Docker / Kubernetes (Linux/amd64) のいずれかに対応しています。
  • Dockerは 18.09.03 以上のバージョンが必要です。Shared runnerを利用する場合は考慮不要です。
  • スキャン対象のコンテナは、サポートするディストリビューションとマッチしている必要があります。
  • スキャン対象はコンテナレジストリに配置されたものなので、CI/CDの中でBuild / Pushをする必要があります。
  • GitLabのコンテナレジストリ以外の外部レジストリを利用する場合、アクセスするための認証情報が必要となります。

検証

ここからコンテナスキャン機能を検証します。検証はGitLab SaaS版 (Freeプラン) で行っています。

GitLab Registryに配置したイメージをスキャン

まずはGitLab Registryに配置するイメージに対するスキャンを検証します。ここでは簡単なプログラムを含んだイメージをビルドし、それに対するスキャンを実行します。

利用する .gitlab-ci.yml は以下の通りです。

stages:
  - build
  - test

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

container_scanning:
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

パイプラインを実行すると、 test ステージで container_scanning というJobが実行されます。

Jobの結果を見ると、スキャン結果を確認できます。

このJobの実行結果はアーティファクトとして保存されます。ここでは以下のファイルが格納されます。

  • artifacts.zip
  • metadata.gz
  • job.log

ファイルの中身の一部を載せておきます。

gl-container-scanning-report.json

{
    "vulnerabilities": [
                ...
        {
            "id": "cc2bfb8e198e16ce133e23745de34a762c89263e",
            "severity": "High",
            "location": {
                "dependency": {
                    "package": {
                        "name": "libcrypto1.1"
                    },
                    "version": "1.1.1q-r0"
                },
                "operating_system": "alpine 3.16.2",
                "image": "registry.gitlab.com/fy0323/container-scan-test:faa4979e"
            },
            "identifiers": [
                {
                    "type": "cve",
                    "name": "CVE-2023-0286",
                    "value": "CVE-2023-0286",
                    "url": "https://access.redhat.com/errata/RHSA-2023:2165"
                }
            ],
            "links": [
                {
                    "url": "https://access.redhat.com/errata/RHSA-2023:2165"
                },
                                ...
            ],
            "details": {
                "vulnerable_package": {
                    "name": "Vulnerable Package",
                    "type": "text",
                    "value": "libcrypto1.1:1.1.1q-r0"
                }
            },
            "description": "There is a type confusion vulnerability relating to X.400 address processing\ninside an X.509 GeneralName. X.400 addresses were parsed as an ASN1_STRING but\nthe public structure definition for GENERAL_NAME incorrectly specified the type\nof the x400Address field as ASN1_TYPE. This field is subsequently interpreted by\nthe OpenSSL function GENERAL_NAME_cmp as an ASN1_TYPE rather than an\nASN1_STRING.\n\nWhen CRL checking is enabled (i.e. the application sets the\nX509_V_FLAG_CRL_CHECK flag), this vulnerability may allow an attacker to pass\narbitrary pointers to a memcmp call, enabling them to read memory contents or\nenact a denial of service. In most cases, the attack requires the attacker to\nprovide both the certificate chain and CRL, neither of which need to have a\nvalid signature. If the attacker only controls one of these inputs, the other\ninput must already contain an X.400 address as a CRL distribution point, which\nis uncommon. As such, this vulnerability is most likely to only affect\napplications which have implemented their own functionality for retrieving CRLs\nover a network.\n\n",
            "solution": "Upgrade libcrypto1.1 to 1.1.1t-r0"
        },
                ...

外部レジストリに配置したイメージをスキャン

docs.gitlab.com

次は、Amazon ECRに格納したイメージをスキャンする例を実施します。今回は .gitlab-ci.ecr.yml という別の定義ファイルを用意し、CI/CDの設定を修正します。以前のAmazon ECSサービスを更新する記事の設定を流用しています。

.gitlab-ci.ecr.yml

stages:
  - build
  - test

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  before_script:
    - apk add --no-cache aws-cli
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn ${AWS_IAM_ROLE}
      --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token ${GITLAB_OIDC_TOKEN}
      --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text))
  script:
    - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ECR_LOGIN_URL
    - docker build -t $AWS_ECR_REPOSITORY:$CI_COMMIT_SHORT_SHA .
    - docker push $AWS_ECR_REPOSITORY:$CI_COMMIT_SHORT_SHA

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

container_scanning:
  before_script:
    - ruby -r open-uri -e "IO.copy_stream(URI.open('https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip'), 'awscliv2.zip')"
    - unzip -q awscliv2.zip
    - sudo ./aws/install
    - aws --version
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn ${AWS_IAM_ROLE}
      --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token ${GITLAB_OIDC_TOKEN}
      --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text))
    - export AWS_ECR_PASSWORD=$(aws ecr get-login-password --region $AWS_DEFAULT_REGION)
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  variables:
    CS_IMAGE: $AWS_ECR_REPOSITORY:$CI_COMMIT_SHORT_SHA
    CS_REGISTRY_USER: AWS
    CS_REGISTRY_PASSWORD: $AWS_ECR_PASSWORD
    AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION

ただし、GitLabから外部レジストリ上のイメージをスキャンするために、IAMロールに権限を追加する必要があります。今回は以下の2つの権限を追加しています。

  • ecr:BatchGetImage
  • ecr:GetDownloadUrlForLayer

また、 .gitlab-ci.ecr.yml で使用する変数も追加しておきます。

設定が正しければ、パイプラインも問題なく実行されます。スキャン結果は先ほどと同様なので割愛します。

開発言語向けスキャンの有効化

docs.gitlab.com

GitLabのコンテナスキャンは、デフォルトではOSのパッケージに関する脆弱性のみを報告します。 CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN というCI/CD変数を false にセットすることで、開発言語に関する脆弱性も報告します。

ここでは .gitlab-ci.language.yml という定義ファイルを用意します。

.gitlab-ci.language.yml

stages:
  - build
  - test

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

container_scanning:
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "false"

また、ここでは脆弱性を検知するため、こちらの記事にあるDockerfileを使用しました。

Dockerfileを修正してパイプラインを実行します。

スキャン結果を確認すると、確かにGo言語に関する脆弱性が報告されています。

Grypeの利用

docs.gitlab.com

GitLabのコンテナスキャンは、デフォルトではTrivyを利用します。 CS_ANALYZER_IMAGE で利用するイメージを指定すると、Grypeも利用できます。

ここでは .gitlab-ci.grype.yml に定義します。

stages:
  - build
  - test

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

container_scanning:
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    CS_ANALYZER_IMAGE: registry.gitlab.com/security-products/container-scanning/grype:6

修正後にパイプラインを実行した結果を見ると、確かにTrivyとは異なる結果が表示されました。

その他

GitLab Ultimateプランを利用すると、以下の機能を利用できます。

  • 脆弱性レポートの表示先が増える: Merge Request や CI/CDパイプラインJobの Security タブから、脆弱性レポートを確認できます。
  • 脆弱性に対する自動修復: 脆弱性に対する修復案をGitLabが提案し、それを利用して修復できます。
  • 脆弱性の許可リスト: 脆弱性を許容するCVE IDをリストに登録し、検知を抑制できます。
  • セキュリティダッシュボード: 脆弱性チェックの結果を評価するためのダッシュボードが利用できます。
  • 依存関係のページ: プロジェクト・グループ間やSBoMによる依存関係のリストを利用できます。