Security Hub Automation Rules を複数リージョンに配布する方法について考えてみた

Security Hub Automation Rules をマルチアカウントで利用する際、Security Hub の集約設定を行うことで管理者アカウントで設定したルールをメンバーアカウントにも適用可能です。
一方でマルチリージョンの場合、リージョン集約をしていても全てのリージョンでルールを作成する必要があります。

Security Hub 管理者アカウントのみが自動化ルールを作成、削除、編集、表示できます。管理者が作成したルールは、管理者アカウントとメンバーアカウントの検出結果に適用されます。
メンバーアカウント ID を基準として定義することで、Security Hub 管理者は自動化ルールを使用して特定のメンバーアカウントの検出結果を更新したり、検出結果に対してアクションを実行したりすることもできます。
自動化ルール

そこで、Security Hub Automation Rules を複数リージョンに楽に配布する方法について考えてみました。
ぱっと思いつく方法だと下記が挙げられるでしょうか。

  • リージョン数分 AWS CLI を実行する
    • aws securityhub create-automation-rule を実行すれば良い。
    • batch-update-automation-rules は ARN を指定してルールを更新するためのコマンドであり、作成と更新は別コマンドで実施する必要がある。
    • 一度に作成できる Automation Rules は一つであるため、ルールを複数定義する際は ( ルール数 × リージョン数 ) 分 CLI を実行する必要がある
      • シェルスクリプトでラップするような形で無いとコマンド実行数が増えがち
  • SDK を利用したスクリプトを用意して、全てのリージョンに対して設定を行う
    • Python スクリプトが AWS から公開されている
      • 複数リージョンに複数のルールを配布可能
      • 特定リージョンで失敗しても、後続処理には影響が無い(公開されているスクリプトを使う分には、エラー処理を自分で実装する必要が無い)
      • AWS ドキュメント内で multi-Region automation rules deployment script として言及されている
  • CloudFormation を利用する
    • yaml 形式で記述可能
    • アカウント内のあるべき状態を CloudFormation テンプレートして定義できる
    • CloudFormation StackSets を利用することで、楽に複数リージョンに配布可能
    • StackSets のセットアップが面倒な場合は単純に aws cloudformation deploy をリージョン数分実行することも可能
  • Terraform で Multiple Provider Configurations を使って配る
    • 既に Terraform を利用中であれば、最初に検討すべき
    • しかし、 2023 年 8 月時点では Terraform が Security Hub Automation Rules の作成に対応していない

公式ドキュメントで紹介されているかつ楽に使い始めることができるので、公開されている Python スクリプトを利用する形がまず候補に上がるかと思います。
また、CloudFormation を利用することで享受できるメリットも多そうなので、こちらも良さそうに思いました。
今回はこの 2 つの方法を試してみました。

AWS から公開されているスクリプトを利用して Automation Rules を複数リージョンに配布してみた

AWS から公開されている Python スクリプトを試してみました。
現在の設定を一覧表示する list-automation-rules.pyautomation-rules-create.py で用意されています。
list-automation-rules.py を実行すると下記のような出力が得られました。 良い感じにまとめて表示してくれています。

$ python list-automation-rules.py --deployed_regions ap-northeast-1,us-east-1
Listing rules in these regions: ['ap-northeast-1', 'us-east-1']
*******************************************
Retrieving rules from region:  ap-northeast-1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/3feff4d0-4bd5-4045-869f-a5cc85ca0b7b
Rule Name:  Automation Rules created by manual
Rule Status:  ENABLED
Rule Order:  1
*******************************************
Retrieving rules from region:  us-east-1
No rules in this region

サンプルとして用意されている JSON ファイルを利用して、automation-rules-create.py を実行してみました。
使用した JSON ファイルは下記になります。

{
    "RuleOrder": 1,
    "RuleName": "sample-rule-critical",
    "RuleStatus": "ENABLED",
    "Description": "Set finding's severity to Critical for specific finding resource ID",
    "Criteria":
    {
        "ProductName":
        [
            {
                "Value": "Security Hub",
                "Comparison": "EQUALS"
            }
        ],
        "ComplianceStatus":
        [
            {
                "Value": "FAILED",
                "Comparison": "EQUALS"
            }
        ],
        "RecordState":
        [
            {
                "Value": "ACTIVE",
                "Comparison": "EQUALS"
            }
        ],
        "WorkflowStatus":
        [
            {
                "Value": "NEW",
                "Comparison": "EQUALS"
            }
        ],
        "ResourceId":
        [
            {
                "Value": "arn:aws:s3:::examplebucket/developers/design_info.doc",
                "Comparison": "EQUALS"
            }
        ]
    },
    "Actions":
    [
        {
            "Type": "FINDING_FIELDS_UPDATE",
            "FindingFieldsUpdate":
            {
                "Severity":
                {
                    "Label": "CRITICAL"
                },
                "Note":
                {
                    "Text": "Urgent – look into this critical S3 bucket",
                    "UpdatedBy": "sechub-automation"
                }
            }
        }
    ]
}

