はじめに

クラウドインテグレーション事業部 構築第六セクション所属の浅岡 冬馬です。

本記事では、Google Cloudのドキュメントに公開されているウイルススキャンの仕組みについて、実際にパイプラインを組んでみたので、構築の流れや動作のイメージ、ハマったところについて書いていきます。

今回使用するウイルス対策ソフトは ClamAV です。
ClamAV はオープンソースかつマルチプラットフォームで動作するウイルス対策ソフトです。
他のウイルス対策ソフトと比較し軽量なため、パフォーマンス影響を与えにくく、導入しやすいのが特徴です。

また、本記事にて記載のコマンドは全て Cloud Shell での実行を想定しています。

構成図

本構成図についても、ドキュメントに記載の内容を参考に作成しています。

上記の図で以下2つのパイプラインを示しています。

  • アップロードされたファイルにマルウェアが含まれていないかどうかを確かめるもの
  • マルウェアデータベースのミラー情報を更新するもの

作ってみた

以下記載の手順に関しても、ドキュメントに記載の手順に沿って作成しており、各手順について要所を説明します。

01. 環境設定

今後の手順で使用する値(リージョンやプロジェクトID)の設定を行います。
今回は、Cloud Run や Cloud Storage のリージョンとして asia-northeast1 を、Eventarc や Cloud Scheduler のロケーションとして asia を使用します。

01-1. 環境変数の設定

REGION=asia-northeast1
LOCATION=asia
PROJECT_ID=${PROJECT_ID}
SERVICE_NAME="malware-scanner"
SERVICE_ACCOUNT="${SERVICE_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

PROJECT_ID は実際のプロジェクトIDを入力します。

01-2. gcloud 環境の初期化

gcloud config set project "${PROJECT_ID}"

01-3. Cloud Storage バケットの作成

gcloud storage buckets create "gs://unscanned-${PROJECT_ID}" --location="${LOCATION}"
gcloud storage buckets create "gs://quarantined-${PROJECT_ID}" --location="${LOCATION}"
gcloud storage buckets create "gs://clean-${PROJECT_ID}" --location="${LOCATION}"
gcloud storage buckets create "gs://cvd-mirror-${PROJECT_ID}" --location="${LOCATION}"

これら4つのバケットはそれぞれ以下の役割を持ちます。

unscanned-${PROJECT_ID}:マルウェアスキャンが実行される前のファイルが格納されるバケット。ユーザはこのバケットにコンテンツをアップロードします。
quarantined-${PROJECT_ID}:マルウェアスキャンが実行され、 マルウェアが含まれている と判断されたファイルが格納されるバケット。
clean-${PROJECT_ID}:マルウェアスキャンが実行され、 マルウェアが含まれていない と判断されたファイルが格納されるバケット。
cvd-mirror-${PROJECT_ID}:ClamAV マルウェアデータベースのミラーが保管されるバケット。

02. サービスアカウントの設定

マルウェアスキャンサービスで使用するサービスアカウントを設定します。
作成したサービスアカウントが Cloud Storage に対して読み取りと書き込みの権限を持つように権限を付与します。

02-1. サービスアカウントの作成

gcloud iam service-accounts create ${SERVICE_NAME}

SERVICE_NAME 変数には 01-1 で指定したものを使用するため、「malware-scanner」という名前でサービスアカウントを作成します。
このサービスアカウントは、後の手順で作成するCloud Runに付与します。

02-2. バケットに対する権限付与

gcloud storage buckets add-iam-policy-binding "gs://unscanned-${PROJECT_ID}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" --role=roles/storage.objectAdmin
gcloud storage buckets add-iam-policy-binding "gs://clean-${PROJECT_ID}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" --role=roles/storage.objectAdmin
gcloud storage buckets add-iam-policy-binding "gs://quarantined-${PROJECT_ID}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" --role=roles/storage.objectAdmin
gcloud storage buckets add-iam-policy-binding "gs://cvd-mirror-${PROJECT_ID}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" --role=roles/storage.objectAdmin

02-3. 指標の書き込みロールの付与

gcloud projects add-iam-policy-binding \
  "${PROJECT_ID}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role=roles/monitoring.metricWriter

03. マルウェアスキャンサービスの作成

システムのコアとなるマルウェアスキャンサービスを Cloud Run を使用して作成します。

03-1. GitHub リポジトリのクローンを作成

git clone https://github.com/GoogleCloudPlatform/docker-clamav-malware-scanner.git

03-2. ディレクトリの移動

