I Built a Mechanism to Route Security Service Notifications by Account in an AWS Multi-Account Environment

I Built a Mechanism to Route Security Service Notifications by Account in an AWS Multi-Account Environment

Built a system for an AWS multi-account environment that aggregates and formats findings from Security Hub, GuardDuty, IAM Access Analyzer, and Inspector via EventBridge and Step Functions, then routes notifications to per-account SNS topics.
2026.05.19

This page has been translated by machine translation. View original

Hello! This is Yoshida from the Cloud Business Division.

Previously, I wrote an article summarizing the configuration of security services in a multi-account environment.

https://dev.classmethod.jp/articles/multi-account-aws-security-services-architecture/

That article introduced a configuration that aggregates the findings from Security Hub CSPM, GuardDuty, IAM Access Analyzer, and Inspector into Security Hub CSPM in the Audit account.
This time, I will build a mechanism that sorts the aggregated findings by account and sends notifications.

【ブログ使用】セキュリティサービス全体構成図.png

Image source: AWSのマルチアカウント環境におけるセキュリティサービスの構成をまとめてみた | DevelopersIO

What We Want to Do

We want to sort security service findings by the account they originated from and notify the person in charge.

The overall picture of the notification platform we will build this time is as follows.

【使用】セキュリティサービス通知.png

The processing flow is as follows.

  • Forward Security Hub CSPM findings from the management account to the EventBridge default event bus in the Audit account using EventBridge
  • Filter by service with EventBridge rules in the Audit account
  • Format the notification text with Step Functions (formatting process) and forward it to an EventBridge custom event bus
  • Start Step Functions (dispatch process) from the custom event bus
  • Dispatch to the corresponding SNS topic based on the account ID

The formatting process and the dispatch process are separated into two Step Functions state machines.
This allows you to change the notification text format and add notification destinations independently.

This formatting and dispatch process is a mechanism similar to the configuration for centrally managing CloudWatch Alarms in a multi-account environment that I wrote about previously.

https://dev.classmethod.jp/articles/multi-account-cloudwatch-alarm-centralized-management/

Notification Targets

The findings notified in this configuration are as follows.

Service Notification Target Filter Conditions
Security Hub CSPM Control violations MEDIUM/HIGH/CRITICAL, FAILED/WARNING/NOT_AVAILABLE
GuardDuty Threat detection MEDIUM/HIGH/CRITICAL
IAM Access Analyzer External access detection All findings (access from outside the organization)
Inspector Vulnerability detection MEDIUM/HIGH/CRITICAL

For Security Hub CSPM, only control violations of the security standard (AWS Foundational Security Best Practices) are targeted.

Notification Text Format

Here is the format of the notification text generated by Step Functions (formatting process).
The notification text is formatted using the States.Format function to generate the subject and body.

Security Hub CSPM

Subject

緊急度: {Severity.Label} Security Hubセキュリティアラート Account: {AwsAccountId}

Body

Security Hubで設定の問題が検知されました。

検知内容: {Title}
リソース種類: {Resources[0].Type}
リソースID: {Resources[0].Id}
詳細: {Description}
リージョン: {Region}
推奨事項URL: {ProductFields.RecommendationUrl}

Sample

件名: 緊急度: MEDIUM Security Hubセキュリティアラート Account: 123456789012

Security Hubで設定の問題が検知されました。

検知内容: S3.8 S3 Block Public Access setting should be enabled at the bucket level
リソース種類: AwsS3Bucket
リソースID: arn:aws:s3:::example-bucket
詳細: This control checks whether Amazon S3 buckets have bucket-level public access blocks applied.
リージョン: ap-northeast-1
推奨事項URL: https://docs.aws.amazon.com/console/securityhub/S3.8/remediation

GuardDuty

Subject

緊急度: {Severity.Label} GuardDutyセキュリティアラート Account: {AwsAccountId}

Body

以下の脅威を検知しました。

検出タイプ: {Types[0]}
詳細: {Description}
リージョン: {Region}
推奨事項URL: https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/guardduty_finding-types-active.html

Sample

件名: 緊急度: HIGH GuardDutyセキュリティアラート Account: 123456789012

以下の脅威を検知しました。

検出タイプ: TTPs/Command and Control/Backdoor:EC2-C&CActivity.B
詳細: EC2 instance i-0abc123def456 is communicating with a known command and control server.
リージョン: ap-northeast-1
推奨事項URL: https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/guardduty_finding-types-active.html

