🔒

Security Hub CSPMのコントロール "SSM.7" を解消するための設定を組織内全アカウント全リージョンで行いたい

に公開

https://qiita.com/HIRA_dayo/items/ad0e7221c6501c89cefe

AWSのセキュリティサービスの一つである「Security Hub CSPM」にて「SSM.7」という新規のコントロールが追加され、その影響で大量にノイジーなアラートが発生しました。

AWSコンソール上の「パブリック共有をブロック」設定
内容としては「SSMドキュメントの『パブリック共有をブロック』設定は有効でなければならない」というもので、デフォルトではこれが無効でした。
設定自体はAWSコンソールから行うことができるのですが、組織内全アカウント全リージョンでポチポチするのは非常に面倒なので、上記記事を参考にCLI経由で一括設定を試みました。

CloudFormation StackSetsでIAMロールを作成

今回、CLIの実行は組織のルートアカウントのCloudShellから、各アカウントに対してAssumeRoleをした上で行う形としました。
そのため、組織内の全アカウントにIAMロールを作成する必要があります。
CloudFormation StackSetsにて、以下のCFnテンプレートを用いてサービスマネージドなStackSetsを作成することで完了です。
ManagementAccountIdには組織のルートアカウントのIDを入力してください。

AWSTemplateFormatVersion: "2010-09-09"
Description: "IAM Role for managing SSM public sharing settings across accounts via StackSets"

Parameters:
  ManagementAccountId:
    Type: String
    Description: AWS Account ID of the management account that will assume this role
    AllowedPattern: "[0-9]{12}"
    ConstraintDescription: Must be a valid 12-digit AWS Account ID

  RoleName:
    Type: String
    Default: SSMPublicSharingAdminRole
    Description: Name of the IAM role to create
    AllowedPattern: "[a-zA-Z0-9+=,.@_-]+"
    MinLength: 1
    MaxLength: 64

Resources:
  SSMPublicSharingAdminRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      Description: Role for managing SSM public sharing settings across AWS accounts
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${ManagementAccountId}:root"
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SSMPublicSharingManagement
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: ManageSSMServiceSettings
                Effect: Allow
                Action:
                  - ssm:UpdateServiceSetting
                  - ssm:GetServiceSetting
                Resource: "*"
              - Sid: DescribeRegions
                Effect: Allow
                Action:
                  - ec2:DescribeRegions
                Resource: "*"
      Tags:
        - Key: Purpose
          Value: SSMPublicSharingManagement
        - Key: ManagedBy
          Value: StackSets
        - Key: CreatedBy
          Value: CloudFormation

Outputs:
  RoleArn:
    Description: ARN of the created IAM role
    Value: !GetAtt SSMPublicSharingAdminRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}-SSMPublicSharingAdminRole-Arn"

  RoleName:
    Description: Name of the created IAM role
    Value: !Ref SSMPublicSharingAdminRole
    Export:
      Name: !Sub "${AWS::StackName}-SSMPublicSharingAdminRole-Name"

  AssumeRoleCommand:
    Description: AWS CLI command to assume this role
    Value: !Sub |
      aws sts assume-role \
        --role-arn "${SSMPublicSharingAdminRole}" \
        --role-session-name "SSMPublicSharingSession"

CloudShellでスクリプトを実行

組織のルートアカウントでCloudShellを立ち上げ、以下のシェルスクリプトを実行します。
順次実行なので、完了までには結構時間がかかります。

#!/bin/bash

set -euo pipefail

# ---------------------------
# 設定
# ---------------------------
ROLE_NAME="SSMPublicSharingAdminRole"
SESSION_NAME="ManageSSMPublicSharingSession"
DRY_RUN=false
SETTING_VALUE="Disable"  # デフォルト設定値

# 色付き出力
GREEN="\e[32m"
YELLOW="\e[33m"
RED="\e[31m"
RESET="\e[0m"

export AWS_MAX_ATTEMPTS=5  # スロットリング緩和

# ---------------------------
# 関数
# ---------------------------
log_info() { echo -e "${GREEN}[INFO]${RESET} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $1"; }
log_error() { echo -e "${RED}[ERROR]${RESET} $1"; }

assume_role() {
    local account_id="$1"
    aws sts assume-role \
        --role-arn "arn:aws:iam::$account_id:role/$ROLE_NAME" \
        --role-session-name "$SESSION_NAME" \
        --output json 2>/dev/null
}

usage() {
    echo "Usage: $0 [--setting VALUE] [--role ROLE_NAME] [--dry-run]"
    echo "  --setting    SSM パブリック共有設定: Enable または Disable (デフォルト: Disable)"
    echo "  --role       クロスアカウントアクセス用のIAMロール名 (デフォルト: $ROLE_NAME)"
    echo "  --dry-run    実際に更新せずに実行内容を表示"
}

# ---------------------------
# 引数処理
# ---------------------------
while [[ $# -gt 0 ]]; do
    case "$1" in
        --setting)
            if [[ "$2" == "Enable" || "$2" == "Disable" ]]; then
                SETTING_VALUE="$2"
                shift 2
            else
                log_error "不正な設定値: $2. 'Enable' または 'Disable' を指定してください"
                usage
                exit 1
            fi
            ;;
        --role)
            ROLE_NAME="$2"
            shift 2
            ;;
        --dry-run)
            DRY_RUN=true
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            log_error "不明なオプション: $1"
            usage
            exit 1
            ;;
    esac
