whoAMI-scanner を AWS Organizations 全体で実行してみた(複数アカウント・複数リージョン対応)
こんにちは!クラウド事業本部のおつまみです。
先日、whoAMI攻撃への対策をまとめたブログを公開しました。
このブログで紹介しているwhoAMI-scannerは、現環境のEC2インスタンスが信頼できるAMIを利用しているか確認できる大変便利なツールです。しかし、標準の使用方法では以下の制約があります。
- 単一アカウント内でのみ実行可能
- 全リージョンまたは単一リージョンのみの実行に限定
しかし今回以下のような要件に直面しました。
- AWS Organizations配下の全アカウントを一括調査したい
- SCPによるリージョン制限があるため、特定の複数リージョンのみを対象に調査したい
そこで本記事では、whoAMI-scannerを管理アカウントから全メンバーアカウントに対して一括実行する方法を紹介します。
事前作業
管理アカウントから各リソースアカウントのリソースを操作するために必要なクロスアカウント用IAMロール(CrossAccountAdminRole)を作成してください。
手順はこちらのブログをご参考ください。
作業手順
1.スクリプト実行前準備
-
共通アカウントにて、CloudShellを起動します
-
以下コマンドを入力し、作業ディレクトリの作成およびwhoAMI-scanner をインストールする。
# 作業ディレクトリを作成 mkdir -p whoAMI-scanner-org cd whoAMI-scanner-org # バイナリをダウンロード wget https://github.com/DataDog/whoAMI-scanner/releases/download/v1.0.0/whoAMI-scanner_1.0.0_linux_amd64.tar.gz tar -xzf whoAMI-scanner_1.0.0_linux_amd64.tar.gz
(実行結果例)
-
以下コマンドを入力し、実行するスクリプトの作成および実行権限を付与する。(トグルを開くと内容を確認できます。)
実行コマンド
cat > scan_by_region.py << 'EOF' #!/usr/bin/env python3 import boto3 import subprocess import json import os import sys import argparse from datetime import datetime import traceback import fcntl # JSTタイムゾーン対応(pytzがなくても動作するように修正) def get_jst_now(): """現在の時刻を取得(JST表記)""" return datetime.now().strftime('%Y-%m-%d %H:%M:%S') + " JST" def get_all_accounts(): """Organization 内の全アカウントのIDとステータスを取得""" client = boto3.client('organizations') accounts = [] try: paginator = client.get_paginator('list_accounts') for page in paginator.paginate(): for account in page['Accounts']: if account['Status'] == 'ACTIVE': accounts.append(account['Id']) except Exception as e: print(f"Error fetching accounts: {str(e)}") traceback.print_exc() return accounts def assume_role(account_id, role_name): """指定したアカウントの IAM ロールを引き受ける""" sts_client = boto3.client('sts') try: response = sts_client.assume_role( RoleArn=f'arn:aws:iam::{account_id}:role/{role_name}', RoleSessionName='WhoAMIScannerSession' ) return { 'AWS_ACCESS_KEY_ID': response['Credentials']['AccessKeyId'], 'AWS_SECRET_ACCESS_KEY': response['Credentials']['SecretAccessKey'], 'AWS_SESSION_TOKEN': response['Credentials']['SessionToken'] } except Exception as e: print(f"Error assuming role for account {account_id}: {str(e)}") return None def run_scanner(account_id, credentials, region, output_dir): """whoAMI-scanner を実行""" print(f"Scanning account: {account_id} in region: {region}") # 環境変数を設定して whoAMI-scanner を実行 env = os.environ.copy() if credentials: env.update(credentials) # whoAMI-scanner の存在確認 scanner_path = './whoAMI-scanner' if not os.path.exists(scanner_path): error_msg = f"Error: whoAMI-scanner not found at {os.path.abspath(scanner_path)}" print(error_msg) return False # 実行権限の確認 if not os.access(scanner_path, os.X_OK): print(f"Adding execute permission to {scanner_path}") os.chmod(scanner_path, 0o755) # コマンドを構築 command = [scanner_path, '-verbose', '-region', region] # スキャン実行 try: result = subprocess.run( command, env=env, capture_output=True, text=True ) # アカウント別の結果ファイル account_result_file = f"{output_dir}/account_{account_id}_results.txt" # ファイルロックを使用して安全に追記 with open(account_result_file, 'a') as f: fcntl.flock(f, fcntl.LOCK_EX) f.write(f"\n\n{'='*80}\n") f.write(f"REGION: {region} | TIME: {get_jst_now()}\n") f.write(f"{'='*80}\n\n") f.write(result.stdout) fcntl.flock(f, fcntl.LOCK_UN) # エラーがある場合のみエラーファイルに追記 if result.stderr: account_error_file = f"{output_dir}/account_{account_id}_errors.txt" with open(account_error_file, 'a') as f: fcntl.flock(f, fcntl.LOCK_EX) f.write(f"\n\n{'='*80}\n") f.write(f"REGION: {region} | TIME: {get_jst_now()}\n") f.write(f"{'='*80}\n\n") f.write(result.stderr) fcntl.flock(f, fcntl.LOCK_UN) print(f"Completed scanning account: {account_id} in region: {region}") return True except Exception as e: error_msg = f"Error scanning account {account_id} in region {region}: {str(e)}" print(error_msg) # エラーファイルを作成 account_error_file = f"{output_dir}/account_{account_id}_errors.txt" with open(account_error_file, 'a') as f: fcntl.flock(f, fcntl.LOCK_EX) f.write(f"\n\n{'='*80}\n") f.write(f"REGION: {region} | TIME: {get_jst_now()}\n") f.write(f"{'='*80}\n\n") f.write(f"{error_msg}\n") f.write(traceback.format_exc()) fcntl.flock(f, fcntl.LOCK_UN) return False def ensure_file_header(file_path, account_id, file_type): """ファイルが存在しない場合はヘッダーを作成""" if not os.path.exists(file_path): with open(file_path, 'w') as f: fcntl.flock(f, fcntl.LOCK_EX) f.write(f"whoAMI-scanner {file_type} - Account: {account_id}\n") f.write(f"Scan started at: {get_jst_now()}\n") f.write(f"{'='*80}\n") fcntl.flock(f, fcntl.LOCK_UN) def main(): parser = argparse.ArgumentParser(description='Run whoAMI-scanner for a specific region across all accounts') parser.add_argument('--region', required=True, help='AWS region to scan') parser.add_argument('--role', default='CrossAccountAdminRole', help='IAM role name to assume (default: CrossAccountAdminRole)') parser.add_argument('--output-dir', default='scan_results', help='Directory to store scan results (default: scan_results)') args = parser.parse_args() # 出力ディレクトリを設定 output_dir = args.output_dir os.makedirs(output_dir, exist_ok=True) # リージョン別の結果概要ファイル summary_file = f"{output_dir}/region_{args.region}_summary.txt" # 概要ファイルのヘッダーを作成 with open(summary_file, 'w') as f: f.write(f"whoAMI-scanner Summary - Region: {args.region}\n") f.write(f"Scan started at: {get_jst_now()}\n") f.write(f"{'='*80}\n\n") # 管理アカウントのIDを取得 sts_client = boto3.client('sts') try: management_account_id = sts_client.get_caller_identity()['Account'] print(f"Management account ID: {management_account_id}") # 管理アカウント用の結果ファイルのヘッダーを確保 account_result_file = f"{output_dir}/account_{management_account_id}_results.txt" ensure_file_header(account_result_file, management_account_id, "Results") # 管理アカウントは直接実行 print(f"Scanning management account: {management_account_id} in region: {args.region}") run_scanner(management_account_id, None, args.region, output_dir) # 概要ファイルに追記 with open(summary_file, 'a') as f: f.write(f"- Scanned management account: {management_account_id}\n") # Organization 内の全アカウントを取得 accounts = get_all_accounts() print(f"Found {len(accounts)} accounts in the organization") # 他のアカウントはロールを引き受けて実行 for account_id in accounts: if account_id != management_account_id: # アカウント用の結果ファイルのヘッダーを確保 account_result_file = f"{output_dir}/account_{account_id}_results.txt" ensure_file_header(account_result_file, account_id, "Results") credentials = assume_role(account_id, args.role) if credentials: run_scanner(account_id, credentials, args.region, output_dir) # 概要ファイルに追記 with open(summary_file, 'a') as f: f.write(f"- Scanned account: {account_id}\n") else: # 概要ファイルに追記 with open(summary_file, 'a') as f: f.write(f"- Failed to scan account: {account_id} (role assumption failed)\n") print(f"Skipping account {account_id} due to role assumption failure") # スキャン完了時間を記録 with open(summary_file, 'a') as f: f.write(f"\nScan completed at: {get_jst_now()}\n") print(f"Scan completed for region {args.region}") print(f"Summary stored in {summary_file}") except Exception as e: print(f"Error in main execution: {str(e)}") sys.exit(1) if __name__ == "__main__": main() EOF chmod +x scan_by_region.py cat > run_org_scan.sh << 'EOF' #!/bin/bash # JSTタイムゾーンを設定 export TZ=Asia/Tokyo # 実行開始時間を記録 (JST) start_time=$(date +"%Y-%m-%d %H:%M:%S JST") echo "スキャン開始時間: $start_time" # スクリプトのディレクトリを取得 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" echo "スクリプトディレクトリ: $SCRIPT_DIR" # whoAMI-scanner の存在確認 if [ ! -f "$SCRIPT_DIR/whoAMI-scanner" ]; then echo "whoAMI-scanner が見つかりません。ダウンロードします。" wget https://github.com/DataDog/whoAMI-scanner/releases/download/v1.0.0/whoAMI-scanner_1.0.0_linux_amd64.tar.gz tar -xzf whoAMI-scanner_1.0.0_linux_amd64.tar.gz chmod +x whoAMI-scanner fi # リージョンの配列を定義 regions=("us-east-1" "ap-northeast-1" "ap-northeast-3") # タイムスタンプを生成(すべてのリージョンで共通のディレクトリを使用) timestamp=$(date +"%Y%m%d_%H%M%S") output_dir="$SCRIPT_DIR/scan_results/$timestamp" mkdir -p "$output_dir" echo "出力ディレクトリ: $output_dir" # IAMロール名(必要に応じて変更) role_name="CrossAccountAdminRole" # 各リージョンを別々のプロセスで実行 pids=() for region in "${regions[@]}"; do echo "Starting scan for region: $region" python3 "$SCRIPT_DIR/scan_by_region.py" --region "$region" --role "$role_name" --output-dir "$output_dir" > "${output_dir}/${region}_execution.log" 2>&1 & pids+=($!) done # すべてのバックグラウンドプロセスが終了するまで待機 echo "すべてのリージョンのスキャンを実行中..." for pid in "${pids[@]}"; do wait $pid status=$? if [ $status -ne 0 ]; then echo "プロセス $pid が異常終了しました(終了コード: $status)" fi done # 実行終了時間を記録 (JST) end_time=$(date +"%Y-%m-%d %H:%M:%S JST") echo "スキャン終了時間: $end_time" # 結果の概要を作成 echo "すべてのリージョンのスキャンが完了しました" echo "結果は以下のディレクトリに保存されています: $output_dir" # 全リージョンの結果を1つのファイルにまとめる consolidated_file="${output_dir}/all_regions_summary.txt" echo "全リージョンの概要を1つのファイルにまとめています..." # ヘッダーを作成 echo "whoAMI-scanner Consolidated Summary - All Regions" > "$consolidated_file" echo "Scan period: $start_time to $end_time" >> "$consolidated_file" echo "$(printf '=%.0s' {1..80})" >> "$consolidated_file" echo "" >> "$consolidated_file" # 各リージョンの概要を統合 for region in "${regions[@]}"; do region_summary="${output_dir}/region_${region}_summary.txt" if [ -f "$region_summary" ]; then echo "" >> "$consolidated_file" echo "$(printf '#%.0s' {1..80})" >> "$consolidated_file" echo "# REGION: $region" >> "$consolidated_file" echo "$(printf '#%.0s' {1..80})" >> "$consolidated_file" echo "" >> "$consolidated_file" cat "$region_summary" >> "$consolidated_file" else echo "Warning: Summary file for $region not found at $region_summary" fi done # 各アカウントの結果ファイルに完了時間を追記 for account_file in "${output_dir}"/account_*_results.txt; do if [ -f "$account_file" ]; then echo -e "\n\nScan completed at: $end_time" >> "$account_file" echo "$(printf '=%.0s' {1..80})" >> "$account_file" fi done # 結果の概要を表示 echo "アカウント別スキャン結果概要:" # アカウント結果ファイルを検索 account_files=$(find "$output_dir" -name "account_*_results.txt" | sort) for file in $account_files; do account_id=$(basename "$file" | cut -d'_' -f2) error_file="${output_dir}/account_${account_id}_errors.txt" if [ -f "$error_file" ]; then echo "- アカウント $account_id: スキャン完了 (エラーあり)" else echo "- アカウント $account_id: スキャン完了" fi done # 結果をZIPファイルにまとめる(階層を変更) zip_file="${SCRIPT_DIR}/whoAMI_scan_${timestamp}.zip" echo "結果をZIPファイルにまとめています: $zip_file" # 現在のディレクトリを保存 current_dir=$(pwd) # 出力ディレクトリに移動してZIPを作成(相対パスを使用) cd "$output_dir" zip -r "$zip_file" . > /dev/null cd "$current_dir" echo "スキャン完了!" echo "開始時間: $start_time" echo "終了時間: $end_time" echo "統合概要ファイル: $consolidated_file" echo "結果ZIP: $zip_file" EOF chmod +x run_org_scan.sh
-
以下のコマンドを入力し、
run_org_scan.sh
,scan_by_region.py
が作成されていることを確認する。# スクリプトファイルが作成されていることを確認 ls -la
(実行結果例)
2.スクリプト実行・結果ファイルのダウンロード
-
以下のコマンドを入力し、スクリプトを実行する。
# スクリプト実行 ./run_org_scan.sh
(実行結果例)
-
[結果ZIP]に出力されたzipファイルをCloudShellからダウンロードする。[アクション]→[ファイルのダウンロード]を選択する。
-
[個別のファイルパス]に[結果ZIPに出力されたzipファイル名を入力し、[ダウンロード]を選択する。
3.結果ファイルの確認
-
ローカルにダウンロードしたzipファイルを解凍する。以下のパスでファイルが出力される。
/ ├── account_[アカウントID]_results.txt # 各アカウントの結果(選択した全リージョンをまとめたもの) ├── account_[アカウントID]_errors.txt # エラーがある場合のみ(選択した全リージョンをまとめたもの) ├── region_[リージョン]_summary.txt # リージョン別の概要 ├── all_regions_summary.txt # 全リージョンの概要 └── [リージョン]_execution.log # 実行ログ(デバッグ用)
-
account_[アカウントID]_results.txt
を開き、Public, unverified, & unknown
にカウントされているインスタンスおよびAMIがないか確認する。
(出力結果例)whoAMI-scanner Results - Account: 123456789012 Scan started at: 2025-03-04 10:11:31 JST ================================================================================ ================================================================================ REGION: ap-northeast-3 | TIME: 2025-03-04 10:11:31 JST ================================================================================ [ 👀 whoAMI-scanner v1.0.0 👀 ] AWS Caller Identity: arn:aws:sts::123456789012:assumed-role/cm-matsunami.kana/cm-matsunami.kana [*] Verbose mode enabled. [*] Starting AMI analysis... [*] [ap-northeast-3] Allowed AMI Accounts status: Disabled Summary Key: +-------------------------------+-----------------------------------------------------------+ | Term | Definition | +-------------------------------+-----------------------------------------------------------+ | Self hosted | AMIs from this account | | Allowed AMIs | AMIs from an allowed account per the AWS Allowed AMIs API | | Trusted AMIs | AMIs from an trusted account per user input to this tool | | Verified AMIs | AMIs from Verified Accounts (Verified by Amazon) | | Shared with me (Private) | AMIs shared privately with this account but NOT from a | | | verified, trusted or allowed account. If you trust this | | | account, add it to your Allowed AMIs API or specify it as | | | trusted in the whoAMI-scanner command line. | | Public, unverified, but known | AMIs from unverified accounts, but we found the account | | | ID in fwdcloudsec's known_aws_accounts mapping: | | | https://github.com/fwdcloudsec/known_aws_accounts. | | | These are likely safe to use but worth investigating. | | Public, unverified, & unknown | AMIs from unverified accounts. Be cautious with these | | | unless they are from accounts you control. If not from | | | your accounts, look to replace these with AMIs from | | | verified accounts | +-------------------------------+-----------------------------------------------------------+ Summary: AWS's "Allowed AMI" config status by region Enabled/Audit-mode/Disabled: 0/0/1 Total Instances: 0 Total AMIs: 0 Self hosted AMIs: 0 Allowed AMIs: 0 Trusted AMIs: 0 Verified AMIs: 0 Shared with me (Private) AMIs: 0 Public, unverified, but known: 0 Public, unverified, & unknown AMIs: 0 [!] No regions have AWS's "Allowed AMIs" feature enabled or in audit mode. Enabling Allowed AMIs protects you against the whoAMI attack. Visit https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-allowed-amis.html for more information. ================================================================================ REGION: ap-northeast-1 | TIME: 2025-03-04 10:11:31 JST ================================================================================ [ 👀 whoAMI-scanner v1.0.0 👀 ] AWS Caller Identity: arn:aws:sts::123456789012:assumed-role/cm-matsunami.kana/cm-matsunami.kana [*] Verbose mode enabled. [*] Starting AMI analysis... [*] [ap-northeast-1] Allowed AMI Accounts status: Disabled [1/1][ap-northeast-1] ami-072298436ce5cb0c4 being analyzed (Instance: i-04ca1a803f1315e6e) [1/1][ap-northeast-1] ami-072298436ce5cb0c4 is a community AMI from an AWS verified account. Summary Key: +-------------------------------+-----------------------------------------------------------+ | Term | Definition | +-------------------------------+-----------------------------------------------------------+ | Self hosted | AMIs from this account | | Allowed AMIs | AMIs from an allowed account per the AWS Allowed AMIs API | | Trusted AMIs | AMIs from an trusted account per user input to this tool | | Verified AMIs | AMIs from Verified Accounts (Verified by Amazon) | | Shared with me (Private) | AMIs shared privately with this account but NOT from a | | | verified, trusted or allowed account. If you trust this | | | account, add it to your Allowed AMIs API or specify it as | | | trusted in the whoAMI-scanner command line. | | Public, unverified, but known | AMIs from unverified accounts, but we found the account | | | ID in fwdcloudsec's known_aws_accounts mapping: | | | https://github.com/fwdcloudsec/known_aws_accounts. | | | These are likely safe to use but worth investigating. | | Public, unverified, & unknown | AMIs from unverified accounts. Be cautious with these | | | unless they are from accounts you control. If not from | | | your accounts, look to replace these with AMIs from | | | verified accounts | +-------------------------------+-----------------------------------------------------------+ Summary: AWS's "Allowed AMI" config status by region Enabled/Audit-mode/Disabled: 0/0/1 Total Instances: 1 Total AMIs: 1 Self hosted AMIs: 0 Allowed AMIs: 0 Trusted AMIs: 0 Verified AMIs: 1 Shared with me (Private) AMIs: 0 Public, unverified, but known: 0 Public, unverified, & unknown AMIs: 0 [!] No regions have AWS's "Allowed AMIs" feature enabled or in audit mode. Enabling Allowed AMIs protects you against the whoAMI attack. Visit https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-allowed-amis.html for more information. ================================================================================ REGION: us-east-1 | TIME: 2025-03-04 10:11:32 JST ================================================================================ [ 👀 whoAMI-scanner v1.0.0 👀 ] AWS Caller Identity: arn:aws:sts::123456789012:assumed-role/cm-matsunami.kana/cm-matsunami.kana [*] Verbose mode enabled. [*] Starting AMI analysis... [*] [us-east-1] Allowed AMI Accounts status: Disabled Summary Key: +-------------------------------+-----------------------------------------------------------+ | Term | Definition | +-------------------------------+-----------------------------------------------------------+ | Self hosted | AMIs from this account | | Allowed AMIs | AMIs from an allowed account per the AWS Allowed AMIs API | | Trusted AMIs | AMIs from an trusted account per user input to this tool | | Verified AMIs | AMIs from Verified Accounts (Verified by Amazon) | | Shared with me (Private) | AMIs shared privately with this account but NOT from a | | | verified, trusted or allowed account. If you trust this | | | account, add it to your Allowed AMIs API or specify it as | | | trusted in the whoAMI-scanner command line. | | Public, unverified, but known | AMIs from unverified accounts, but we found the account | | | ID in fwdcloudsec's known_aws_accounts mapping: | | | https://github.com/fwdcloudsec/known_aws_accounts. | | | These are likely safe to use but worth investigating. | | Public, unverified, & unknown | AMIs from unverified accounts. Be cautious with these | | | unless they are from accounts you control. If not from | | | your accounts, look to replace these with AMIs from | | | verified accounts | +-------------------------------+-----------------------------------------------------------+ Summary: AWS's "Allowed AMI" config status by region Enabled/Audit-mode/Disabled: 0/0/1 Total Instances: 0 Total AMIs: 0 Self hosted AMIs: 0 Allowed AMIs: 0 Trusted AMIs: 0 Verified AMIs: 0 Shared with me (Private) AMIs: 0 Public, unverified, but known: 0 Public, unverified, & unknown AMIs: 0 [!] No regions have AWS's "Allowed AMIs" feature enabled or in audit mode. Enabling Allowed AMIs protects you against the whoAMI attack. Visit https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-allowed-amis.html for more information. Scan completed at: 2025-03-04 10:11:35 JST ================================================================================
結果の確認方法については、以下もご参考ください。
4.お片付け
-
共通アカウントのCloudShellにて以下のコマンドを実行し、作業ディレクトリを削除します。
# ディレクトリを移動 cd .. # 作業ディレクト全体を削除 rm -rf whoAMI-scanner-org/
(実行結果)
-
共通アカウントにて、以下手順の「スタックを削除する」のみ実施する
おまけ:スクリプトのカスタマイズ方法
今回作成したスクリプトをカスタマイズする方法をご紹介します。
スキャン対象リージョンの変更
run_org_scan.sh
ファイル内の以下の部分を編集して、スキャン対象のリージョンを変更できます。
# リージョンの配列を定義
regions=("us-east-1" "ap-northeast-1" "ap-northeast-3")
例えば、すべての主要リージョンをスキャンする場合は、以下のように設定します。
regions=("us-east-1" "us-east-2" "us-west-1" "us-west-2" "ap-northeast-1" "ap-northeast-2" "ap-northeast-3" "ap-southeast-1" "ap-southeast-2" "eu-central-1" "eu-west-1" "eu-west-2" "eu-west-3" "sa-east-1")
IAM ロール名の変更
クロスアカウントアクセスに使用するIAMロール名を変更する場合は、run_org_scan.sh
ファイル内の以下の部分を編集します
# IAMロール名(必要に応じて変更)
role_name="CrossAccountAdminRole"
特定のアカウントのみスキャン
特定のアカウントのみをスキャンする場合は、scan_by_region.py
を修正して、アカウントIDのフィルタリングを追加します。
# 特定のアカウントのみをスキャン
target_accounts = ["123456789012", "098765432109"]
for account_id in accounts:
if account_id in target_accounts and account_id != management_account_id:
credentials = assume_role(account_id, args.role)
if credentials:
run_scanner(account_id, credentials, args.region, output_file, error_file)
さいごに
今回は、whoAMI-scannerを管理アカウントから全メンバーアカウントに対して一括実行する方法をご紹介しました。
AMIの安全性確認は、whoAMI攻撃対策として非常に重要ですが、複数アカウント・複数リージョンにまたがる環境では実施が煩雑になりがちです。今回ご紹介したスクリプトを活用することで、AWS Organizations全体の安全性を効率的に確認できるため、ぜひご活用ください!
最後までお読みいただきありがとうございました!
以上、おつまみ(@AWS11077)でした!