IAM Access Analyzer

Subject

IAM Access Analyzerセキュリティアラート Account: {AwsAccountId}

Body

以下リソースが外部共有されています。

リソース種類: {Resources[0].Type}
リソース名: {Resources[0].Id}
リージョン: {Region}
詳細: {Description}

Sample

件名: IAM Access Analyzerセキュリティアラート Account: 123456789012

以下リソースが外部共有されています。

リソース種類: AWS::S3::Bucket
リソース名: arn:aws:s3:::example-public-bucket
リージョン: ap-northeast-1
詳細: The S3 bucket policy grants public access to the bucket.

Inspector

Subject

緊急度: {Severity.Label} Inspectorセキュリティアラート Account: {AwsAccountId}

Body

脆弱性が検出されました。

タイトル: {Title}
リソースタイプ: {Resources[0].Type}
リソースID: {Resources[0].Id}
初回検出日時: {FirstObservedAt}
最終検出日時: {LastObservedAt}
リージョン: {Resources[0].Region}

Sample

件名: 緊急度: HIGH Inspectorセキュリティアラート Account: 123456789012

脆弱性が検出されました。

タイトル: CVE-2023-12345 - Example Package Remote Code Execution
リソースタイプ: AwsEc2Instance
リソースID: arn:aws:ec2:ap-northeast-1:123456789012:instance/i-0abc123def456
初回検出日時: 2024-01-15T10:30:00Z
最終検出日時: 2024-01-20T14:00:00Z
リージョン: ap-northeast-1

To customize the notification text, modify the Step Functions state machine definition.

Build Steps (Audit Account)

We will build using the AWS CLI in CloudShell. Resources are created in the reverse order of the processing flow. This is because if the target resources are not prepared first, they cannot be referenced when configuring rules and state machines.

Setting Variables

First, set the variables according to your environment.

# Auditアカウント(自アカウント)
AUDIT_ACCOUNT_ID="123456789012"

# 管理アカウント
MGMT_ACCOUNT_ID="111122223333"

# メンバーアカウント
MEMBER1_ACCOUNT_ID="444455556666"
MEMBER2_ACCOUNT_ID="777788889999"

# リージョン
REGION="ap-northeast-1"

Creating SNS Topics

Create the SNS topics that serve as dispatch destinations. This time, we prepare topics for four accounts (the management account, the Audit account, and two member accounts) plus a default notification destination.

# デフォルト通知先(未登録アカウント用)
aws sns create-topic --name security-alert-default --region ${REGION}

# 管理アカウント用
aws sns create-topic --name security-alert-management --region ${REGION}

# Auditアカウント用
aws sns create-topic --name security-alert-audit --region ${REGION}

# メンバーアカウント1用
aws sns create-topic --name security-alert-member1 --region ${REGION}

# メンバーアカウント2用
aws sns create-topic --name security-alert-member2 --region ${REGION}

Add subscriptions.

A confirmation email will arrive, so click the link to approve it. Add subscriptions to the other topics in the same way.

Creating Step Functions (Dispatch Process)

Create a state machine that dispatches formatted events to SNS topics based on the account ID.

First, create an IAM role.

sfn-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "states.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
aws iam create-role \
  --role-name security-alert-sfn-dispatcher-role \
  --assume-role-policy-document file://sfn-trust-policy.json

Grant permission to publish to the SNS topics. Replace <AuditアカウントID> according to your environment.

sfn-dispatcher-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": [
        "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-default",
        "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-management",
        "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-audit",
        "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-member1",
        "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-member2"
      ]
    }
  ]
}
aws iam put-role-policy \
  --role-name security-alert-sfn-dispatcher-role \
  --policy-name security-alert-sfn-dispatcher-policy \
  --policy-document file://sfn-dispatcher-policy.json

Create the state machine. A Choice state evaluates the account ID and publishes to the matching SNS topic.
If no account matches, it notifies the default topic.

Replace <管理アカウントID>, <AuditアカウントID>, <メンバーアカウント1ID>, and <メンバーアカウント2ID> according to your environment.

