こんにちは。BASE BANK 株式会社 Dev Division にて、 Software Developer をしている東口(@hgsgtk)です。
TL;DR
- AWS のマネージド脅威検出サービスである Amazon GuardDuty を有効化する場合、全リージョンに対して設定することが推奨される
- Amazon GuardDuty を全リージョンで有効化し、検出した内容を Slack に通知するまでの構成を説明・それを実現する具体的な Terraform コードを解説する
- 記事公開時点で terraform-provider-aws が AWS Chatbot に対応していないため、一部 Console 画面で作成する
- 当記事のサンプルコードはこちらにて公開している
Amazon GuardDuty / AWS Chatbotとは
Amazon GuardDuty(略:GuardDuty)は悪意のある操作・不正動作をモニタリングするツールです。AWS 環境を実運用する場合、セキュリティ上の脅威が含まれていないか継続して確認する事が重要でしょう。GuardDuty はセキュリティベストプラクティスとして導入できる 1 つのサービスです。
Amazon GuardDuty は、AWS のアカウント、ワークロード、および Amazon S3 に保存されたデータを保護するために、悪意のあるアクティビティや不正な動作を継続的にモニタリングする脅威検出サービスです。
GuardDuty は発見的統制のサービスとなります。発見的統制とは望ましくない事象が発生したことを発見するIT統制活動の一種です。セキュリティ保護に関して GuardDuty がどのような役割を担うかについては次の YouTube 動画にて詳しく説明されています。
そして、AWS Chatbot を用いると Slack チャンネルへの通知を手間少なくかんたんに実現できます。
AWS Chatbot は、Slack チャンネルや Amazon Chime チャットルームで AWS のリソースを簡単にモニタリングおよび操作できるようにしてくれるインタラクティブエージェントです。
今回は、この2つのサービスを組み合わせて検出された脅威を Slack に通知するワークフローを紹介します。CloudFormation での構築例はいくつかあったのですが、Terraform で構築する例について解説している内容がインターネット上に少なかったので、Terraform で運用している開発現場の方にとって参考になる手順となれば幸いです。
当記事で出来ることと構成図
当記事では、GuardDuty からの検出結果を AWS Chatbot を用いて Slack 通知する構成を紹介しています。そのため、当記事の内容・サンプルコードを用いることで次のような通知を Slack で受けることが出来るようになります。
なお、画像内にある検出内容は GuardDuty の機能の 1 つである「Generate sample findings(結果のサンプルの生成)」によって生成したサンプル内容です。
そして、これを実現するための構成図が以下です。
各リージョンごとに必要なリソースと、AWS Chatbot や IAM Role といった Global なものが存在します。
また、執筆当時 AWS Chatbot はterraform-provider-awsが対応していないため直接 Terraform 管理にすることはできません。
そのため、当記事ではこの構成の作成のため次の手順を踏みます。
- Terraform で全リージョン分の GuardDuty の有効化・通知ワークフローを構築する
- AWS Console 画面から AWS Chatbot を設定する
なお、@gainingsさんにご教示いただきましたが、CloudFormation を Terraform 管理することで間接的に AWS Chatbot を Terraform 管理下に置くという方法があるそうです。Booth で販売されている『クラウド破産を回避するAWS実践ガイド』という書籍にその方法について詳しく説明があります。どのような方針でコード化していくかによりますが、AWS Chatbot を構築するひとつの選択肢になりますね。
Terraformで構築する
今回のサンプルコードは次のリポジトリに公開しています。
当リポジトリの構成は以下です。
. ├── hgsgtk-dev <- 構築する環境、moduleを利用して全リージョン分設定する └── modules ├── chatbot <- AWS Chatbot module └── guardduty <- GuardDuty module
guardduty module
GuardDuty とその通知ワークフローのための構成要素は以下です。
- GuardDuty の有効化
- EventBridge(CloudWatch Event)の設定
- SNS Topic の暗号化
- SNS Topic の作成
GuardDutyの有効化
GuardDuty の有効化は次の記述のみで完了です。
resource "aws_guardduty_detector" "guardduty" { enable = true }
その他、S3 へのエクスポートなど様々な設定が可能です。当記事では通知ワークフローの紹介が趣旨なので詳しく紹介しませんが、気になる方はAWSの開発ガイドやTerraformのドキュメントを確認してください。
EventBridge(CloudWatch Event)の設定
EventBridge では GuardDuty からの検出をイベントとして受け取って後続の SNS Topic をターゲットにハンドリングします。
まず Event Rule を作成します。
resource "aws_cloudwatch_event_rule" "guardduty" { name = "capture-guardduty" description = "Capture Guard Duty finding events" event_pattern = <<EOF { "source": [ "aws.guardduty" ], "detail-type": [ "GuardDuty Finding" ] } EOF }
event_pattern で設定した内容は、「イベントを検出した」ことを条件にしています。これらの設定以外にも検出内容の severity(緊急度)を条件に追加できます。
{ "source": [ "aws.guardduty" ], "detail-type": [ "GuardDuty Finding" ], "detail": { "severity": [ 4, 4.0, 4.1, 4.2, (省略) ] } }
筆者の現場では最小限の設定で始めて必要に応じて通知対象の severity(緊急度)を調整する方針としました。
次に、作成したルールで受け取ったイベントの送信ターゲットに次に作成する SNS Topic を設定します。
resource "aws_cloudwatch_event_target" "guardduty-sns" { rule = aws_cloudwatch_event_rule.guardduty.id target_id = "guardduty-sns" arn = aws_sns_topic.event_bridge_to_chatbot.arn }
SNS Topicの暗号化
以前、当開発ブログで Terraform のセキュリティ静的解析 tfsec を紹介しました。
tfsec の指摘項目にはSNS Topic の暗号化が含まれています。セキュリティ上のベタープラクティスのひとつとして SNS Topic は Amazon KMS(Key Management Store)によって暗号化します。
resource "aws_kms_key" "for_encrypt_sns_topic" { description = "guarddutyからのeventを受けるsns topic暗号化用" enable_key_rotation = true policy = data.aws_iam_policy_document.policy_for_encrypt_sns_topic.json } resource "aws_kms_alias" "for_encrypt_sns_topic_alias" { name = "alias/guardduty/for_encrypt_sns_topic" target_key_id = aws_kms_key.for_encrypt_sns_topic.key_id } data "aws_iam_policy_document" "policy_for_encrypt_sns_topic" { version = "2012-10-17" # defaultでついてくるルートアカウントに対する権限設定 statement { sid = "Enable Root User Permissions" effect = "Allow" principals { type = "AWS" identifiers = ["arn:aws:iam::${var.aws_account_id}:root"] } actions = [ "kms:*" ] resources = [ "*", ] } # events.amazonaws.com に対する権限が暗号化対象のサービス操作に必要 statement { sid = "AWSEvents" effect = "Allow" principals { type = "Service" identifiers = ["events.amazonaws.com"] } actions = [ "kms:GenerateDataKey", "kms:Decrypt" ] resources = [ "*", ] } }
注意点として、今回の構成で CloudWatch Event Rule のターゲットとして設定する場合、events.amazonaws.com
に対して復号化(Decrypt
)・データキーの生成(GenerateDataKey
)のアクションを許可する必要があります。
SNS Topicの作成
SNS Topic 暗号化用の KMS Key が作成できたので SNS Topic を作成します。作成した Key はaws_sns_topic.kms_master_key_id
で設定します。
resource "aws_sns_topic" "event_bridge_to_chatbot" { name = "event-bridge-to-chatbot" kms_master_key_id = aws_kms_key.for_encrypt_sns_topic.key_id } resource "aws_sns_topic_policy" "event_bridge_to_chatbot_policy" { arn = aws_sns_topic.event_bridge_to_chatbot.arn policy = data.aws_iam_policy_document.sns_topic_policy.json } data "aws_iam_policy_document" "sns_topic_policy" { version = "2012-10-17" # defaultでついてくるルートアカウントに対する権限設定 statement { sid = "__default_statement_ID" effect = "Allow" principals { type = "AWS" identifiers = ["*"] } actions = [ "SNS:GetTopicAttributes", "SNS:SetTopicAttributes", "SNS:AddPermission", "SNS:RemovePermission", "SNS:DeleteTopic", "SNS:Subscribe", "SNS:ListSubscriptionsByTopic", "SNS:Publish", "SNS:Receive" ] resources = [ aws_sns_topic.event_bridge_to_chatbot.arn, ] condition { test = "StringEquals" variable = "AWS:SourceOwner" values = [ var.aws_account_id ] } } # events.amazonaws.com がイベント発行するために必要 statement { sid = "allow_AWSEvents_publish" effect = "Allow" principals { type = "Service" identifiers = ["events.amazonaws.com"] } actions = [ "sns:Publish", ] resources = [ aws_sns_topic.event_bridge_to_chatbot.arn, ] } }
ここでは、events.amazonaws.com
に対してsns:Publish
アクションを許可する必要があります。
chatbot module
AWS Chatbot 用の module を用意します。
AWS Chatbot 自体は前述したとおり Terraform でのコード管理対象外になりますが、AWS Chatbot が利用する IAM Role は既存の IAM を利用できるので Console で作成する前に Terraform で作成します。
AWS Chatbot では 4 パターンの許可設定がテンプレートで用意されています。
- 通知のアクセス許可
- 読み取り専用コマンドのアクセス許可
- Lambda 呼び出しコマンドのアクセス許可
- AWS サポートコマンドのアクセス許可
今回のユースケースでは、「通知のアクセス許可」を権限としてもつ IAM Role を作成することになります。
resource "aws_iam_role" "chatbot-notification-only" { name = "chatbot-notification-only" assume_role_policy = jsonencode( { Version : "2012-10-17", Statement : [ { Sid : "", Effect : "Allow", Principal : { Service : "chatbot.amazonaws.com" }, Action : "sts:AssumeRole" } ] } ) description = "AWS Chatbot Execution Role for Only Notification" } resource "aws_iam_role_policy_attachment" "chatbot-notification-only-attach" { policy_arn = aws_iam_policy.chatbot-notification-only.arn role = aws_iam_role.chatbot-notification-only.name } resource "aws_iam_policy" "chatbot-notification-only" { name = "chatbot-notification-only" policy = jsonencode( { Version = "2012-10-17" Statement : [ { Sid : "", Effect : "Allow", Action : [ "cloudwatch:Describe*", "cloudwatch:Get*", "cloudwatch:List*" ], Resource : "*" } ] } ) }
作成した IAM Role に紐づく IAM Policy は AWS 公式ドキュメント内の解説や実際に Console 上で試しに作成したものを参考にしています。
moduleを利用して全リージョン分作成する
module の用意が終わったので全リージョン作成していきます。
全リージョン作成するために、今回の作成方法では複数の Provider を用意する方針とします。Terraform では module を利用する際、明示的に Provider を渡せます。
その仕様を用いてリージョンごとに module を用意します。
- 全リージョン分のProviderを作成
- guardduty moduleを利用し、GuardDutyと通知ワークフローを作成
- chatbot moduleを利用し、AWS Chatbot用のIAMロールを作成
全リージョン分のProviderを作成
全リージョン分のProviderを作成します。
# デフォルトのProvider provider "aws" { version = "~> 3.18.0" region = var.region } provider "aws" { region = "us-east-1" alias = "us-east-1" } provider "aws" { region = "us-east-2" alias = "us-east-2" } # (省略)
注意点としては、有効化していないリージョンがある場合は全リージョン設定する必要がない点です。以下のリージョンは有効化しない限り管理対象にする必要はない可能性があります。
- af-south-1 アフリカ (ケープタウン)
- ap-east-1 アジアパシフィック (香港)
- eu-south-1 欧州 (ミラノ)
- me-south-1 中東 (バーレーン)
guardduty moduleを利用し、GuardDutyと通知ワークフローを作成
作成した Provider を利用して全リージョン分の module 利用コードを用意します。
module "guardduty-us-east-1" { source = "../modules/guardduty" aws_account_id = var.aws_account_id providers = { aws = aws.us-east-1 } } module "guardduty-us-east-2" { source = "../modules/guardduty" aws_account_id = var.aws_account_id providers = { aws = aws.us-east-2 } } # (省略)
chatbot moduleを利用し、AWS Chatbot用のIAMロールを作成
AWS Chatbot はリージョン設定がない global なサービスなため 1 つだけ作成します。
module "chatbot" { source = "../modules/chatbot" }
Console 画面で残りの AWS Chatbotを作成する
最後 AWS Chatbot を Console 画面から作成していきます。
AWS Chatbot は当記事の公開時点では Amazon Chime と Slack の2つのクライアントをサポートしています。今回は Slack 通知が要件なので Slack を選択します。
Configure client
を選択し Slack の Authorization を完了すると、対象の workspace が作成されます。
実際に通知するチャネルを当該画面のConfigure Slack channel
から設定していきます。
Logging では「エラーのみ」か「全てのイベント」が選択できますが、このロギング先の CloudWatch Logs はUS East (N. Virginia)
となります。その理由は、公式ドキュメントにて次のように説明されているとおりです。
You can view your logs in the Amazon CloudWatch console. Note that you must specify US East (N. Virginia) for the Region.
AWS Chatbot 用に必要な IAM Role は事前に作成したものを利用できるため、Terraform で作成した IAM Role を指定します。
最後に AWS Chatbot で通知する SNS Topic を指定します。ここでは、Terraform で事前に全リージョン分作成した SNS Topic を設定してきます。
この設定をすると内部的には AWS Chatbot からの protocol: HTTPS
の SNS Subscription を作成されます。
なお、筆者は Terraform で SNS Subscription を作成すること検証しましたが不可でした。理由は、terraform上の記法であるsns_topic_subscriptionのprotocolの仕様です。
https -- delivery of JSON-encoded messages via HTTPS. Supported only for the end points that auto confirms the subscription.
そして、AWS Chatbot の通知設定から SNS Topic を指定するフローでなければ「確認済み」にならないようです。そのため、SNS Topic に対する Subscription も Console 画面から作成する必要があります。Terraform コードで管理する場合は、terraform-provider-aws の AWS Chatbot のサポートを待つ必要があります。
以上で設定は完了です。
ここまでやると冒頭で紹介したとおり当該 Slack チャンネルに通知が来るようになります。
動作確認方法
GuardDuty では「Generate sample findings(結果のサンプルの生成)」という機能があります。
この機能ではサンプルの検出結果を生成してくれます。この機能で生成された結果は CloudWatch Event にも発行されるため、本記事で紹介しているような通知ワークフローを組む際のデバックに有用です。
おわりに
GuardDuty を有効化し Slack 通知を行なう構成を Terraform で作成する事例について情報が少なかったので紹介させていただきました。実際に作成する際に参考になれば幸いです。