Google Cloud 組織、フォルダ、プロジェクトでの IAM ロール変更を検知して Slack に通知する
はじめに
こんにちは!クラウドエースの SRE 部に所属している小田です。
IAM(Identity and Access Management)は、Google Cloud リソースへのアクセスを制御する上で非常に重要な役割を持っていますが、IAM ロールの追加や削除により、意図せずセキュリティ上の脆弱性を生み出したり、システムの誤動作を招く可能性があるため監視が必要です。
本記事では、Google Cloud 組織、フォルダ、プロジェクトのどこかで IAM ロールが変更された際に、検知して通知する Slack Bot の実装方法をご紹介します。
Cloud Asset Inventory とは
Google Cloud 組織、フォルダ、プロジェクト内のすべての Google Cloud のアセット メタデータの履歴を保持し、リソースや IAM ポリシー、ランタイム情報などをリアルタイムでモニタリング、分析することができるサービスです。
Cloud Asset Inventory vs Cloud Audit Logs
IAM ロールの変更を Slack に通知したい場合、Cloud Audit Logs から IAM ロールの変更操作ログを取得する方法と、Cloud Asset Inventory でアセットの変更を監視する方法があります。
Cloud Asset Inventory を使うメリットとしては、ログの集約シンクを設定することなく組織全体での IAM ロールの変更をシンプルに監視することができます。
デメリットとしては、Cloud Audit Logs のように IAM ロールの変更操作を誰がいつ行ったのかを特定することが出来ない点があります。
そのため、どちらのサービスを利用するべきかはユースケースによって異なります。
Slack Bot を作る
作成する Slack Bot は以下のような構成です。
まず最初にプロジェクトで必要な Cloud API を有効化しておきます。
- Cloud Asset API (
cloudasset.googleapis.com
) - Cloud Pub/Sub API (
pubsub.googleapis.com
) - Cloud Resource Manager API (
cloudresourcemanager.googleapis.com
) - Cloud Functions API (
cloudfunctions.googleapis.com
) - Cloud Build API (
cloudbuild.googleapis.com
) - Artifact Registry API (
artifactregistry.googleapis.com
) - Eventarc API (
eventarc.googleapis.com
)
Cloud Shell で以下のコマンドを実行して、API を有効化します。
gcloud services enable cloudasset.googleapis.com pubsub.googleapis.com cloudresourcemanager.googleapis.com cloudfunctions.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com eventarc.googleapis.com
変更通知を受け取るための Pub/Sub トピックを作成します。
gcloud pubsub topics create <Pub/Sub トピック名>
次に Asset Feed を作成して、Resource Manager での変更通知を Pub/Sub トピックで受け取る設定をします。
gcloud asset feeds create <Asset Feed 名> \
--organization=<組織 ID> \
--content-type=iam-policy \
--asset-types="cloudresourcemanager.*" \
--pubsub-topic="projects/test-asset-inventory/topics/<Pub/Sub トピック名>"
Cloud Asset Service Agent のサービス エージェントを作成します。
service-PROJECT_NUMBER@gcp-sa-cloudasset.iam.gserviceaccount.com
gcloud beta services identity create \
--service=cloudasset.googleapis.com \
--project=test-asset-inventory
Pub/Sub トピックで Cloud Asset Service Agent のサービス エージェントに対して、Pub/Sub パブリッシャー ロールを付与します。
gcloud pubsub topics add-iam-policy-binding \
projects/test-asset-inventory/topics/grant-iam-role \
--member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-cloudasset.iam.gserviceaccount.com \
--role=roles/pubsub.publisher
次に、こちらの公式ドキュメントを参考に Slack Bot を作成しておきます。
Bot Token Scopes に chat:write
, chat:write.public
を追加して発行された Bot User OAuth Token をメモします。
また、通知先の チャンネル ID
もメモしておきます。
続いて Slack Bot 用のコードを作成します。
今回は Go 言語で Pub/Sub トピックからのメッセージを加工後、Slack に通知するコードを作成しました。
package example
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"reflect"
"strings"
"cloud.google.com/go/functions/metadata"
"github.com/slack-go/slack"
)
type PubSubMessage struct {
Data []byte `json:"data"`
}
type Asset struct {
AssetType string `json:"assetType"`
Name string `json:"name"`
IamPolicy struct {
Bindings []struct {
Role string `json:"role"`
Members []string `json:"members"`
} `json:"bindings"`
} `json:"iamPolicy"`
}
type Message struct {
Asset Asset `json:"asset"`
PriorAsset Asset `json:"priorAsset"`
}
func IamAlert(ctx context.Context, m PubSubMessage) error {
meta, _ := metadata.FromContext(ctx)
fmt.Printf("Function triggered by message on topic: %s\n", meta.Resource)
msgStr := string(m.Data)
var msg Message
json.Unmarshal([]byte(msgStr), &msg)
delta := findBindingChanges(msg)
if len(delta) > 0 {
assetType, assetName := getAssetTypeName(msg.Asset.AssetType, msg.Asset.Name)
return sendSlack(assetType, assetName, delta)
}
return nil
}
func findBindingChanges(msg Message) []map[string]interface{} {
delta := make([]map[string]interface{}, 0)
priorBindings := make(map[string][]string)
for _, binding := range msg.PriorAsset.IamPolicy.Bindings {
priorBindings[binding.Role] = binding.Members
}
for _, binding := range msg.Asset.IamPolicy.Bindings {
priorMembers, exists := priorBindings[binding.Role]
if !exists || !reflect.DeepEqual(priorMembers, binding.Members) {
action := "Added"
if exists {
action = "Modified"
}
delta = append(delta, map[string]interface{}{
"members": binding.Members,
"role": binding.Role,
"membersAction": action,
})
}
delete(priorBindings, binding.Role)
}
for role, members := range priorBindings {
delta = append(delta, map[string]interface{}{
"members": members,
"role": role,
"membersAction": "Removed",
})
}
return delta
}
func getAssetTypeName(assetType, assetName string) (string, string) {
assetTypeParts := strings.Split(assetType, "/")
assetNameParts := strings.Split(assetName, "/")
return assetTypeParts[len(assetTypeParts)-1], assetNameParts[len(assetNameParts)-1]
}
func sendSlack(assetType, assetName string, delta []map[string]interface{}) error {
slackToken := os.Getenv("SLACK_TOKEN")
slackChannel := os.Getenv("SLACK_CHANNEL")
api := slack.New(slackToken)
headerTextBlock := slack.NewTextBlockObject("plain_text", "IAM ロール変更通知", false, false)
headerSection := slack.NewHeaderBlock(headerTextBlock)
assetField := slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*%s:* %s", assetType, assetName), false, false)
assetSection := slack.NewSectionBlock(assetField, nil, nil)
blocks := slack.Blocks{
BlockSet: []slack.Block{
headerSection,
assetSection,
},
}
for _, binding := range delta {
blocks.BlockSet = append(blocks.BlockSet, slack.NewDividerBlock())
membersText := slack.NewTextBlockObject("mrkdwn",
fmt.Sprintf("*Principal:*\n%s", strings.Join(binding["members"].([]string), "\n")), false, false)
action := binding["membersAction"].(string)
roleText := slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*%s:* %s", action, binding["role"]), false, false)
section := slack.NewSectionBlock(nil, []*slack.TextBlockObject{membersText, roleText}, nil)
blocks.BlockSet = append(blocks.BlockSet, section)
}
urlStr := fmt.Sprintf("https://console.cloud.google.com/iam-admin/iam?%s=%s", strings.ToLower(assetType), url.QueryEscape(assetName))
buttonText := slack.NewTextBlockObject("plain_text", "Open Google Cloud Console", false, false)
button := slack.NewButtonBlockElement("", "", buttonText)
button.URL = urlStr
buttonSection := slack.NewActionBlock("", button)
blocks.BlockSet = append(blocks.BlockSet, buttonSection)
_, _, err := api.PostMessage(slackChannel, slack.MsgOptionBlocks(blocks.BlockSet...))
return err
}
Pub/Sub トピックをトリガーにした Cloud Functions(第 2 世代)にデプロイします。
環境変数で Bot User OAuth Token と通知先のチャンネル ID を設定しています。
gcloud functions deploy iam-role-notification \
--gen2 \
--region=asia-northeast1 \
--runtime=go122 \
--entry-point=IamAlert \
--trigger-topic=<Pub/Sub トピック名> \
--set-env-vars SLACK_TOKEN=xoxb-xxxxxxxx-xxxxxxxx-ABCDEFG,SLACK_CHANNEL=C0123456ABC
動作確認
作成した Slack Bot の動作を行います。
組織、フォルダ、プロジェクトで IAM ロールを変更して Slack チャンネルに通知されるか確認してみます。
組織で user-01@example.com
に 閲覧者
ロールを付与してみます。
ロールを付与するとすぐに通知されました。
フォルダで user-01@example.com
に Cloud Run デベロッパー
ロールを付与してみます。
プロジェクトで user-01@example.com
に BigQuery データ編集者
と Cloud SQL 編集者
ロールを付与してみます。
続いて BigQuery データ編集者
を削除し、Cloud SQL 編集者
を Cloud SQL 管理者
に変更してみます。
IAM ロールの変更内容が正しく Slack に通知されているようです。
まとめ
今回は Cloud Asset Inventory を利用して、Google Cloud の IAM ロールの変更内容を検知して通知する Slack Bot を作ってみました。
冒頭でも記載しましたが、Cloud Asset Inventory と Cloud Audit Logs どちらを利用しても同じように変更を通知することができるため、ユースケースに応じて使い分けてください。
Discussion