S3のマルウェアチェックをGuardDutyにやってもらったらミスった
この記事は MICIN Advent Calendar 2024 の 18日目の記事です。 前回はboosun13さんの、「増え続けるユーザーを支えるためのRails開発について」 でした。
1.はじめに
株式会社MICINのセキュリティチームのkurosawaです。
MICINのプロダクトに対して、セキュリティ絡みの対応をしたり、インフラ関連の作業をしています。
2024年7月に、GuardDutyの保護プランを有効化した記事を書きました。
この中で、当時新機能だったGuardDuty Malware Protection for S3(以下マルウェアプロテクション)にも触れたのですが、その時は有効化する余裕がありませんでした。
GuardDutyのマルウェアプロテクションについては、awsの紹介ページに詳細があります。
簡単にいうと、S3にアップロードされるオブジェクトのマルウェアスキャンを行ってくれる機能です。
以前のMICINではマルウェアスキャンにClamAVを利用しており、主な処理にはlambdaを使っていました。
しかし、使っているlambdaのランタイムがpython3.7であり、既にサポート切れを迎えているため、これを機にマルウェアプロテクションに置き換える事になりました。
今回の記事では、MICINでGuardDutyのマルウェアプロテクション機能を有効化した際の諸々を記録していきます。
特長
MICINにとってのマルウェアプロテクション導入メリットを記載します。
-
マネージドサービス
パターンファイルの更新や動作プラットフォームのアップデートを気にする必要がないので、日々の運用負荷が軽減されます、素敵です。 -
マルウェアスキャンでの検査が可能
ファイルのアップロードをトリガーにスキャンが可能です、頼もしい。 -
GuardDutyの一機能として動作
イベント管理や通知の仕組みを別途作る必要がなく、GuardDutyの仕組みをそのまま使う事が可能です、楽です。
前回の記事に記載した通り、GuardDutyはAWS環境でセキュリティ的に怪しげな動きを検知、通知してくれる機能です。
MICINの環境では、GuardDutyの枠組みを流用できる3点目は大きなメリットです。
あわせてマルウェアプロテクションを有効化するために作成するリソースを最小限に抑えられるのも嬉しいですね。
注意点
-
アカウントが多いと有効化が面倒
対象となるアカウント毎にチェック対象となるS3バケットを指定して有効化する必要があります。
各アカウント毎、対象バケット毎に作業が必要なので、その数が多い場合は手間が増えます。 -
スケジュールスキャンは実施不可
スキャンはファイルがアップロードされたタイミングのみです。
機能を有効化する前に保存されたファイルへのスキャンや、時間を決めてS3バケット全体をスキャンといった事は行えません。
MICINの環境では、複数のAWSアカウントをOrganizationにより統合管理しています。
GuardDutyもこの機能の恩恵を受けており、新規でアカウントを登録すれば自動的にGuardDutyは有効化され、その他の保護プランもAuditアカウントで一元管理されます。
しかしマルウェアプロテクションの有効化にはこの仕組みが適用できず、個々のアカウントでの作業が必要です。
対象アカウントが多い場合はそれなりに作業量が増えてしまいます。
コスト
マルウェアプロテクションのコストについてはAWSのコストページに記載されています。
その内訳は以下2つのコストの合計です。
(1) スキャンリクエストの回数
アップロードされたファイルをスキャンした回数により発生する料金です。
2024年12月時点の東京リージョンでは、$0.282/1000回となります。
(2) スキャンサイズ
アップロードされたファイルサイズにより発生する料金です。
2024年12月時点の東京リージョンでは、$0.78/GBとなります。
計算式の例
例:1ヶ月のファイルのアップロード回数が5000回、その合計サイズが10GBの場合
(5000回 / 1000回 * $0.282) + (10GB * $0.78) = $9.21
2.実装方法
GuardDutyは既に有効化されているため、実際に必要な設定は何と3つだけです。
マルウェアプロテクションに必要な設定は3つあります
例によってterraformを使ったコード化を行います。
まずはマルウェアプロテクション自体の設定です。
マルウェアプロテクションの作成
マルウェアプロテクションのリソースの中で、S3バケットの指定、スキャン時のタグ付け動作、適用するロールを指定しています。
resource "aws_guardduty_malware_protection_plan" "bucket_protection" {
role = "適用するロールのarn"
protected_resource {
s3_bucket {
bucket_name = "対象S3バケット名"
}
}
actions {
tagging {
status = "ENABLED"
}
}
tags = {
"Name" = "bucket_protection"
}
}
次にマルウェアプロテクションを動作させるロールとポリシーを設定します。
マルウェアプロテクションに適用するロールとポリシーの作成
ロールとポリシーの作成、ロールへのポリシーのアタッチを行います。
data "aws_iam_policy_document" "mw_protection_policy" {
statement {
sid = "AllowManagedRuleToSendS3EventsToGuardDuty"
actions = [
"events:PutRule",
"events:DeleteRule",
"events:PutTargets",
"events:RemoveTargets"
]
resources = [
"arn:aws:events:ap-northeast-1:`awsアカウントID`:rule/*"
]
condition {
test = "StringLike"
variable = "events:ManagedBy"
values = ["malware-protection-plan.guardduty.amazonaws.com"]
}
}
statement {
sid = "AllowGuardDutyToMonitorEventBridgeManagedRule"
actions = [
"events:DescribeRule",
"events:ListTargetsByRule"
]
resources = [
"arn:aws:events:ap-northeast-1:`awsアカウントID`:rule/*"
]
condition {
test = "StringLike"
variable = "events:ManagedBy"
values = ["malware-protection-plan.guardduty.amazonaws.com"]
}
}
statement {
sid = "AllowPostScanTag"
actions = [
"s3:PutObjectTagging",
"s3:GetObjectTagging",
"s3:PutObjectVersionTagging",
"s3:GetObjectVersionTagging"
]
resources = [
"arn:aws:s3:::`対象S3バケット名`/*"
]
}
statement {
sid = "AllowEnableS3EventBridgeEvents"
actions = [
"s3:PutBucketNotification",
"s3:GetBucketNotification"
]
resources = [
"arn:aws:s3:::`対象S3バケット名`"
]
}
statement {
actions = [
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:GetBucketPolicy",
"s3:GetBucketAcl",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPublicAccessBlock"
]
resources = [
"arn:aws:s3:::`対象S3バケット名`"
]
}
statement {
sid = "AllowPutValidationObject"
actions = [
"s3:PutObject"
]
resources = [
"arn:aws:s3:::`対象S3バケット名`/malware-protection-resource-validation-object"
]
}
statement {
sid = "AllowMalwareScan"
actions = [
"s3:GetObject",
"s3:GetObjectVersion"
]
resources = [
"arn:aws:s3:::`対象S3バケット名`/*"
]
}
statement {
sid = "AllowDecryptForMalwareScan"
actions = [
"kms:GenerateDataKey",
"kms:Decrypt"
]
resources = [
"arn:aws:kms:ap-northeast-1:`awsアカウントID`:key/*"
]
condition {
test = "StringLike"
variable = "kms:ViaService"
values = ["s3.*.amazonaws.com"]
}
}
}
resource "aws_iam_policy" "mw_protection_policy" {
name = "mw-protection-policy"
path = "/"
description = ""
policy = data.aws_iam_policy_document.mw_protection_policy.json
}
resource "aws_iam_role" "mw_protection_role" {
name = "mw-protection-role"
assume_role_policy = data.aws_iam_policy_document.mw_protection_role.json
}
data "aws_iam_policy_document" "mw_protection_role" {
statement {
sid = "AllowGuardDutyMalwareProtection"
effect = "Allow"
actions = [
"sts:AssumeRole"
]
principals {
type = "Service"
identifiers = ["malware-protection-plan.guardduty.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "mw_protection_role" {
role = aws_iam_role.mw_protection_role.name
policy_arn = aws_iam_policy.mw_protection_policy.arn
}
ClamAVを利用していた頃と比べて、ファイル数が減ってスッキリしました。
実装に必要なファイル数は半分以下になりました
3.導入してみる
まず幾つかの環境で試してみた所、あっさり有効化できました。
先に説明したようにOrganizationでの一元管理ができないため、各アカウントの管理コンソールからしか動作状況を確認できないので注意が必要です。
ステータスがActiveであれば有効化されています
疑似ウィルスを使っての検知とSlackへの通知も順調です。
疑似ウィルスもきっちり検出、通知してくれます
以下、実装時に注意が必要だったポイントを記載しておきます。
terraformのプロバイダバージョン
適用時にterraformのプロバイダのバージョンが古くてエラーとなるケースがありました。
定期的に環境のアップデートを行っていれば問題ないかもしれませんが、古い場合は事前にアップデートしておく必要があります。
以下画像のケースでは4.X代と古かったawsプロバイダを5.7x代まで上げたらplanが通るようになりました。
プロバイダのバージョンが古いとinitの段階でエラーになります
スキャン後のファイルの取り扱い
当初、マルウェアスキャンを行い、マルウェアが検出された際、そのオブジェクトへのアクセスを拒否する設定を想定していました。
具体的にはオブジェクトのGuardDutyMalwareScanStatus
というタグにNO_THREATS_FOUND
という値がついていないファイルへのアクセスを全て禁止するというものです。
つまりマルウェアプロテクションによる安全が確認されていないファイルへのアクセスは一切禁止するというルールです。
タグの値でファイルが安全か否かを判断できます
新規に構築する環境であれば問題無いのですが、既存のプロダクトでマルウェアプロテクションを有効化した場合に問題が出ます。
最初に記述したように、マルウェアプロテクションで行えるのはファイルアップロードをトリガーにしたスキャンですので、過去にアップロードされていたファイルはスキャンされず、当然タグがついていません。
この場合、禁止設定を有効化した瞬間に過去のファイルにアクセスできなくなるというインシデントが発生してしまいます。
PRにコメントをもらって気付いたものの、気づかずに進めてたら危なかった。
コメントのおかげでインシデントを回避できました
S3バケットへのアクセス
対象となるS3バケットに固有のバケットポリシーが適用されている場合に、上記の設定だけではS3バケットにアクセスできないケースがあります。
その場合、バケットポリシーにGuardDutyからのアクセスを許可してあげる必要があります。
以下は、他のAWSアカウントからも参照する目的で、固有のポリシーが設定されていたバケットに追加したマルウェアプロテクション用のポリシーです。
バケット固有のポリシーが適用されている場合は注意
そしてインシデントへ
色々対応して大丈夫だろうと油断した所でやらかしてしまいました。
運転免許取り立ての初心者が調子に乗って事故るようなものです。
あるプロダクトでマルウェアプロテクションを有効化した後、一部の処理が行えなくなるという事象が発生し、サービスの提供に支障が出ました。
原因
技術的な原因を、以下に記載します。
- ファイルアップロード後に、ECSタスク上のアプリケーションがファイルをコピーしています
- マルウェアプロテクションを有効化したことで、アップロードされたファイルにスキャン結果を示すタグがつくようになりました
- このファイルをコピーするにはECSタスクに
タグを操作する権限
が必要です - ところが構築当初の事情により、このプロダクトだけECSタスクのポリシーにその権限が付与されていませんでした
- 結果的にアプリケーション側で必要な処理が行えなくなりました
stg環境で有効化した際に管理コンソール上は問題が無かったため、アプリを使った動作確認を怠った事も大きなインシデント要因です。
後々確認すると、ちゃんとstg環境からのアラートがSlackに通知されていたものの、それに気づけなかったのも迂闊でした。
対応
この事象は、ECSタスクのポリシーにs3:GetObjectTagging
とs3:PutObjectTagging
の2つを追加する事で解決しました。
ポリシーにタグ操作に必要な権限を追加します
調べるとタグの付いたオブジェクト操作については、Mediumの記事にもガッツリ記述がありました。
幸いだったのは、障害に気づいたプロダクト担当の方が即座にマルウェアプロテクションを無効化し復旧してくださった事と、他のプロダクトでは同様の問題が確認されなかった事でした。
作業のポイントを反映してみる
反省の念も込めて作業のポイントを更新してみます。
マルウェアプロテクション以外で確認しておくポイントを追記しています
4.さいごに
迂闊な作業でサービス障害を引き起こし、海より深く悔やまれる2024年の年の瀬です。
作業は慎重にとは知っているはずなのに、ついつい手を抜いてしまいがちな点は改めて大いに反省し、今後の作業に取り組んでいきます。
(ご迷惑をおかけした関係者の皆様、本当にごめんなさい)
最初に書いたように、マルウェアプロテクション自体は非常に有効な機能で、簡単に有効化できるので使わない手はありません。
GuardDutyを使っていなくても、この機能単体で動作させる事ができるというのも敷居が低くていいですね。
更に運用が楽なのは他の何にも代え難いメリットです。
これでマルチアカウント環境を一元管理できたらなぁ。
最後までお読みいただきありがとうございます。
この記事がこれから作業を計画されている方に少しでもお役に立てば幸いです。
MICINではメンバーを大募集しています。
迂闊な私の作業もフォローしてくれる、優しくて頼もしいメンバーが皆さんをお待ちしています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
Discussion