AWS Security Hubで複数リージョン複数コントロールをまとめて無効化するシェルスクリプトを作ってみた

複数コントロールを特定リージョンを除いて無効化する場合に
2024.06.16

複数あるグローバルリソースのコントロールを複数リージョンまとめて無効化したい

こんにちは、のんピ(@non____97)です。

皆さんはSecurity Hubで複数あるグローバルリソースのコントロールを複数リージョンまとめて無効化したいと思ったことはありますか? 私はあります。

先日、Security Hubでグローバルリソースの課金を抑えるためには「一つのリージョンでのみグローバルリソースを対象としたコントロールを有効化し、その他のリージョンでは無効化するべき」ということを紹介しました。

こちらの対応をしない場合のコストインパクトについては以下記事が分かりやすいです。

では、コントロールの無効化はどのように行えば良いでしょうか。

2024/6/16時点でグローバルリソースを含む Security Hub コントロールは49個あります。詳細は以下AWS公式ドキュメントをご覧ください。

私のAWSアカウントでは17リージョンが有効になっています。仮にマネジメントコンソールから49コントロール分を地道にポチポチする場合、833回も無効にする必要があります。とてもやってられません。

ということで、AWS CLIを使ったシェルスクリプトでまとめて処理を行います。

DevelopersIO上では複数リージョン複数コントロールをまとめて無効化するシェルスクリプトを紹介しているものがなかったので作ってみます。

Pythonや二重ループで無効化しているものはありました。併せてご覧いただくと良いと思います。

やってみた

現在の状態の確認

まず、現在の状態を確認します。

簡単にシェルスクリプトを書くとすると以下でしょうか。

# 確認したいコントロールID
$ security_control_id='CloudTrail.1'

# 東京リージョン以外のリージョン名でループ
$ aws ec2 describe-regions \
  --query Regions[].[RegionName] \
  --output text \
  | grep -v -e 'ap-northeast-1' \
  | sort \
  | while read -r region; do
    # 確認するStandardを指定
    standard_arn="arn:aws:securityhub:${region}::standards/aws-foundational-security-best-practices/v/1.0.0"

    # 指定したコントロールIDがStandardと関連付いているか確認
    association_status=$(aws securityhub list-standards-control-associations \
      --security-control-id "${security_control_id}" \
      --region "${region}" \
      --query "StandardsControlAssociationSummaries[?StandardsArn=='${standard_arn}'].AssociationStatus"\
      --output text)
    
    # リージョン毎に有効/無効を確認
    echo -e "${security_control_id} ${region} : ${association_status}"
  done

実行すると、以下のように指定したコントロールの各リージョンの状態を表示してくれます。

CloudTrail.1 ap-east-1 : ENABLED
CloudTrail.1 ap-northeast-2 : ENABLED
CloudTrail.1 ap-northeast-3 : ENABLED
CloudTrail.1 ap-south-1 : ENABLED
CloudTrail.1 ap-southeast-1 : ENABLED
CloudTrail.1 ap-southeast-2 : ENABLED
CloudTrail.1 ca-central-1 : ENABLED
CloudTrail.1 eu-central-1 : ENABLED
CloudTrail.1 eu-north-1 : ENABLED
CloudTrail.1 eu-west-1 : ENABLED
CloudTrail.1 eu-west-2 : ENABLED
CloudTrail.1 eu-west-3 : ENABLED
CloudTrail.1 sa-east-1 : ENABLED
CloudTrail.1 us-east-1 : ENABLED
CloudTrail.1 us-east-2 : ENABLED
CloudTrail.1 us-west-1 : ENABLED
CloudTrail.1 us-west-2 : ENABLED

では、今回のように複数のコントロールを確認する場合はどうすれば良いでしょうか。

以下のようにコントロールIDの配列を用意し、リージョンとコントロールIDの配列を二重で回す形でしょうか。

# 確認したいコントロールID
$ declare -a security_control_ids=(
  'CloudTrail.2'
  'CloudTrail.4'
  'IAM.1'
  'IAM.2'
  'IAM.3'
  'IAM.4'
  'IAM.5'
  'IAM.7'
  'IAM.8'
  'IAM.21'
  'KMS.1'
  'KMS.2'
)

# 東京リージョン以外のリージョン名でループ
$ aws ec2 describe-regions \
  --query Regions[].[RegionName] \
  --output text \
  | grep -v -e 'ap-northeast-1' \
  | sort \
  | while read -r region; do
    # 確認するStandardを指定
    standard_arn="arn:aws:securityhub:${region}::standards/aws-foundational-security-best-practices/v/1.0.0"

    # 指定したコントロールIDがStandardと関連付いているか確認
    for security_control_id in "${security_control_ids[@]}"; do
      association_status=$(aws securityhub list-standards-control-associations \
        --security-control-id "${security_control_id}" \
        --region "${region}" \
        --query "StandardsControlAssociationSummaries[?StandardsArn=='${standard_arn}'].AssociationStatus"\
        --output text)
      
      # リージョン毎に有効/無効を確認
      echo -e "${region} ${security_control_id} : ${association_status}"
    done
  done

