皆様こんにちは。SEの小池と申します。
前々回はコンテナレジストリ、前回はコンテナスキャンと、最近の本ブログはCI/CDパイプラインを使ったコンテナ関連処理祭りですが、今回はちょっとだけ応用編としてGitLabでコンテナスキャンを実装し、緊急性の高い脆弱性が無ければコンテナレジストリに登録させる処理を実装する話となります。
本記事の対象の方
- GitLabのAuto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否の処理を分岐させたい方。
- 前々回 及び 前回の本ブログをご覧になったうえで、ちょっと応用チックな処理を実装したい方。
今回のブログのゴール
このブログのゴールはこちらです。
- Auto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否を分岐させる。
このブログをお読みいただくにあたっての事前ご連絡事項
- 本記事はオンプレミス版 (Self-Managed) のGitLab Enterprise Edition 14.10.4-ee (Ultimate) における仕様をベースに記載しております。それ以外のエディションやバージョンではこの記事に記載の通りではない可能性がございます。
- 本記事は前回 (GitLab Auto DevOpsを使ったコンテナスキャンの実装) と 前々回 (GitLabのコンテナレジストリにイメージを登録する) を前提とした内容です。
- 本記事ではGitLabのAuto DevOpsにおけるコンテナスキャンと、GitLabのコンテナレジストリがどういったものなのかの説明は記載しておりません。これについては恐れ入りますがGitLab Docs (こちら) をご参照ください。
- 本記事のGitLabのGUIは日本語にローカライズした状態で掲載しております。それ以外の言語をご利用の方は適宜読み替えてください。
実装するナリオ
今回は以下のシナリオをGitLabで実現していこうと思います。
シナリオは3ステップで構成されます。
まずGitLabのCI/CDパイプランを使って任意のコンテナイメージをビルドします。
ビルドしたイメージを、Auto DevOpsのコンテナスキャンを使ってスキャンし、脆弱性があるかをチェックします。
Auto DevOpsのコンテナスキャンは結果をjson形式のファイルに出力します。 (アーティファクト)
スキャン結果が出力されたjson形式のファイル (アーティファクト) を参照し、脆弱性が何件あったかをチェックします。
脆弱性が0件だった場合は、GitLabのコンテナレジストリにこのコンテナイメージを登録します。
脆弱性が1件以上だった場合は、その旨のメッセージを出してCI/CDパイプラインのジョブを異常終了させます。
アーティファクトが無いなど、上記以外のケースの場合もCI/CDパイプラインのジョブを異常終了させます。
なお、今回の3つ目のシナリオの実装に際し、以下の.gitlab-ci.yml
ファイルの内容を参考にさせていただきました。
参考 : https://gitlab.cern.ch/gitlabci-examples/container_scanning/-/blob/master/.gitlab-ci.yml
事前準備
GitLabの Auto DevOps でコンテナスキャンを使うにあたっては以下いずれかのRunnnerが必要になります。
- エクゼキューターが
docker
のRunner - エクゼキューターが
kubernetes
のRunner - エクゼキューターが
shell
且つ Docker Engine がインストール済のRunner
お手元のGitLab環境にこれを満たすRunnerがない場合は、GitLab Docsや弊社の過去のブログをご参照いただき、事前にご準備をお願い致します。
Runnerのインストールに関するGitLab Docs : Install GitLab Runner | GitLab
弊社の技術ブログ : GitとCI/CDに関する知識ゼロのSEが、GitLabでRunner (Docker) を登録するだけの話
また、今回のシナリオではGitLabのコンテナレジストリを有効にする必要があります。
オンプレミス版 (Self-Managed) の場合に限りデフォルトでは有効になっていない場合があります。
その場合はGitLab Docs もしくは 弊社の技術ブログをご参照いただき、コンテナレジストリを有効にしてください。
コンテナレジストリに関するGitLab Docs : GitLab Container Registry | GitLab
弊社の技術ブログ : GitとCI/CDに関する知識ゼロのSEが、GitLabでCI/CDパイプラインを使ってコンテナレジストリにイメージを登録する話
シナリオの実装
シナリオ1 : CI/CDパイプラインを使って任意のイメージをビルドする
まずはシナリオの1つ目、CI/CDパイプラインを使って任意のイメージをビルドします。
このブログでは例として簡易なイメージをビルドしてみます。
既にお手元のGitLabで、スキャンしたいイメージをCI/CDパイプラインでビルドしていらっしゃる場合は、この手順はスキップして2個目のシナリオの実装から実施してください。
前回 及び 前々回 のブログと重複するため、不要な場合は読み飛ばしていただいて問題ありません。
まず、今回の検証のために空のプロジェクト (Blank Project) を作成します。
空のプロジェクトにDockerfile
と.gitlab-ci.yml
を追加します。
Dockerfile
の内容はご自由に設定なさってください。
以下にサンプルを載せますので、こちらをコピーしていただいても大丈夫です。
(コメントアウトしている2行目は重要度High以上の脆弱性が有るパターンを試すためのエントリです。)
FROM hello-world
#FROM centos/nginx-114-centos7:20201215-32cf52f
次に、.gitlab-ci.yml
を作成し、先ほどのDockerfile
を使ってイメージをビルドする処理を記載します。
サンプルは以下の通りです。
最終的にコンテナレジストリにイメージを登録することを念頭に、イメージのタグはGitLabのコンテナレジストリの命名規則に従っています。
そのままコピーしていただいても、アレンジしていただいても、どちらでも大丈夫です。
アレンジなさる場合はタグがGitLabのコンテナレジストリの命名規則を満たすように編集なさってください。
参考 : GitLab Container Registry | GitLab
stages: - build Build-SmapleJob: stage: build image: docker:20.10.17 variables: # ビルドイメージの変数作成 IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: # ビルド - docker build --tag $IMAGE .
変更をコミットし、mainブランチにDockerfile
と.gitlab-ci.yml
をマージしてください。
なおこの際、マージリクエストの作成は任意です。
この時点でパイプラインが成功しているかどうかを確認します。
対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスが 緑色で [成功] となっていることを確認します。
以上が1つ目のシナリオの実装でした。
なお、この時点ではイメージをビルドだけしてどこにも保存していませんが、最終的にコンテナレジストリに登録しますのでご安心ください。
シナリオ2 : ビルドしたイメージをAuto DevOpsを使ってスキャンする
前のシナリオでは、CI/CDパイプラインでイメージをビルドする処理を作成しました。
次のシナリオでは、そのイメージをGitLabのAuto DevOpsを使ってコンテナスキャンを実行します。
リポジトリに追加した.gitlab-ci.yml
ファイルを使って、コンテナスキャンを有効にします。
既存の.gitlab-ci.yml
にtestステージと、キーワードinclude
を使ってジョブを追加します。
(以下のサンプルの場合は3行目と、15~17行目を追加しています。)
stages: - build - test Build-SmapleJob: stage: build image: docker:20.10.17 variables: # ビルドイメージの変数作成 IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: # ビルド - docker build --tag $IMAGE . # イメージスキャン include: - template: Security/Container-Scanning.gitlab-ci.yml
上記のymlファイルでコンテナスキャンは可能ですが、これだとすべての重要度の脆弱性を検知してしまいます。
シナリオには緊急度の高い脆弱性という条件があるので、これを加えます。
具体的にはAuto DevOpsのテンプレートで作成されるcontainer_scanning
ジョブに対してCS_SEVERITY_THRESHOLD
という変数を使い、検知する脆弱性を限定します。
今回のシナリオでは緊急度の高い脆弱性が重要度 High 以上の脆弱性を指すものとし、.gitlab-ci.yml
ファイルに重要度High以上の脆弱性のみを検知するように設定します。
(以下のサンプルの場合は19~23行目を追加しています。)
stages: - build - test Build-SmapleJob: stage: build image: docker:20.10.17 variables: # ビルドイメージの変数作成 IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: # ビルド - docker build --tag $IMAGE . # イメージスキャン include: - template: Security/Container-Scanning.gitlab-ci.yml # イメージスキャンジョブのカスタム設定 container_scanning: variables: # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical CS_SEVERITY_THRESHOLD: High
参考 : Container Scanning | GitLab
変更をコミットし、mainブランチに.gitlab-ci.yml
をマージしてください。
この際、マージリクエストの作成は任意です。下図は差分を示した図です。
この時点でパイプラインが成功しているかどうかを確認します。
対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスが 緑色で [成功] となっていることを確認します。
以上が2つ目のシナリオの実装でした。
シナリオ3 : ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する
前のシナリオでは、ビルドしたイメージをコンテナスキャンでスキャンする処理を実装しました。
次のシナリオでは、コンテナスキャンの結果をチェックし脆弱性が0件だった場合はレジストリに登録する処理を実装します。
まず、ステージを追加し、前の手順で作成したジョブのアーティファクトを参照する処理を作成します。
(以下のサンプルの場合は4行目と、26行目以降を追加しています。)
stages: - build - test - push_image Build-Job: stage: build image: docker:20.10.17 variables: # ビルドイメージの変数作成 IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: # ビルド - docker build --tag $IMAGE . #イメージスキャン include: template: Container-Scanning.gitlab-ci.yml # イメージスキャンジョブのカスタム設定 container_scanning: variables: # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical CS_SEVERITY_THRESHOLD: High #イメージスキャンの結果に基づくレジストリへの登録 PushImage-Job: stage: push_image image: docker:20.10.17 script: - | apk add jq; export vuln_counts=$(jq -e "( .vulnerabilities | length )" ./gl-container-scanning-report.json); echo 脆弱性の数 : $vuln_counts; dependencies: - container_scanning
上のyamlファイルのジョブPushImage-Job
のscript
では以下を実行しています。
- 32行目:アーティファクトがjson形式なので、jqコマンドをインストールしています。
- 33行目:アーティファクトの中のキー
vulnerabilities
に入っているオブジェクト数を取得し、それを変数vuln_counts
に代入しています。 - 34行目:人間による目視確認用の出力。コンテナスキャンで見つかった脆弱性の数。
- 35, 36行目:アーティファクトをフェッチするジョブのリストの定義。
次に、アーティファクトから取得した脆弱性の数によって、コンテナイメージをレジストリに登録するかしないかの分岐処理を作成します。
今回のシナリオの場合、アーティファクトに出力されている脆弱性の数が0個であればレジストリに登録し、1個以上ある場合はレジストリには登録しません。
(以下のサンプルの場合は35~45行目を追加しています。)
stages: - build - test - push_image Build-Job: stage: build image: docker:20.10.17 variables: # ビルドイメージの変数作成 IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: # ビルド - docker build --tag $IMAGE . #イメージスキャン include: template: Container-Scanning.gitlab-ci.yml # イメージスキャンジョブのカスタム設定 container_scanning: variables: # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical CS_SEVERITY_THRESHOLD: High #イメージスキャンの結果に基づくレジストリへの登録 PushImage-Job: stage: push_image image: docker:20.10.17 script: - | apk add jq; export vuln_countss=$(jq -e "( .vulnerabilities | length )" ./gl-container-scanning-report.json); echo 脆弱性の数 : $vuln_counts; if [ "$vuln_counts" -gt "0" ]; then echo 脆弱性が1個以上あるため、コンテナレジストリに登録できません。\(exit code : 101\) exit 101 elif [ "$vuln_counts" -eq "0" ]; then echo 脆弱性が0個なので、コンテナレジストリに登録します。 docker login --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD $CI_REGISTRY docker push $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA else echo コンテナスキャン結果が不明な値のため、コンテナレジストリに登録できません。\(exit code : 102\) exit 102 fi dependencies: - container_scanning
上のyamlファイルのジョブPushImage-Job
のscript
では以下を実行しています。
- 35~37行目:脆弱性が1個以上あった場合の処理。コード101で異常終了させる。
- 38~41行目:脆弱性が0個だった場合の処理。コンテナレジストリにログインし、イメージをプッシュする。
- 42~45行目:上記のどちらにも当てはまらなかった場合の処理。コード102で異常終了させる。
変更をコミットしてください。これにより、追加したステージpush_image
とジョブPushImage-Job
が実行されます。
下図は差分を示した図です。(差分が多すぎて見切れていますが、実際は47行目まで更新しています。)
この時点でパイプラインが成功しているかどうかを確認します。
対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスのボタンをクリックします。
(パイプラインのステータスはスキャン対象のイメージにおける脆弱性の有無に依存するため、ここではあまり気になさらないでください。)
ジョブPushImage-Job
のステータスを確認します。
下図のように全てのジョブが成功している場合は、対象のコンテナイメージに重要度High以上の脆弱性が無く、且つ、コンテナレジストリに登録できているはずです。
この場合、プロジェクトの [パッケージとレジストリ] > [コンテナレジストリ] に、対象のコンテナイメージが登録されていることが確認できます。
ジョブPushImage-Job
が失敗している場合、[失敗したジョブ] タブからジョブの終了ステータスを簡易的に確認することができます。
下図の場合、スキャン対象イメージに重要度High以上の脆弱性が27個あったため、登録しなかった旨が表示されています。
下図の場合、コンテナスキャンジョブが失敗しているため、アーティファクトが作成されていません。
そのため、ジョブPushImage-Job
は脆弱性の数とは異なる理由 (yamlファイルで設定した exit code : 102) で終了しています。
以上が3つ目のシナリオ、緊急性の高い脆弱性が無ければコンテナレジストリに登録させる処理の実装でした。
なお、もし、脆弱性のあるイメージをすぐに思いつかない・・・といった場合は、こちらのサンプルで提示したDockerfile
において、1行目 (Hello worldのエントリ) をコメントアウトし、2行目のコメントを外し、再度CI/CDパイプラインを実行してください。
これでおそらく重要度High以上の脆弱性を1件以上検知するケースを試行することができます。
【余談】筆者が検討したけど断念した実装方法
今回のシナリオを実装するにあたり、筆者が「こんな方法で実装できないかな?」と思って検討したものの、色々な理由で実装を断念した方法をご紹介いたします。
コンテナスキャンの結果を変数で取得したい
これは筆者が
GitLabのことだから、コンテナスキャンの結果を変数で取得できるんじゃね?
と思ったことが発端です。
結論として、Auto DevOpsのコンテナスキャンの結果を取得できる変数は、2022年7月26日時点で存在しません。
実はこの「コンテナスキャンで検知した脆弱性の数を格納する変数」は変数名CS_VULNERABILITY_THRESHOLD
として実装が検討されていたらしいのですが、実装の優先度が下がってしまった模様です。
参考 : Add CS_VULNERABILITY_THRESHOLD var to Container Scanning (#213087) · Issues · GitLab.org / GitLab · GitLab
上記のような便利な変数が無いとわかったため、おとなしくjqコマンドをインストールし、アーティファクトを参照して、脆弱性の有無を確認する方法を採用致しました。
上記の参考URLのイシューのステータスはまだオープンなので、そのうち実装される・・・ことを願います。
コンテナスキャンで脆弱性を検知したらジョブの終了ステータスを緑 (正常) 以外にしたい
これは筆者が
そうすれば後続のジョブでいちいちアーティファクトの中身を確認する必要なくね?
と思ったことが発端です。
結論として、Auto DevOpsのテンプレートをインポートして自動作成されたコンテナスキャンジョブcontainer_scanning
の終了ステータスを、脆弱性の有無によって変更させることは難しいようです。
コンテナスキャンのジョブcontainer_scanning
においては、コンテナスキャンのスキャン自体が正常に終了しさえすれば、脆弱性の有無を問わず、ジョブは正常終了する仕様になっています。
ただし、SAST、DAST 及び Fuzzingについてはスキャンジョブにallow_failure
を設定することにより、脆弱性を検知した場合はそのスキャンジョブを正常終了以外のステータスで終了させることができる模様です。
参考 : 🎨 Design: Update secure job status with corresponding exit code with correct icons (#300415) · Issues · GitLab.org / GitLab · GitLab, Security scanner integration | GitLab
以上のことから、テンプレートによって自動作成されるコンテナスキャンジョブcontainer_scanning
の終了ステータスを、脆弱性の有無によって変化させることは断念致しました。無念・・・。
最後に
この度はGitもCI/CDもよくわかっていないど素人SEによるGitLab検証ブログをお読みいただき、誠にありがとうございます。
このブログの目標は以下のとおりでしたが、皆さまはいかがでしたでしょうか。
- Auto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否を分岐させる。
CI/CDパイプラインでビルドして、コンテナスキャンして、脆弱性が0件ならコンテナレジストリに登録して…と、簡単ではありながら、なかなかそれっぽい実装例となりました。
この記事がGitLabを触り始めた方の一助となれば幸いにございます。
GitLabに関するお問い合わせは、以下のフォームからお願い致します。
GitLab製品 お問い合わせ
GitLab操作デモ動画 (基本編) を作ってみました。(音声の録音は自宅でiPhoneのボイスメモ使うという超低クオリティですが…。)
つたない内容ではありますが、ご興味がおありでしたら是非ご視聴いただければと存じます。