dispatcher-state-machine.json
{
  "Comment": "Security Alert Dispatcher State Machine",
  "StartAt": "DetermineNotificationTarget",
  "States": {
    "DetermineNotificationTarget": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.detail.AccountId",
          "StringEquals": "<管理アカウントID>",
          "Comment": "Management Account",
          "Next": "NotifyManagement"
        },
        {
          "Variable": "$.detail.AccountId",
          "StringEquals": "<AuditアカウントID>",
          "Comment": "Audit Account",
          "Next": "NotifyAudit"
        },
        {
          "Variable": "$.detail.AccountId",
          "StringEquals": "<メンバーアカウント1ID>",
          "Comment": "Member Account 1",
          "Next": "NotifyMember1"
        },
        {
          "Variable": "$.detail.AccountId",
          "StringEquals": "<メンバーアカウント2ID>",
          "Comment": "Member Account 2",
          "Next": "NotifyMember2"
        }
      ],
      "Default": "NotifyDefault"
    },
    "NotifyManagement": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-management",
        "Subject.$": "$.detail.Subject",
        "Message.$": "$.detail.Message"
      },
      "End": true
    },
    "NotifyAudit": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-audit",
        "Subject.$": "$.detail.Subject",
        "Message.$": "$.detail.Message"
      },
      "End": true
    },
    "NotifyMember1": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-member1",
        "Subject.$": "$.detail.Subject",
        "Message.$": "$.detail.Message"
      },
      "End": true
    },
    "NotifyMember2": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-member2",
        "Subject.$": "$.detail.Subject",
        "Message.$": "$.detail.Message"
      },
      "End": true
    },
    "NotifyDefault": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn": "arn:aws:sns:ap-northeast-1:<AuditアカウントID>:security-alert-default",
        "Subject.$": "$.detail.Subject",
        "Message.$": "$.detail.Message"
      },
      "End": true
    }
  }
}
aws stepfunctions create-state-machine \
  --name security-alert-dispatcher \
  --definition file://dispatcher-state-machine.json \
  --role-arn "arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-sfn-dispatcher-role" \
  --region ${REGION}

Creating the Custom Event Bus and EventBridge Rule

Create a custom event bus that receives formatted events.

aws events create-event-bus \
  --name security-alert-custom-bus \
  --region ${REGION}

Create an EventBridge rule that invokes the dispatch Step Functions from the custom event bus. First, create an IAM role.

eventbridge-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
aws iam create-role \
  --role-name security-alert-eb-to-dispatcher-role \
  --assume-role-policy-document file://eventbridge-trust-policy.json
eb-to-sfn-dispatcher-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "states:StartExecution",
      "Resource": "arn:aws:states:ap-northeast-1:<AuditアカウントID>:stateMachine:security-alert-dispatcher"
    }
  ]
}
aws iam put-role-policy \
  --role-name security-alert-eb-to-dispatcher-role \
  --policy-name security-alert-eb-to-dispatcher-policy \
  --policy-document file://eb-to-sfn-dispatcher-policy.json

Create the rule. The pattern is set to receive events put by the formatting process.

event-pattern-custom-bus.json
{
  "source": ["custom.securityalert.stepfunctions"],
  "detail-type": ["Shaped Security Findings"]
}
aws events put-rule \
  --name security-alert-custom-to-dispatcher \
  --event-bus-name security-alert-custom-bus \
  --event-pattern file://event-pattern-custom-bus.json \
  --state ENABLED \
  --region ${REGION}

Set the target.

aws events put-targets \
  --rule security-alert-custom-to-dispatcher \
  --event-bus-name security-alert-custom-bus \
  --targets "[{
    \"Id\": \"DispatcherTarget\",
    \"Arn\": \"arn:aws:states:${REGION}:${AUDIT_ACCOUNT_ID}:stateMachine:security-alert-dispatcher\",
    \"RoleArn\": \"arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-eb-to-dispatcher-role\"
  }]" \
  --region ${REGION}

Creating Step Functions (Formatting Process)

Create a state machine that formats the findings from each security service into notification text.

Create an IAM role. Reuse the sfn-trust-policy.json trust policy created earlier.

aws iam create-role \
  --role-name security-alert-sfn-formatter-role \
  --assume-role-policy-document file://sfn-trust-policy.json

Grant PutEvents permission to the custom event bus.

sfn-formatter-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "events:PutEvents",
      "Resource": "arn:aws:events:ap-northeast-1:<AuditアカウントID>:event-bus/security-alert-custom-bus"
    }
  ]
}
aws iam put-role-policy \
  --role-name security-alert-sfn-formatter-role \
  --policy-name security-alert-sfn-formatter-policy \
  --policy-document file://sfn-formatter-policy.json

Create the state machine.
A Choice state evaluates ProductName and formats the notification text for each service.
For Security Hub, whether it is a control violation is determined by the presence of StandardsArn.