automation-rules-create.py を実行します。

$python automation-rules-create.py --input_file automation-rule-definition.json --enabled_regions ap-northeast-1,us-east-1
Deploying rule in these regions: ['ap-northeast-1', 'us-east-1']
Deploying rule to region:  ap-northeast-1
Rule sample-rule-critical deployed successfully.
Deploying rule to region:  us-east-1
Rule sample-rule-critical deployed successfully.

再度 list-automation-rules.py を実行します。

> python list-automation-rules.py --deployed_regions ap-northeast-1,us-east-1
Listing rules in these regions: ['ap-northeast-1', 'us-east-1']
*******************************************
Retrieving rules from region:  ap-northeast-1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/3feff4d0-4bd5-4045-869f-a5cc85ca0b7b
Rule Name:  Automation Rules created by manual
Rule Status:  ENABLED
Rule Order:  1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/8cccf15f-c20e-4718-9750-525b7d0bac1b
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1
*******************************************
Retrieving rules from region:  us-east-1
------------------------------------
Rule ARN:  arn:aws:securityhub:us-east-1:xxxxxxxxxxxx:automation-rule/0cca44f1-2ec6-4f97-98c0-55b5876a0ebf
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1

定義したルールを複数リージョンに配布することに成功しました。
同じ Rule Order のルールが複数作成されていますが、この場合は最後に編集された方が後に適用されます。(つまり Rule Order が大きい扱いとなる)

Security Hub API または AWS CLI を使用してルールを作成すると、Security Hub は RuleOrder で数値が最も小さいルールを最初に適用します。その後、後続のルールを昇順で適用します。複数の検出結果に同じ RuleOrder がある場合、Security Hub は UpdatedAt フィールドに以前の値のルールを最初に適用します (つまり、最後に編集されたルールが最後に適用されます)。
自動化ルール

Rule Order はあくまで適用順番を決定するための一要素であり、実際には UpdatedAt と合わせて適用順位が決定されるということですね。
同じ JSON ファイルを利用して、もう一度 automation-rules-create.py を実行すると下記状況になりました。

$ python3 list-automation-rules.py --deployed_regions ap-northeast-1,us-east-1
Listing rules in these regions: ['ap-northeast-1', 'us-east-1']
*******************************************
Retrieving rules from region:  ap-northeast-1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/3feff4d0-4bd5-4045-869f-a5cc85ca0b7b
Rule Name:  Automation Rules created by manual
Rule Status:  ENABLED
Rule Order:  1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/8cccf15f-c20e-4718-9750-525b7d0bac1b
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1
------------------------------------
Rule ARN:  arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:automation-rule/30805ebb-c600-4266-9723-5b161b9bff44
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1
*******************************************
Retrieving rules from region:  us-east-1
------------------------------------
Rule ARN:  arn:aws:securityhub:us-east-1:xxxxxxxxxxxx:automation-rule/0cca44f1-2ec6-4f97-98c0-55b5876a0ebf
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1
------------------------------------
Rule ARN:  arn:aws:securityhub:us-east-1:xxxxxxxxxxxx:automation-rule/43192b2c-adef-44ae-a90c-1158d3c9f9a5
Rule Name:  sample-rule-critical
Rule Status:  ENABLED
Rule Order:  1

Security Hub Automation Rules では、名前と Rule Order が被っていても問題無く作成されます。
スクリプト実行に冪等性が無いこと、ルールの更新や削除ができないことがこちらの方法の欠点になるかと思います。
とはいえ、Python の実行環境さえあれば複数リージョンに複数ルールを一気に配布できるのは非常に楽です。
ルールを配布した後は塩漬けする場合、こちらの利用が第一候補に上がると思います。

CloudFormation を利用して Automation Rules を複数リージョンに配布してみた

AWS::SecurityHub::AutomationRule を利用することで Automation Rules を定義可能です。
アカウント全体の Automation Rules について、あるべき状態を CloudFormation テンプレートとして管理できますし、更新や削除も作成と同じ手順で実行可能です。
特に CI やレビューを通した後に CloudFormation テンプレートをデプロイする仕組みを既に用意している場合、こちらに寄せた方が良いと思います。
例として CDK Bootstrap で作成したリソースを特定コントロール(ECR.1)を対象に自動抑制する Automation Rules を書いてみました。

AWSTemplateFormatVersion: "2010-09-09"