こちらでも以下のように問題なく出力されます。

ap-east-1 CloudTrail.2 : ENABLED
ap-east-1 CloudTrail.4 : ENABLED
ap-east-1 IAM.1 : ENABLED
ap-east-1 IAM.2 : ENABLED
.
.
(中略)
.
.
ap-northeast-2 CloudTrail.2 : ENABLED
ap-northeast-2 CloudTrail.4 : ENABLED
ap-northeast-2 IAM.1 : ENABLED
ap-northeast-2 IAM.2 : ENABLED
.
.
(中略)
.
.
ap-northeast-3 CloudTrail.2 : ENABLED
ap-northeast-3 CloudTrail.4 : ENABLED
ap-northeast-3 IAM.1 : ENABLED
ap-northeast-3 IAM.2 : ENABLED
.
.
(中略)
.
.

us-west-2 IAM.21 : ENABLED
us-west-2 KMS.1 : ENABLED
us-west-2 KMS.2 : ENABLED

一方で各リージョンの各コントロールの状態を一つづつ確認をしているので非常に時間がかかります。上のパターンだと6分かかりました。

確認したいコントロールとリージョンが増加すると、その分確認にかかる時間もかかります。

今回は対応として、describe-standards-controlsを使いました。

describe-standards-controlsは指定したSecurity Hubのセキュリティ基準内で条件に当てはまるコントロールを一回で取得することが可能です。

これによりリージョンとコントロールで二重ループさせる必要がなくなります。

具体的には以下のようになります。

# AWSアカウントID
$ account_id=$(aws sts get-caller-identity \
  --output text \
  --query Account
)

# コントロールID
$ declare -a control_ids=(
  'CloudTrail.2'
  'CloudTrail.4'
  'IAM.1'
  'IAM.2'
  'IAM.3'
  'IAM.4'
  'IAM.5'
  'IAM.7'
  'IAM.8'
  'IAM.21'
  'KMS.1'
  'KMS.2'
)
# grep用にコントロールIDを | で連結
$ control_ids_pattern=$(printf '(%s)|' "${control_ids[@]}")

# 末尾の | は不要なので削除
$ control_ids_pattern=${control_ids_pattern%|}

# 東京リージョン以外のリージョン名でループ
$ aws ec2 describe-regions \
  --query Regions[].[RegionName] \
  --output text \
  | grep -v -e 'ap-northeast-1' \
  | sort \
  | while read -r region; do
    # 確認するStandardのサブスクリプションを指定指定
    standards_subscription_arn="arn:aws:securityhub:${region}:${account_id}:subscription/aws-foundational-security-best-practices/v/1.0.0"

    echo "==============================================================="
    echo "[${region}]"

    # 指定したStandard内のコントロールIDとコントロールの状態を取得
    # grep でリストアップしたコントロールIDのみを抽出
    aws securityhub describe-standards-controls \
      --standards-subscription-arn "${standards_subscription_arn}" \
      --region "${region}" \
      --query "Controls[].[ControlId,ControlStatus]" \
      --output text \
      | grep -E "${control_ids_pattern}"
  done

実行結果は以下のとおりです。

===============================================================
[ap-east-1]
CloudTrail.2    ENABLED
CloudTrail.4    ENABLED
IAM.1   ENABLED
IAM.2   ENABLED
IAM.21  ENABLED
IAM.3   ENABLED
IAM.4   ENABLED
IAM.5   ENABLED
IAM.7   ENABLED
IAM.8   ENABLED
KMS.1   ENABLED
KMS.2   ENABLED
===============================================================
[ap-northeast-2]
CloudTrail.2    ENABLED
CloudTrail.4    ENABLED
IAM.1   ENABLED
IAM.2   ENABLED
IAM.21  ENABLED
IAM.3   ENABLED
IAM.4   ENABLED
IAM.5   ENABLED
IAM.7   ENABLED
IAM.8   ENABLED
KMS.1   ENABLED
KMS.2   ENABLED
===============================================================
[ap-northeast-3]
CloudTrail.2    ENABLED
CloudTrail.4    ENABLED
IAM.1   ENABLED
IAM.2   ENABLED
IAM.3   ENABLED
IAM.5   ENABLED
IAM.7   ENABLED
IAM.8   ENABLED
===============================================================
.
.
(中略)
.
.
===============================================================
[us-west-2]
CloudTrail.2    ENABLED
CloudTrail.4    ENABLED
IAM.1   ENABLED
IAM.2   ENABLED
IAM.21  ENABLED
IAM.3   ENABLED
IAM.4   ENABLED
IAM.5   ENABLED
IAM.7   ENABLED
IAM.8   ENABLED
KMS.1   ENABLED
KMS.2   ENABLED