cd docker-clamav-malware-scanner/cloudrun-malware-scanner

03-3. config.json 構成ファイルを編集

sed "s/-bucket-name/-${PROJECT_ID}/" config.json.tmpl > config.json

バケットはプロジェクトIDを用いて作成しているため、置換を行うことで構成ファイルの編集が可能です。

03-4. データベースミラーの取り込み

python3 -m venv pyenv
. pyenv/bin/activate
pip3 install crcmod cvdupdate
./updateCvdMirror.sh "cvd-mirror-${PROJECT_ID}"
deactivate

cvd-mirror-${PROJECT_ID} にClamAV マルウェアデータベースの初期データをミラー用として取り込みます。
取り込みが完了した後は、gcloud storage ls コマンドで取り込みが完了しているか確認しておきます。

03-5. マルウェアスキャンサービスのデプロイ

gcloud beta run deploy "${SERVICE_NAME}" \
  --source . \
  --region "${REGION}" \
  --no-allow-unauthenticated \
  --memory 4Gi \
  --cpu 1 \
  --concurrency 20 \
  --min-instances 1 \
  --max-instances 5 \
  --no-cpu-throttling \
  --cpu-boost \
  --service-account="${SERVICE_ACCOUNT}"

ここでは、vCPU 1/メモリ 4GiB のCloud Run を作成します。
Cloud Run の名前は SERVICE_NAME 変数を使用することで、malware-scanner とします。
サービスアカウントには 02 の手順で作成したサービスアカウントを使用します。

サービスのデプロイには10分程度時間がかかることがあります。
完了後は、以下のようなメッセージが表示されます。

Service [malware-scanner] revision [malware-scanner-UNIQUE_ID] has been deployed and is serving 100 percent of traffic.
Service URL: https://malware-scanner-UNIQUE_ID.a.run.application

03-6. Service URL を変数に格納

SERVICE_URL=https://...run.app

この後の手順でこのURLを使用するため、03-5 で表示された Service URL を新たに変数に格納します。

04.Eventarc Cloud Storage トリガーの作成

Cloud Storage にコンテンツが格納されたことを検知し、マルウェアスキャンサービスを開始するためのトリガーを作成します。

04-1. Cloud Storage サービス アカウントに対して権限付与

STORAGE_SERVICE_ACCOUNT=$(gcloud storage service-agent --project="${PROJECT_ID}")

gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member "serviceAccount:${STORAGE_SERVICE_ACCOUNT}" \
  --role "roles/pubsub.publisher"

Cloud Storage のサービスアカウントに Pub/Sub パブリッシャーのロールを付与します。
これを行うことで、Cloud Storage イベントを Eventarc で検知することが可能になります。

04-2. malware-scanner サービスアカウントに対して権限付与

gcloud run services add-iam-policy-binding "${SERVICE_NAME}" \
  --region="${REGION}" \
  --member "serviceAccount:${SERVICE_ACCOUNT}" \
  --role roles/run.invoker
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member "serviceAccount:${SERVICE_ACCOUNT}" \
  --role "roles/eventarc.eventReceiver"

malware-scanner サービスアカウントが Cloud Run を呼び出す権限を付与します。
同時に、Eventarc からイベントを受け取る権限も付与します。
これを行うことで、Eventarc イベントを受け取った malware-scanner サービスが Cloud Run を開始できます。

04-3.  Eventarc トリガーの作成

BUCKET_NAME="unscanned-${PROJECT_ID}"
gcloud eventarc triggers create "trigger-${BUCKET_NAME}-${SERVICE_NAME}" \
  --destination-run-service="${SERVICE_NAME}" \
  --destination-run-region="${REGION}" \
  --location="${LOCATION}" \
  --event-filters="type=google.cloud.storage.object.v1.finalized" \
  --event-filters="bucket=${BUCKET_NAME}" \
  --service-account="${SERVICE_ACCOUNT}"

unscanned-${PROJECT_ID} バケットにコンテンツが格納されたことを検知して Cloud Run にイベントを送信するトリガーを作成します。

以下のようなエラーが発生した場合は、数分時間をおいて再度コマンドを実行します。

ERROR: (gcloud.eventarc.triggers.create) INVALID_ARGUMENT: The request was invalid: Bucket "unscanned-PROJECT_ID" was not found. Please verify that the bucket exists.
ERROR: (gcloud.eventarc.triggers.create) FAILED_PRECONDITION: Invalid resource state for "": Permission denied while using the Eventarc Service Agent. If you recently started to use Eventarc, it may take a few minutes before all necessary permissions are propagated to the Service Agent. Otherwise, verify that it has Eventarc Service Agent role.