formatter-state-machine.json
{
  "Comment": "Security Alert Shaping State Machine",
  "StartAt": "ChoiceService",
  "States": {
    "ChoiceService": {
      "Type": "Choice",
      "Choices": [
        {
          "And": [
            {
              "Variable": "$.detail.findings[0].ProductName",
              "StringEquals": "Security Hub"
            },
            {
              "Variable": "$.detail.findings[0].ProductFields.StandardsArn",
              "IsPresent": true
            }
          ],
          "Next": "SecurityHubShaping"
        },
        {
          "Variable": "$.detail.findings[0].ProductName",
          "StringEquals": "GuardDuty",
          "Next": "GuardDutyShaping"
        },
        {
          "Variable": "$.detail.findings[0].ProductName",
          "StringEquals": "IAM Access Analyzer",
          "Next": "AccessAnalyzerShaping"
        },
        {
          "Variable": "$.detail.findings[0].ProductName",
          "StringEquals": "Inspector",
          "Next": "InspectorShaping"
        }
      ],
      "Default": "Fail"
    },
    "SecurityHubShaping": {
      "Type": "Pass",
      "Parameters": {
        "Subject.$": "States.Format('緊急度: {} Security Hubセキュリティアラート Account: {}', $.detail.findings[0].Severity.Label, $.detail.findings[0].AwsAccountId)",
        "Message.$": "States.Format('Security Hubで設定の問題が検知されました。\n\n検知内容: {}\nリソース種類: {}\nリソースID: {}\n詳細: {}\nリージョン: {}\n推奨事項URL: {}', $.detail.findings[0].Title, $.detail.findings[0].Resources[0].Type, $.detail.findings[0].Resources[0].Id, $.detail.findings[0].Description, $.detail.findings[0].Region, $.detail.findings[0].ProductFields.RecommendationUrl)",
        "AccountId.$": "$.detail.findings[0].AwsAccountId"
      },
      "Next": "PutToCustomBus"
    },
    "GuardDutyShaping": {
      "Type": "Pass",
      "Parameters": {
        "Subject.$": "States.Format('緊急度: {} GuardDutyセキュリティアラート Account: {}', $.detail.findings[0].Severity.Label, $.detail.findings[0].AwsAccountId)",
        "Message.$": "States.Format('以下の脅威を検知しました。\n\n検出タイプ: {}\n詳細: {}\nリージョン: {}\n推奨事項URL: https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/guardduty_finding-types-active.html', $.detail.findings[0].Types[0], $.detail.findings[0].Description, $.detail.findings[0].Region)",
        "AccountId.$": "$.detail.findings[0].AwsAccountId"
      },
      "Next": "PutToCustomBus"
    },
    "AccessAnalyzerShaping": {
      "Type": "Pass",
      "Parameters": {
        "Subject.$": "States.Format('IAM Access Analyzerセキュリティアラート Account: {}', $.detail.findings[0].AwsAccountId)",
        "Message.$": "States.Format('以下リソースが外部共有されています。\n\nリソース種類: {}\nリソース名: {}\nリージョン: {}\n詳細: {}', $.detail.findings[0].Resources[0].Type, $.detail.findings[0].Resources[0].Id, $.detail.findings[0].Region, $.detail.findings[0].Description)",
        "AccountId.$": "$.detail.findings[0].AwsAccountId"
      },
      "Next": "PutToCustomBus"
    },
    "InspectorShaping": {
      "Type": "Pass",
      "Parameters": {
        "Subject.$": "States.Format('緊急度: {} Inspectorセキュリティアラート Account: {}', $.detail.findings[0].Severity.Label, $.detail.findings[0].AwsAccountId)",
        "Message.$": "States.Format('脆弱性が検出されました。\n\nタイトル: {}\nリソースタイプ: {}\nリソースID: {}\n初回検出日時: {}\n最終検出日時: {}\nリージョン: {}', $.detail.findings[0].Title, $.detail.findings[0].Resources[0].Type, $.detail.findings[0].Resources[0].Id, $.detail.findings[0].FirstObservedAt, $.detail.findings[0].LastObservedAt, $.detail.findings[0].Resources[0].Region)",
        "AccountId.$": "$.detail.findings[0].AwsAccountId"
      },
      "Next": "PutToCustomBus"
    },
    "PutToCustomBus": {
      "Type": "Task",
      "Resource": "arn:aws:states:::events:putEvents",
      "Parameters": {
        "Entries": [
          {
            "Detail": {
              "Subject.$": "$.Subject",
              "Message.$": "$.Message",
              "AccountId.$": "$.AccountId"
            },
            "DetailType": "Shaped Security Findings",
            "EventBusName": "security-alert-custom-bus",
            "Source": "custom.securityalert.stepfunctions"
          }
        ]
      },
      "End": true
    },
    "Fail": {
      "Type": "Fail",
      "Error": "UnknownSource",
      "Cause": "Unknown event source"
    }
  }
}
aws stepfunctions create-state-machine \
  --name security-alert-formatter \
  --definition file://formatter-state-machine.json \
  --role-arn "arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-sfn-formatter-role" \
  --region ${REGION}