こちらは実行完了までにおおよそ45秒でした。コントロール数が増減しても実行時間に特に差はありませんでした。複数コントロールを一気に確認したい場合は役立ちそうです。

コントロールの無効化

それでは実際にコントロールの無効化を行います。

こちらもコントロールでループを回すと非常に時間がかかりそうです。

そんな時はbatch-update-standards-control-associationsです。

batch-update-standards-control-associations--standards-control-association-updatesで指定した複数のコントロールに対してまとめて更新をかけることが可能です。

--standards-control-association-updatesはリストです。ワンライナーでリストを表現するのはなかなか見づらいです。今回は--generate-cli-skeletonで出力されたフォーマットを元にJSONを整形して、--cli-input-jsonでパラメーターを指定します。

$ aws securityhub batch-update-standards-control-associations --generate-cli-skeleton
{
    "StandardsControlAssociationUpdates": [
        {
            "StandardsArn": "",
            "SecurityControlId": "",
            "AssociationStatus": "ENABLED",
            "UpdatedReason": ""
        }
    ]
}

具体的なコードは以下のとおりです。

# 無効化するコントロール一覧と無効化する理由
$ declare -A disable_controls=(
  ['CloudTrail.2']="マルチリージョンの証跡しか有効にしておらず、各リージョンで重複して記録されることを防ぐため"
  ['CloudTrail.4']="マルチリージョンの証跡しか有効にしておらず、各リージョンで重複して記録されることを防ぐため"
  ['IAM.1']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.2']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.3']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.4']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.5']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.7']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.8']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['IAM.21']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['KMS.1']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
  ['KMS.2']="IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
)

# 東京リージョン以外のリージョン名でループ
$ aws ec2 describe-regions \
  --query Regions[].[RegionName] \
  --output text \
  | grep -v -e 'ap-northeast-1' \
  | sort \
  | while read -r region; do
    # 確認するStandardを指定
    standard_arn="arn:aws:securityhub:${region}::standards/aws-foundational-security-best-practices/v/1.0.0"

    # batch-update-standards-control-associations の --cli-input-json で指定する変数の用意
    batch_update_standards_control_associations=$(jq -n '{"StandardsControlAssociationUpdates": []}')

    # コントロールIDでループ
    for disable_control_id in "${!disable_controls[@]}"; do
        # 無効化するコントロール毎の更新理由を取得
        update_reason="${disable_controls[${disable_control_id}]}"

        # StandardsControlAssociationUpdates の配列に追加する要素の組み立て
        standards_control_association_update=$(jq \
          -n \
          --arg standard_arn "$standard_arn" \
          --arg disable_control_id "$disable_control_id" \
          --arg update_reason "$update_reason" \
          '{
              "StandardsArn": $standard_arn,
              "SecurityControlId": $disable_control_id,
              "AssociationStatus": "DISABLED",
              "UpdatedReason": $update_reason
          }'
        )

        # JSONの StandardsControlAssociationUpdates キーに追加
        batch_update_standards_control_associations=$(jq \
          --argjson control "${standards_control_association_update}" '.StandardsControlAssociationUpdates += [$control]' \
          <<< "${batch_update_standards_control_associations}"
        )
    done

    echo "==============================================================="
    echo "[${region}]"

    # 複数コントロールをまとめて無効化
    aws securityhub batch-update-standards-control-associations \
      --cli-input-json "${batch_update_standards_control_associations}" \
      --region "${region}"
  done

実行結果は以下のとおりです。

===============================================================
[ap-east-1]
===============================================================
[ap-northeast-2]
===============================================================
[ap-northeast-3]
===============================================================
[ap-south-1]
===============================================================
[ap-southeast-1]
===============================================================
[ap-southeast-2]
===============================================================
[ca-central-1]
===============================================================
[eu-central-1]
===============================================================
[eu-north-1]
===============================================================
[eu-west-1]
===============================================================
[eu-west-2]
===============================================================
[eu-west-3]
===============================================================
[sa-east-1]
===============================================================
[us-east-1]
===============================================================
[us-east-2]
===============================================================
[us-west-1]
===============================================================
[us-west-2]

エラーがあればUnprocessedAssociationUpdatesが返ってくるようですが、今回は特に返ってきませんでした。

ちなみにbatch-update-standards-control-associations--cli-input-jsonで指定する変数は以下のような形になります。