04-4. Pub/Sub サブスクリプションの調整

SUBSCRIPTION_NAME=$(gcloud eventarc triggers describe \
"trigger-${BUCKET_NAME}-${SERVICE_NAME}" \
  --location="${LOCATION}" \
  --format="get(transport.pubsub.subscription)")
gcloud pubsub subscriptions update "${SUBSCRIPTION_NAME}" --ack-deadline=120

Eventarc トリガーが使用する Pub/Sub サブスクリプションのメッセージ確認応答期限ですが、デフォルトでは10秒となっています。
本環境では10秒だと短く、エラーの原因となる可能性があるため2分に変更しておきます。

05. Cloud Scheduler ジョブの作成

最新のマルウェアに対応するため、ClamAV マルウェアデータベースのミラーは常に最新である必要があります。
ここでは、ClamAV マルウェアデータベースのミラーを定期的に更新する Cloud Scheduler を作成します。

gcloud scheduler jobs create http \
  "${SERVICE_NAME}-mirror-update" \
  --location="${REGION}" \
  --schedule="0 */12 * * *" \
  --oidc-service-account-email="${SERVICE_ACCOUNT}" \
  --uri="${SERVICE_URL}" \
  --http-method=post \
  --message-body='{"kind":"schedule#cvd_update"}' \
  --headers="Content-Type=application/json"

データベースのミラーの定期実行の間隔は12時間としています。
これは、ClamAVデータベースが1日に1~2回更新されるためです。

動作確認

最後に構築したパイプラインの動作確認を行います。
テスト用に準備した、クリーンなファイル と EICAR標準のマルウェアファイル を unscanned-${PROJECT_ID} に格納し動きを確認します。

クリーンなファイルの作成

vi clean.txt

clean.txt ファイルにはテスト用と分かる適当な値を記載しておきます。

クリーンなファイルのテスト

以下図に記載の経路イメージのテストを行います。

gcloud storage cp clean.txt "gs://unscanned-${PROJECT_ID}"

06-1 で作成したテストファイルを unscanned-${PROJECT_ID} バケットにコピーします。
数分後以下コマンドを実行し、適切にファイルが処理されたことを確認します。

gcloud storage ls "gs://clean-${PROJECT_ID}" --recursive
gcloud storage ls "gs://unscanned-${PROJECT_ID}" --recursive

unscanned-${PROJECT_ID} バケットから clean-${PROJECT_ID} バケットへコンテンツが移動されていればクリーンなファイルのテストは完了です。

マルウェアファイルのテスト

以下図に記載の経路イメージのテストを行います。

echo -e 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' \
| gcloud storage cp - "gs://unscanned-${PROJECT_ID}/eicar-infected.txt"

EICAR標準のマルウェア対策テスト署名を含む、eicar-infected.txt というファイルを unscanned-${PROJECT_ID} バケットにコピーします。
こちらの文字列は、テスト目的でマルウェアスキャンサービスをトリガーする署名が含まれていますが、実際のマルウェアが含まれている訳ではなくサービスには影響のないものです。

数分後以下コマンドを実行し、適切にファイルが処理されたことを確認します。

gcloud storage ls "gs://quarantined-${PROJECT_ID}" --recursive
gcloud storage ls "gs://unscanned-${PROJECT_ID}" --recursive

unscanned-${PROJECT_ID} バケットから quarantined-${PROJECT_ID} バケットへコンテンツが移動されていればマルウェアファイルのテストは完了です。

ハマったところ

テスト用に環境を構築する際、可能な限り最小構成で運用したく 03-5 で作成する Cloud Run の vCPU/メモリ を最小にし作成を行いました。
Cloud Run リソースの作成は完了されましたが、Cloud Scheduler 定期実行が失敗するというエラーが発生しました。

原因は、Clam AV の動作要件として、vCPU 1/メモリ 3GiB 以上が推奨されており、これを満たしていないことでした。
https://docs.clamav.net/

これを踏まえ、vCPU 1/メモリ 4GiB で再度作成を行ったところ、Cloud Scheduler 定期実行が正常に稼働しました。
本記事の構成でマルウェアスキャン環境を作成する場合は、Cloud Run のvCPU/メモリ設定の値に注意が必要です。

【参考】

Cloud Storage にアップロードされたファイルのマルウェア スキャンを自動化する
https://cloud.google.com/architecture/automate-malware-scanning-for-documents-uploaded-to-cloud-storage?hl=ja