Creating EventBridge Rules (Default Bus → Formatting SFN)

Create rules that filter the findings from each security service and forward them to the formatting state machine.

Create an IAM role. This role is shared by the four rules.
Reuse the eventbridge-trust-policy.json trust policy created earlier.

aws iam create-role \
  --role-name security-alert-eb-to-formatter-role \
  --assume-role-policy-document file://eventbridge-trust-policy.json
eb-to-sfn-formatter-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "states:StartExecution",
      "Resource": "arn:aws:states:ap-northeast-1:<AuditアカウントID>:stateMachine:security-alert-formatter"
    }
  ]
}
aws iam put-role-policy \
  --role-name security-alert-eb-to-formatter-role \
  --policy-name security-alert-eb-to-formatter-policy \
  --policy-document file://eb-to-sfn-formatter-policy.json

Rule for Security Hub CSPM

Security Hub control violations are the notification target.
The security standard is specified with ProductFields.StandardsArn.

event-pattern-securityhub.json
{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "Compliance": {
        "Status": ["FAILED", "WARNING", "NOT_AVAILABLE"]
      },
      "RecordState": ["ACTIVE"],
      "ProductFields": {
        "StandardsArn": ["arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0"]
      },
      "Workflow": {
        "Status": ["NEW"]
      },
      "Severity": {
        "Label": ["MEDIUM", "HIGH", "CRITICAL"]
      }
    }
  }
}
aws events put-rule \
  --name security-alert-securityhub \
  --event-pattern file://event-pattern-securityhub.json \
  --state ENABLED \
  --region ${REGION}
aws events put-targets \
  --rule security-alert-securityhub \
  --targets "[{
    \"Id\": \"FormatterTarget\",
    \"Arn\": \"arn:aws:states:${REGION}:${AUDIT_ACCOUNT_ID}:stateMachine:security-alert-formatter\",
    \"RoleArn\": \"arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-eb-to-formatter-role\"
  }]" \
  --region ${REGION}

Rule for GuardDuty

event-pattern-guardduty.json
{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "ProductName": ["GuardDuty"],
      "RecordState": ["ACTIVE"],
      "Workflow": {
        "Status": ["NEW"]
      },
      "Severity": {
        "Label": ["MEDIUM", "HIGH", "CRITICAL"]
      }
    }
  }
}
aws events put-rule \
  --name security-alert-guardduty \
  --event-pattern file://event-pattern-guardduty.json \
  --state ENABLED \
  --region ${REGION}
aws events put-targets \
  --rule security-alert-guardduty \
  --targets "[{
    \"Id\": \"FormatterTarget\",
    \"Arn\": \"arn:aws:states:${REGION}:${AUDIT_ACCOUNT_ID}:stateMachine:security-alert-formatter\",
    \"RoleArn\": \"arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-eb-to-formatter-role\"
  }]" \
  --region ${REGION}

Rule for IAM Access Analyzer

event-pattern-accessanalyzer.json
{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "ProductName": ["IAM Access Analyzer"],
      "RecordState": ["ACTIVE"],
      "Workflow": {
        "Status": ["NEW"]
      }
    }
  }
}
aws events put-rule \
  --name security-alert-accessanalyzer \
  --event-pattern file://event-pattern-accessanalyzer.json \
  --state ENABLED \
  --region ${REGION}
aws events put-targets \
  --rule security-alert-accessanalyzer \
  --targets "[{
    \"Id\": \"FormatterTarget\",
    \"Arn\": \"arn:aws:states:${REGION}:${AUDIT_ACCOUNT_ID}:stateMachine:security-alert-formatter\",
    \"RoleArn\": \"arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-eb-to-formatter-role\"
  }]" \
  --region ${REGION}

Rule for Inspector