{
  "StandardsControlAssociationUpdates": [
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.21",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.7",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.4",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.5",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.2",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.3",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.1",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "IAM.8",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "CloudTrail.2",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "マルチリージョンの証跡しか有効にしておらず、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "CloudTrail.4",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "マルチリージョンの証跡しか有効にしておらず、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "KMS.2",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    },
    {
      "StandardsArn": "arn:aws:securityhub:ap-south-1::standards/aws-foundational-security-best-practices/v/1.0.0",
      "SecurityControlId": "KMS.1",
      "AssociationStatus": "DISABLED",
      "UpdatedReason": "IAMはグローバルリソースであり、各リージョンで重複して記録されることを防ぐため"
    }
  ]
}

無効化されているかの確認

最後に実際に無効化されているのか確認します。

# AWSアカウントID
$ account_id=$(aws sts get-caller-identity \
  --output text \
  --query Account
)

# コントロールID
$ declare -a control_ids=(
  'CloudTrail.2'
  'CloudTrail.4'
  'IAM.1'
  'IAM.2'
  'IAM.3'
  'IAM.4'
  'IAM.5'
  'IAM.7'
  'IAM.8'
  'IAM.21'
  'KMS.1'
  'KMS.2'
)
# grep用にコントロールIDを | で連結
$ control_ids_pattern=$(printf '(%s)|' "${control_ids[@]}")

# 末尾の | は不要なので削除
$ control_ids_pattern=${control_ids_pattern%|}

# 全リージョンでループ
aws ec2 describe-regions \
  --query Regions[].[RegionName] \
  --output text \
  | sort \
  | while read -r region; do
    # 確認するStandardを指定
    standards_subscription_arn="arn:aws:securityhub:${region}:${account_id}:subscription/aws-foundational-security-best-practices/v/1.0.0"

    echo "==============================================================="
    echo "[${region}]"

    # 指定したStandard内のコントロールIDとコントロールの状態を取得
    # grep でリストアップしたコントロールIDのみを抽出
    aws securityhub describe-standards-controls \
      --standards-subscription-arn "${standards_subscription_arn}" \
      --region "${region}" \
      --query "Controls[].[ControlId,ControlStatus]" \
      --output text \
      | grep -E "${control_ids_pattern}"
  done

===============================================================
[ap-east-1]
CloudTrail.2    DISABLED
CloudTrail.4    DISABLED
IAM.1   DISABLED
IAM.2   DISABLED
IAM.21  DISABLED
IAM.3   DISABLED
IAM.4   DISABLED
IAM.5   DISABLED
IAM.7   DISABLED
IAM.8   DISABLED
KMS.1   DISABLED
KMS.2   DISABLED
===============================================================
[ap-northeast-1]
CloudTrail.2    ENABLED
CloudTrail.4    ENABLED
IAM.1   ENABLED
IAM.2   ENABLED
IAM.21  ENABLED
IAM.3   ENABLED
IAM.4   ENABLED
IAM.5   ENABLED
IAM.7   ENABLED
IAM.8   ENABLED
KMS.1   ENABLED
KMS.2   ENABLED
===============================================================
[ap-northeast-2]
CloudTrail.2    DISABLED
CloudTrail.4    DISABLED
IAM.1   DISABLED
IAM.2   DISABLED
IAM.21  DISABLED
IAM.3   DISABLED
IAM.4   DISABLED
IAM.5   DISABLED
IAM.7   DISABLED
IAM.8   DISABLED
KMS.1   DISABLED
KMS.2   DISABLED
===============================================================
[ap-northeast-3]
CloudTrail.2    DISABLED
CloudTrail.4    DISABLED
IAM.1   DISABLED
IAM.2   DISABLED
IAM.3   DISABLED
IAM.5   DISABLED
IAM.7   DISABLED
IAM.8   DISABLED
.
.
(中略)
.
.
===============================================================
[us-west-2]
CloudTrail.2    DISABLED
CloudTrail.4    DISABLED
IAM.1   DISABLED
IAM.2   DISABLED
IAM.21  DISABLED
IAM.3   DISABLED
IAM.4   DISABLED
IAM.5   DISABLED
IAM.7   DISABLED
IAM.8   DISABLED
KMS.1   DISABLED
KMS.2   DISABLED

東京リージョン以外は指定したコントロールが無効になっていますね。

複数コントロールを特定リージョンを除いて無効化する場合に

Security Hubで複数リージョン複数コントロールをまとめて無効化するシェルスクリプトを作ってみました。

AWS Organizationsを使用していない環境で取り敢えずコントロールをまとめて無効にしたい場合は役立ちそうです。

AWS Organizationsを使用している場合は、以下AWS公式サンプルソリューションを参考に改造すると実現できると考えます。

また、Security HubとAWS Organizationsを統合させている環境において、全リージョンで指定したコントロールを無効にしたい場合はSecurity Hubの中央設定(Central Configuration)を使用すると良いでしょう。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!