ECR上のコンテナイメージを脆弱性スキャンするGitLabCI/CDを構築してみた
お疲れさまです。とーちです。
AWS ECR(Elastic Container Registry)にプッシュされたコンテナイメージを脆弱性スキャンするパイプラインをGitLab CI/CDで構築してみました。
ECRにイメージがプッシュされるとEventBridgeを通じてGitLabのパイプラインがトリガーされ、Sysdig CLI Scannerを使って脆弱性スキャンを実行します。スキャンに合格したイメージだけを「検証済みリポジトリ」に転送する仕組みです。
この記事ではGitLab CI/CDのワークフローファイルの説明を中心に紹介します。
前提条件
今回は以下の環境で実装したので、前提として記載しておきます。
- GitLabのバージョン: GitLab Community Edition 17.6
- GitLabのホスティング: セルフホステッド環境(社内GitLab)
- GitLab Runner: セルフマネージドRunner(Docker executorを使用)
- ネットワーク環境: GitLab RunnerからAWS ECRおよびSysdig APIへのアクセスが可能であること
また、脆弱性スキャンにはSysdig CLI Scannerを使っているのでSysdigを購入する必要ありです。
それでは、実際のコードを見ていきましょう。
GitLab CI/CDの設定コード
まずは.gitlab-ci.yml
の全体を見てみましょう。
stages:
- test
variables:
ECR_PUSHED_REPOSITORY_NAME:
ECR_PUSHED_IMAGE_TAG:
ENV: "dev" # 各種リソース名に付与されている環境名
AWS_REGION: ap-northeast-1
AWS_ACCOUNT_ID: XXXXXXXXXXXX
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "trigger"
when: always
- when: never # その他のトリガーでは実行しない
container_scan:
stage: test
# ジョブ実行環境のコンテナイメージを指定
image: docker:24.0
# ジョブ実行環境でメインコンテナが補助的に使用するコンテナイメージを指定
# ここではDocker in Dockerを使用するため、docker:24.0-dindを指定
services:
- name: docker:24.0-dind
alias: docker
command: ["--tls=false"]
variables:
# Docker in Docker関連の環境変数設定
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: "tcp://docker:2375"
DOCKER_DRIVER: overlay2
FF_NETWORK_PER_BUILD: "true"
# AWS関連の設定
ECR_PUSHED_REPOSITORY_NAME: test-${ENV}-from-repository
TARGET_REPOSITORY_NAME: test-${ENV}-to-repository
# 事前に用意されたIAMロールのARNを指定
ASSUME_ROLE_ARN: arn:aws:iam::${AWS_ACCOUNT_ID}:role/test-${ENV}-runner-role01
# Sysdig Secure関連設定
SYSDIG_SECURE_ENDPOINT: "https://us2.app.sysdig.com"
before_script:
# 必要なツールのインストール
- apk --no-cache add curl unzip jq
# AWS CLIのインストール
- apk --update add aws-cli
- aws sts get-caller-identity
# 事前に用意されたIAMロールにスイッチしIAM権限を取得
- |
CREDENTIALS=$(aws sts assume-role \
--role-arn $ASSUME_ROLE_ARN \
--role-session-name "GitLabSession")
export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.Credentials.SessionToken')
- aws sts get-caller-identity
# ECRログイン
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
# SECURE_API_TOKENはGitLab CI/CD変数から取得
- export SECURE_API_TOKEN=$SECURE_API_TOKEN
script:
# 指定されたコンテナイメージをpull
- |
echo "Pulling specified container image: $ECR_PUSHED_IMAGE_TAG"
if docker pull ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}; then
echo "Successfully pulled $ECR_PUSHED_IMAGE_TAG"
echo "### docker images result ###"
docker images | grep $(echo $ECR_PUSHED_IMAGE_TAG | cut -d: -f1)
else
echo "Failed to pull $ECR_PUSHED_IMAGE_TAG"
exit 1
fi
# コンテナイメージの脆弱性スキャン
- curl -LO https://download.sysdig.com/scanning/bin/sysdig-cli-scanner/$(curl -L -s https://download.sysdig.com/scanning/sysdig-cli-scanner/latest_version.txt)/linux/amd64/sysdig-cli-scanner
- chmod +x ./sysdig-cli-scanner
- ./sysdig-cli-scanner --console-log --apiurl $SYSDIG_SECURE_ENDPOINT ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}
# コンテナイメージタグ付け
- |
ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${TARGET_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}"
echo "Tagging image for ECR: $ECR_IMAGE"
# ECRにPush
- |
docker tag ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG} $ECR_IMAGE
echo "Pushing to ECR: $ECR_IMAGE"
docker push $ECR_IMAGE
# 結果確認
- |
echo "### Pushed image details ###"
aws ecr describe-images --repository-name $TARGET_REPOSITORY_NAME --image-ids imageTag=$ECR_PUSHED_IMAGE_TAG
それでは、このCI/CDパイプラインの各部分を詳しく見ていきましょう。
パイプラインの詳細解説
ステージとワークフロー設定
stages:
- test
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "trigger"
when: always
- when: never # その他のトリガーでは実行しない
stagesはパイプラインの中のジョブの順序を指定するための定義ですが、このパイプラインでは、シンプルにtestというステージのみを定義しています。
workflowはパイプライン実行の制御をするための定義で、ここでは実行条件を指定しており、$CI_PIPELINE_SOURCE == "trigger"
でトリガートークンで起動されたパイプラインのみが実行されるようになっています。
このパイプラインは実際にはLambdaから実行しています。LambdaからGitLab APIの実行については以前、記事を書いたので以下をご参照ください。
変数設定
variables:
ECR_PUSHED_REPOSITORY_NAME:
ECR_PUSHED_IMAGE_TAG:
ENV: "dev" # 各種リソース名に付与されている環境名
AWS_REGION: ap-northeast-1
AWS_ACCOUNT_ID: XXXXXXXXXXXX
ここでは、パイプライン全体で使用するグローバル変数を定義しています。ECR_PUSHED_REPOSITORY_NAME
とECR_PUSHED_IMAGE_TAG
は、外部からGitLab CIパイプラインを呼び出す際に渡される値を格納するための変数です。具体的には今回はLambdaから呼び出しを行うのでLambdaのHTTPリクエストの中のフォームデータで変数の値を指定しています。
※Lambdaからの呼び出し部分のご参考
params = {
'token': token,
'ref': REF_NAME,
'variables[ECR_PUSHED_REPOSITORY_NAME]': ecr_pushed_repository_name,
'variables[ECR_PUSHED_IMAGE_TAG]': ecr_pushed_image_tag
}
response = http.request(
'POST',
url,
fields=params,
retries=urllib3.Retry(3),
timeout=urllib3.Timeout(connect=5.0, read=10.0)
)
実行環境の設定
# ジョブ実行環境のコンテナイメージを指定
image: docker:24.0
# ジョブ実行環境でメインコンテナが補助的に使用するコンテナイメージを指定
# ここではDocker in Dockerを使用するため、docker:24.0-dindを指定
services:
- name: docker:24.0-dind
alias: docker
command: ["--tls=false"]
このジョブはdocker:24.0
イメージ上で実行され、Docker in Dockerを使用するためにdocker:24.0-dind
サービスを利用しています。DockerinDockerを使用することで、GitLabランナー内でDockerコマンドを実行し、コンテナイメージの操作が可能になります。
AWS関連の設定
# AWS関連の設定
ECR_PUSHED_REPOSITORY_NAME: test-${ENV}-from-repository
TARGET_REPOSITORY_NAME: test-${ENV}-to-repository
# 事前に用意されたIAMロールのARNを指定
ASSUME_ROLE_ARN: arn:aws:iam::${AWS_ACCOUNT_ID}:role/test-${ENV}-runner-role01
ここでは、ソースとなるECRリポジトリ名と、スキャン後にイメージを転送する先のリポジトリ名を指定しています。
また、ASSUME_ROLE_ARN
では、GitLab RunnerがAWSリソースにアクセスするために一時的に引き受けるIAMロールを指定しています。ロールには、ECRからのイメージのプル・プッシュ権限をつけておきます。GitLabランナーが動くEC2自体にIAMロールでECR関連の権限を付与することも可能ですが、EC2自体のIAMロールとして付与してしまうと、他のプロジェクトにおいても同様の権限が付与されることになり、最小権限の原則からは離れてしまいます。そのため、EC2が持つIAMロール自体にはsts:AssumeRole権限だけをつけておき、そこからスイッチロールでプロジェクトごとに異なるロールを使用させる方式にしました。
前処理(before_script)
before_script:
# 必要なツールのインストール
- apk --no-cache add curl unzip jq
# AWS CLIのインストール
- apk --update add aws-cli
- aws sts get-caller-identity
ジョブの実行前に必要なツール(curl、unzip、jq、AWS CLI)をインストールしています。これらのツールは後続の処理で使用されます。amazon/aws-cli
コンテナイメージ等は、AWS CLIがデフォルトで入っているので、こういったイメージを使っても良かったのかもとも思います。
IAMロールの引き受け
# 事前に用意されたIAMロールにスイッチしIAM権限を取得
- |
CREDENTIALS=$(aws sts assume-role \
--role-arn $ASSUME_ROLE_ARN \
--role-session-name "GitLabSession")
export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.Credentials.SessionToken')
- aws sts get-caller-identity
ここでは、aws sts assume-role
コマンドを使用して、事前に定義したIAMロールを一時的に引き受けています。このコマンドの結果から、一時的な認証情報(アクセスキー、シークレットキー、セッショントークン)を環境変数として設定しています。これによってECRへの各権限を取得しています。
ECRログインとSysdig API設定
# ECRログイン
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
# SECURE_API_TOKENはGitLab CI/CD変数から取得
- export SECURE_API_TOKEN=$SECURE_API_TOKEN
AWS CLIを使用してECRの認証トークンを取得し、それを使ってDockerがECRにアクセスできるようにログインしています。また、Sysdig SecureのAPIトークンはGitLab CI/CD変数から取得することでこのファイル自体に機微な情報を持たせないようにしています。
コンテナイメージのプル
script:
# 指定されたコンテナイメージをpull
- |
echo "Pulling specified container image: $ECR_PUSHED_IMAGE_TAG"
if docker pull ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}; then
echo "Successfully pulled $ECR_PUSHED_IMAGE_TAG"
echo "### docker images result ###"
docker images | grep $(echo $ECR_PUSHED_IMAGE_TAG | cut -d: -f1)
else
echo "Failed to pull $ECR_PUSHED_IMAGE_TAG"
exit 1
fi
ここは説明不要かと思いますが、スキャン対象のコンテナイメージをECRからプルしています。
脆弱性スキャンの実行
# コンテナイメージの脆弱性スキャン
- curl -LO https://download.sysdig.com/scanning/bin/sysdig-cli-scanner/$(curl -L -s https://download.sysdig.com/scanning/sysdig-cli-scanner/latest_version.txt)/linux/amd64/sysdig-cli-scanner
- chmod +x ./sysdig-cli-scanner
- ./sysdig-cli-scanner --console-log --apiurl $SYSDIG_SECURE_ENDPOINT ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}
Sysdig CLI Scannerの最新バージョンをダウンロードし、実行権限を付与してから、スキャン対象のイメージに対して脆弱性スキャンを実行しています。--console-log
オプションでコンソールに詳細なログを出力し、--apiurl
でSysdig SecureのエンドポイントURLを指定しています。ここでは使ってませんが、--policy
オプションで使用するスキャンポリシーを指定することもできます。
このコマンドが成功した場合のみ次の処理に進むため、スキャンに失敗した場合(脆弱性スキャンポリシーに合格しなかった場合)は、後続の処理は実行されません。つまり、検証済みリポジトリへのイメージ転送は行われないということです。
イメージのタグ付けとプッシュ
# コンテナイメージタグ付け
- |
ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${TARGET_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG}"
echo "Tagging image for ECR: $ECR_IMAGE"
# ECRにPush
- |
docker tag ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_PUSHED_REPOSITORY_NAME}:${ECR_PUSHED_IMAGE_TAG} $ECR_IMAGE
echo "Pushing to ECR: $ECR_IMAGE"
docker push $ECR_IMAGE
スキャンに合格したイメージに対して、検証済みリポジトリ用の新しいタグを付け、そのリポジトリにプッシュしています。これにより、脆弱性チェックに合格したイメージだけが検証済みリポジトリに格納されるようになります。
結果確認
# 結果確認
- |
echo "### Pushed image details ###"
aws ecr describe-images --repository-name $TARGET_REPOSITORY_NAME --image-ids imageTag=$ECR_PUSHED_IMAGE_TAG
最後に、プッシュしたイメージの詳細情報をAWS CLIで取得して表示しています。
まとめ
今回は、AWS ECRにプッシュされたコンテナイメージを自動的に脆弱性スキャンし、安全性が確認されたイメージだけを検証済みリポジトリに転送するパイプラインをGitLab CI/CDで構築しました。
誰かの参考になれば幸いです。
以上、とーちでした。