event-pattern-inspector.json
{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "ProductName": ["Inspector"],
      "RecordState": ["ACTIVE"],
      "Workflow": {
        "Status": ["NEW"]
      },
      "Severity": {
        "Label": ["MEDIUM", "HIGH", "CRITICAL"]
      }
    }
  }
}
aws events put-rule \
  --name security-alert-inspector \
  --event-pattern file://event-pattern-inspector.json \
  --state ENABLED \
  --region ${REGION}
aws events put-targets \
  --rule security-alert-inspector \
  --targets "[{
    \"Id\": \"FormatterTarget\",
    \"Arn\": \"arn:aws:states:${REGION}:${AUDIT_ACCOUNT_ID}:stateMachine:security-alert-formatter\",
    \"RoleArn\": \"arn:aws:iam::${AUDIT_ACCOUNT_ID}:role/security-alert-eb-to-formatter-role\"
  }]" \
  --region ${REGION}

Configuring the Resource Policy for the Default Event Bus

Allow event reception from the management account.

aws events put-permission \
  --event-bus-name default \
  --action events:PutEvents \
  --principal ${MGMT_ACCOUNT_ID} \
  --statement-id AllowManagementAccountPutEvents \
  --region ${REGION}

Build Steps (Management Account)

Create an EventBridge rule that forwards the management account's Security Hub findings to the Audit account.

Setting Variables

# 管理アカウント(自アカウント)
MGMT_ACCOUNT_ID="111122223333"

# Auditアカウント
AUDIT_ACCOUNT_ID="123456789012"

# リージョン
REGION="ap-northeast-1"

Creating the EventBridge Rule

Create an IAM role.
The trust policy is the same as the eventbridge-trust-policy.json created in the Audit account.

eventbridge-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
aws iam create-role \
  --role-name security-alert-eb-to-audit-role \
  --assume-role-policy-document file://eventbridge-trust-policy.json

Grant the events:PutEvents permission to the event bus in the Audit account.

eb-to-audit-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "events:PutEvents",
      "Resource": "arn:aws:events:ap-northeast-1:<AuditアカウントID>:event-bus/default"
    }
  ]
}
aws iam put-role-policy \
  --role-name security-alert-eb-to-audit-role \
  --policy-name security-alert-eb-to-audit-policy \
  --policy-document file://eb-to-audit-policy.json

Create the rule.
All Security Hub findings are forwarded to the Audit account.

event-pattern-management-to-audit.json
{
  "source": ["aws.securityhub"],
  "detail-type": ["Security Hub Findings - Imported"]
}
aws events put-rule \
  --name security-alert-to-audit \
  --event-pattern file://event-pattern-management-to-audit.json \
  --state ENABLED \
  --region ${REGION}

Set the target.

aws events put-targets \
  --rule security-alert-to-audit \
  --targets "[{
    \"Id\": \"AuditEventBusTarget\",
    \"Arn\": \"arn:aws:events:${REGION}:${AUDIT_ACCOUNT_ID}:event-bus/default\",
    \"RoleArn\": \"arn:aws:iam::${MGMT_ACCOUNT_ID}:role/security-alert-eb-to-audit-role\"
  }]" \
  --region ${REGION}

Verification

Now, let's verify the behavior using GuardDuty sample findings.
Because the EventBridge rules in this setup filter for MEDIUM or higher severity, we create a sample finding with HIGH severity.

Run the following command in CloudShell in each account to generate a sample finding.

aws guardduty create-sample-findings \
  --detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text --region ap-northeast-1) \
  --finding-types "Backdoor:EC2/C&CActivity.B" \
  --region ap-northeast-1

Notification emails arrived successfully at the email addresses specified for each of the four accounts!

Member Account 1

screenshot.png

Member Account 2

screenshot 1.png

Audit Account

screenshot 2.png

Management Account

screenshot 3.png

Conclusion

We built a mechanism that sorts security service notifications by account in a multi-account environment.

I hope this is helpful to someone.

That was Yoshida from the Cloud Business Division!

References


そのマルチアカウント運用、気合いで支えていませんか

Organizations や Control Tower で土台は作れても、アカウントもポリシーも増えるほど、運用は「詳しい一人」に寄りかかっていく。属人化が限界を迎える前に、組織として回す仕組み=CCoEへ。5,600社の支援から得た立ち上げの型を、無料資料にまとめました。

CCoE総合支援

組織で回す仕組みの資料をもらう

Share this article

AWSのお困り事はクラスメソッドへ