Resources:
  AutomationRule:
    Type: AWS::SecurityHub::AutomationRule
    Properties:
      Actions:
        - Type: FINDING_FIELDS_UPDATE
          FindingFieldsUpdate:
            Workflow:
              Status: SUPPRESSED
            Note:
              Text: Suppress Resource created by CDK Bootstrap
              UpdatedBy: admin
      Criteria:
        ProductName:
          - Comparison: EQUALS
            Value: Security Hub
        ComplianceStatus:
          - Comparison: EQUALS
            Value: FAILED
        RecordState:
          - Comparison: EQUALS
            Value: ACTIVE
        WorkflowStatus:
          - Comparison: EQUALS
            Value: NEW
        ResourceId:
          - Comparison: CONTAINS
            Value: cdk-hnb659fds
        GeneratorId:
          - Comparison: EQUALS
            Value: !Sub "arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:control/aws-foundational-security-best-practices/v/1.0.0/ECR.1"
      Description: Suppress Resource created by CDK Bootstrap
      RuleName: Suppress Resource created by CDK Bootstrap
      RuleOrder: 1
      RuleStatus: ENABLED

GuardDuty 周りの設定だとリージョンごとに違う値として Detector ID を渡す方法を考える必要がありますが、Security Hub は特に考えなくて良いので楽ですね。
Security Hub 自体は別管理で、Automation Rules だけ CloudFormation を利用するような形も取りやすいかと思います。
こちらの CloudFormation スタックをどのように複数リージョンに配布するかですが、AWS CLI の aws cloudformation deploy をリージョン数分実行するか CloudFormation StackSets を利用するかから選択可能です。
Organizations を利用している、もしくは既に他で StackSets を利用中の環境であれば、StackSets を利用する方法が良いと思います。
SERVICE_MANAGED 型を利用する場合を想定して、 CLI でスタックセット、スタックインスタンスを作成してみます。

STACK_SET_NAME="test-stacksets-security-hub-automation-rule"
TEMPLATE=file://security-hub-automation-rules.yml
REGIONS="ap-northeast-1 us-east-1"
ACCOUNT_ID="XXXXXXXXXXXXXXXX"
OU_ID="xxx"

OU_ID は Security Hub 集約先アカウントが含まれている OU であれば何でも良いです。(最終的には INTERSECTION を利用して集約先アカウントのみを指定するためです)

aws cloudformation create-stack-set \
  --region ap-northeast-1 \
  --stack-set-name $STACK_SET_NAME \
  --template-body $TEMPLATE \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=false
aws cloudformation create-stack-instances \
  --region ap-northeast-1 \
  --regions $REGIONS \
  --stack-set-name $STACK_SET_NAME \
  --operation-preferences MaxConcurrentCount=10,RegionConcurrencyType=PARALLEL \
  --deployment-targets OrganizationalUnitIds=$OU_ID,AccountFilterType=INTERSECTION,Accounts=$ACCOUNT_ID

Security Hub の集約先アカウントと Organizations の管理アカウントが同一の場合は SERVICE_MANAGED 型を利用できないので SELF_MANAGED 型を利用する必要があります。
ですが、 Security Hub の管理は管理アカウント以外に委任して可能な限り管理アカウントを利用しないのがベストプラクティスなので、一旦このケースは考えないこととします。(この場合は Security Hub 管理の委任も検討しましょう)

Organizations を利用していないアカウントでも AWSCloudFormationStackSetAdministrationRole と AWSCloudFormationStackSetExecutionRole を作成して、SELF_MANAGED の StackSets を利用可能ですが、Automation Rules のためだけにここまで準備するのは面倒な気がしてます。
この場合は CloudFormation で実装した上で複数リージョン分まとめてデプロイするスクリプトを作ると良いのではないかと思います。
下記のようなイメージです。

#! /bin/bash

# 変数宣言
CFN_TEMPLATE="security-hub-automation-rules.yml"
CFN_STACK_NAME="test-stacksets-security-hub-automation-rule"
REGIONS="ap-northeast-1 us-east-1"

# デプロイ
for region in ${REGIONS[@]}; do
  aws cloudformation deploy \
    --template-file $CFN_TEMPLATE \
    --stack-name $CFN_STACK_NAME \
    --region $region
done

まとめ

作成後もルールを調整する可能性を考えると、CloudFormation に抵抗無ければ CloudFormation 管理するのが良いように思います。(既に Terraform を利用中であればそちらに寄せるのが良さそうですが、現時点では未対応です)
特に CloudFormation StackSets を利用中の環境であれば、StackSets を利用することで複数リージョンの配布も相当楽になります。
後からルールを変更する可能性があまり無さそうな場合は、マルチリージョンで Automation Rules を配布するスクリプトが提供されているのでこちらを検討してみて下さい。
Python の実行環境さえあれば複数リージョンに複数ルールを配布可能です。