done

$DRY_RUN && log_info "ドライランモード: 有効"
log_info "設定値: $SETTING_VALUE"
log_info "ロール名: $ROLE_NAME"

# ---------------------------
# メイン処理
# ---------------------------
log_info "SSM パブリック共有権限の更新処理を開始します..."
log_info "対象設定: $SETTING_VALUE"

# ---------------------------
# 管理アカウントを特定
# ---------------------------
CURRENT_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
log_info "管理アカウントID: $CURRENT_ACCOUNT_ID"

# ---------------------------
# アカウント一覧取得
# ---------------------------
log_info "AWS アカウントIDを取得中..."
ACCOUNT_IDS=$(aws organizations list-accounts \
    --query "Accounts[?Status=='ACTIVE'].Id" \
    --output text 2>/dev/null)

if [ -z "$ACCOUNT_IDS" ]; then
    log_error "アクティブなアカウントが見つからないか、権限が不足しています。"
    exit 1
fi

log_info "アクティブなアカウントを $(echo $ACCOUNT_IDS | wc -w) 件発見しました"

# ---------------------------
# 各アカウント処理
# ---------------------------
for ACCOUNT_ID in $ACCOUNT_IDS; do
    log_info "🔄 アカウント処理中: $ACCOUNT_ID"

    # 管理アカウントの場合は直接実行
    if [[ "$ACCOUNT_ID" == "$CURRENT_ACCOUNT_ID" ]]; then
        log_info "📋 管理アカウントを直接処理"
        
        # 全リージョンを取得
        REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text 2>/dev/null)
        
        if [ -z "$REGIONS" ]; then
            log_error "管理アカウントでリージョンが見つかりません。"
            continue
        fi
        
        # 各リージョンで処理
        for region in $REGIONS; do
            log_info "  🌍 リージョン処理中: $region"
            
            if $DRY_RUN; then
                log_info "  🧪 ドライラン: $region で SSM パブリック共有を $SETTING_VALUE に設定予定"
                log_info "  ✅ 処理完了: ${region} (ドライラン - $SETTING_VALUE に設定予定)"
            else
                log_info "  🔧 $region で SSM 設定を更新中..."
                if aws ssm update-service-setting \
                    --setting-id /ssm/documents/console/public-sharing-permission \
                    --setting-value $SETTING_VALUE \
                    --region ${region}; then
                    log_info "  ✅ 処理完了: ${region} ($SETTING_VALUE に設定)"
                else
                    log_warn "  ⚠️ スキップ: ${region} (エラー詳細は上記を確認)"
                fi
            fi
            sleep 0.5  # スロットリング緩和
        done
    else
        # 他のアカウントの場合はAssume Role
        log_info "🔐 アカウントにロール切り替え中: $ACCOUNT_ID"
        
        CREDS_JSON=$(assume_role "$ACCOUNT_ID")
        if [ -z "$CREDS_JSON" ]; then
            log_warn "❌ アカウント $ACCOUNT_ID にロール切り替えできませんでした、スキップします..."
            continue
        fi

        # 一時的に環境変数を設定
        export AWS_ACCESS_KEY_ID=$(echo "$CREDS_JSON" | jq -r '.Credentials.AccessKeyId')
        export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS_JSON" | jq -r '.Credentials.SecretAccessKey')
        export AWS_SESSION_TOKEN=$(echo "$CREDS_JSON" | jq -r '.Credentials.SessionToken')

        # 全リージョンを取得
        REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text 2>/dev/null)
        
        if [ -z "$REGIONS" ]; then
            log_error "アカウント $ACCOUNT_ID でリージョンが見つかりません。"
            unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
            continue
        fi
        
        # 各リージョンで処理
        for region in $REGIONS; do
            log_info "  🌍 リージョン処理中: $region"
            
            if $DRY_RUN; then
                log_info "  🧪 ドライラン: $region で SSM パブリック共有を $SETTING_VALUE に設定予定"
                log_info "  ✅ 処理完了: ${region} (ドライラン - $SETTING_VALUE に設定予定)"
            else
                log_info "  🔧 $region で SSM 設定を更新中..."
                if aws ssm update-service-setting \
                    --setting-id /ssm/documents/console/public-sharing-permission \
                    --setting-value $SETTING_VALUE \
                    --region ${region}; then
                    log_info "  ✅ 処理完了: ${region} ($SETTING_VALUE に設定)"
                else
                    log_warn "  ⚠️ スキップ: ${region} (エラー詳細は上記を確認)"
                fi
            fi
            sleep 0.5  # スロットリング緩和
        done

        # 環境変数をクリア
        unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
    fi
    
    log_info "✅ アカウント完了: $ACCOUNT_ID"
    sleep 2  # アカウント間のスロットリング緩和
    echo ""
done

log_info "🎉 全アカウント処理完了"

余談

今回、シェルスクリプトはClaude Sonnet 4に作ってもらいました。
「組織内の全アカウント全リージョンで特定のCLIコマンドを実行したい」というパターンには結構遭遇するのですが、AssumeRoleを活用することで結構何でも出来ます。
上記シェルスクリプトを基に各自いじってみてください。

Discussion