共有 AWS アカウントで「誰が作ったか」が分からない問題と、作者特定ツールの話

問題の所在

クラウドセキュリティポスチャ管理(CSPM)サービスは、AWS 上の設定ミスやリスクを継続的に検知する。
検知結果には対象リソースの種別、名前、ARN、所属アカウント ID が載る。
しかし、そのリソースを誰が作成したかは、多くの場合そのままでは分からない。

個人専用の AWS アカウントなら、担当者は一人に定まる。
一方、検証環境や PoC 用など、複数のエンジニアが同一アカウントを使い回す運用では状況が変わる。
セキュリティ上の指摘が来ても「誰に直してもらえばよいか」が最初から決まらない。
結果として、セキュリティ担当が手作業で CloudTrail を調べるか、Slack で「誰が作った?」と聞くか、どちらかになる。

この手作業は毎回同じ手順の繰り返しであり、指摘件数が増えるほどボトルネックになる。
そこで、CSPM の検知結果から Asana タスクを自動作成する同期パイプラインに、作者特定の処理を組み込んだ。

同期パイプラインの全体像

同期ツールは、CSPM から HIGH / CRITICAL の OPEN な指摘を毎日取得し、1 件ずつ Asana タスクに変換する。
取得と作成は Lambda 2 段構成で、SQS を挟んで並列処理する。

EventBridge(日次スケジュール)
    └── Lambda「一覧取得」
            └── CSPM API … HIGH / CRITICAL の OPEN 指摘をページング取得
            └── SQS … 1 指摘 = 1 メッセージ(JSON)
                    └── Lambda「同期ワーカー」
                            ├── 作者特定(Author Resolver)
                            ├── Asana API … タスク作成 / 更新
                            └── DynamoDB … 指摘 ID とタスク ID の対応

一覧取得 Lambda が CSPM から指摘を集め、SQS に投入する。
同期ワーカー Lambda がメッセージを 1 件ずつ取り出し、作者を特定してから Asana タスクを作成または更新する。
DynamoDB には指摘 ID と Asana タスク ID の対応を保存し、同じ指摘が再検知されたときは新規作成せず更新する。

作者特定の成否は、タスクの担当者割り当てに直結する。
特定できれば Asana 上のメールアドレスから担当者を解決し、タスクに割り当てる。
特定できなければ担当者なし(unassigned)のまま作成し、タスク本文に「作者不明」と記載する。

作者特定の優先順位

作者特定ロジック(AuthorResolver)は、複数の情報源を順に試す。
前段で確定すれば、後段の API 呼び出しは行わない。

1. リソースタグ
      → created_by / Creator / owner / Owner / createdby / created-by(大文字小文字を無視)
      → 値があれば作者として採用

2. 個人アカウントマップ(account-map の individual)
      → アカウント ID に対応するメールアドレスを即返却(CloudTrail 検索なし)

3. CloudFormation スタック名タグ
      → aws:cloudformation:stack-name タグがあれば、スタックの CreateStack イベントを CloudTrail で検索

4. CloudTrail LookupEvents(過去 90 日)
      → リソース ID / 名前 / コンソール URL から抽出した ID を候補に、作成系イベント(Create* / Put* / Run* など)を検索
      → 実行者 Username を作者として採用

5. AWS Config で作成時刻を特定 → CloudTrail を前後 2 時間に絞り込み
      → Config の resourceCreationTime を取得し、CloudTrail 検索範囲を狭めて再試行

6. いずれも失敗
      → None(作者不明)

タグに created_byowner が付いていれば、それが最も確実で低コストな。
タグ運用が徹底されている環境では、ここで大半が解決する。

タグが無い場合、次に account-map の判定に入る。
account-map は「この AWS アカウント ID をどう扱うか」を定義する JSON で、SSM Parameter Store に保管する。

json
{
  "individual": {
    "246216520959": "engineer-a@example.com"
  },
  "team": [
    "123456789012",
    "987654321098"
  ]
}

individual は個人占有アカウントを表す。
載っているアカウント ID に対する指摘は、対応するメールアドレスを作者とみなし、CloudTrail 検索を省略する。

Org オーナー(Organizations の管理アカウント)であれば、Accounts API 等でアカウントの連絡先や請求管理者を取得し、individual の登録を API 側で埋められる可能性がある。
本件では、同期ワーカーに Org オーナー相当の権限を渡すのは権限範囲が広すぎると判断し、採用しなかった。
account-map は手動メンテナンスが残るが、ツールが触る権限を member アカウントの CloudTrail / Config 読み取りに限定できる。

team は複数人が共有するアカウントを表す。
載っているアカウント ID に対する指摘だけ、クロスアカウントで CloudTrail / AWS Config を検索する。
どちらにも載っていないアカウントの指摘は、作者特定をスキップする(意図的に「不明」のままにする)。
未登録アカウントへの AssumeRole を防ぐためのガードである。

共有アカウント向けのクロスアカウント設計

team アカウントの CloudTrail は、そのアカウント内に記録される。
同期ワーカー Lambda はセントラルアカウント(ツールをデプロイしたアカウント)で動くため、対象アカウントへ AssumeRole して読み取る必要がある。

各 team アカウントには、読み取り専用ロール AuthorResolver を配置する。
付与する権限は次の 2 つだけである。

  • cloudtrail:LookupEvents
  • config:GetResourceConfigHistory

トラストポリシーでは、セントラルアカウントの同期ワーカーロールからの AssumeRole のみを許可する。
書き込み権限は一切付けない。

Terraform の apply 時に account-map の team リストを読み取り、ワーカーロールの sts:AssumeRole の Resource を動的に構築する。
team にアカウントを追加したあと、IAM ポリシーへ反映するには terraform apply が必要になる(通常は CI/CD パイプラインが走る)。

新規 team アカウントへのロール作成は、GitHub Actions からは行わない。
Actions が全アカウントの IAM 管理権限を持たないよう意図的に分離しており、運用者が手元からスクリプトを実行する。

bash
./scripts/create-author-resolver-role.sh \
  --trusted-account-id <DEV_CENTRAL_ID> \
  --trusted-account-id <PRD_CENTRAL_ID> \
  --profile AdministratorAccess-<新規アカウントID>

CloudTrail 検索の実装上の工夫

CloudTrail の LookupEvents は、ResourceName 属性でフィルタする。
CSPM が返すリソース情報から、検索に使う候補 ID を複数組み立てる。

  1. クラウドコンソール URL から AWS ネイティブ ID を抽出(例: i-07d4bb1bfa5a09738、バケット名、Lambda 関数名)
  2. リソース名(entitySnapshot.name
  3. ARN 全体

候補を順に試し、Create / Put / Run / Launch / Provision / Register で始まるイベント名の実行者 Username を採用する。

リージョンの選び方も誤検索の原因になりやすい。
LookupEvents を送るリージョンは、CSPM が返す region フィールドを優先し、無ければ ARN 内のリージョン、それも無ければ Lambda の実行リージョン(既定: ap-northeast-1)を使う。

90 日分の CloudTrail を広く検索しても見つからない場合、AWS Config に切り替える。
Config の設定履歴から resourceCreationTime を取得し、CloudTrail 検索を作成時刻の前後 2 時間に絞り込む。
イベント数が多いアカウントで、ResourceName だけでは作成イベントが埋もれるケース向けのフォールバックである。

CloudFormation 経由で作られたリソースには aws:cloudformation:stack-name タグが付くことが多い。
このタグがあれば、個別リソース ID ではなくスタック名で CloudTrail を検索する経路も試す。

CloudTrail を AWS Organizations の Organization トレイルとして集約している環境では、管理アカウント側からメンバーアカウント横断でイベントを追いやすい。
本ツールは team アカウントごとに AssumeRole して LookupEvents を呼ぶ実装だが、Org 全体のトレイルがすでに存在するなら、検索の起点を管理アカウントに寄せる選択肢もある。
Organization トレイルを S3 バケットへ配信している構成であれば、保持期間の制約も LookupEvents 単体より緩和しやすい。

account-map の運用

account-map は SSM Parameter Store(SecureString、KMS 暗号化)に 1 つ置く。
dev / prd のセントラルアカウントごとに独立したパラメータが存在する。

更新は GitHub Actions ワークフロー(Manage account-map)が主経路である。
list / add-team / remove-team / add-individual / remove-individual の操作を Web UI から実行できる。
both を選べば dev / prd 両環境へ同じ変更を一括適用する。

ローカル CLI(manage_account_map.py)も用意してある。
SSM から現在値を表示したり、dry-run で追加内容を確認したり、既存 JSON から初期投入したりできる。

team アカウントを追加したときの作業順序は次のとおりである。

  1. GitHub Actions または CLI で account-map の team リストを更新
  2. 対象アカウントに AuthorResolver ロールを手動作成
  3. deployment/dev または deployment/prd へ push し、Terraform apply でワーカーロールの AssumeRole 権限を更新

監査のため、SSM Parameter History と CloudTrail の PutParameter イベント、GitHub Actions の実行履歴の 3 系統で変更者を追える。

タスクに載る情報

作者特定の結果は Asana タスクの本文に含める。
担当者割り当てと本文の両方に反映するため、後から「誰に割り当てようとしたか」を確認できる。

【対象リソース】
  種別: BUCKET
  名前: my-bucket-name
  ID: arn:aws:s3:::my-bucket-name
  サブスクリプション: 123456789012

【リソース作者】engineer-b@example.com

作者不明の場合は担当者なしで作成し、本文にその旨を書く。
dev 環境では DRY_RUN_ASSIGNEE=true を設定し、担当者割り当てを行わず「本来の担当予定者」を本文にだけ記載する。
開発環境から実利用者へ Asana 通知が飛ぶのを防ぐためのフラグである。

限界と今後の改善余地

作者特定は確率的な処理であり、すべての指摘で担当者を決められるわけではない。

本ツールは CloudTrail の LookupEvents API だけを使っており、検索できるのは過去 90 日分に限られる。
CloudTrail のイベント履歴をそのまま各アカウント内に置くだけでは、90 日を過ぎた作成イベントは参照できない。
Organization トレイルで S3 へ配信し、CloudTrail Lake や Athena で問い合わせる構成にしておけば、保持期間と検索手段を別途設計できる。
本記事の実装は LookupEvents に絞っているため、90 日より前に作られたリソースは CloudTrail 経路では特定できない。

AssumeRole 先のロールが未作成、または account-map に未登録のアカウントは、検索自体をスキップする。
IAM ロールや SCP 経由の操作では、CloudTrail の Username が人間のメールアドレスにならないことがある。

タグ運用(created_byowner をリソース作成時に必ず付与する)を徹底すれば、CloudTrail 検索に頼る件数は大きく減る。
Infrastructure as Code でデプロイするリソースには、テンプレート側でタグを埋め込むのが最も効果が高い。

個人アカウントと team アカウントを account-map で明示的に分けたのは、不要な AssumeRole と CloudTrail API 呼び出しを避けるためでもある。
個人占有アカウントと team アカウントが混在する環境では、individual に載せた分だけ API コストとレイテンシを削減できる。

作者特定をパイプラインに載せた結果

共有 AWS アカウントでは、CSPM の指摘だけでは作成者が分からず、対応の初動が遅れやすい。
タグ、account-map、CloudTrail、AWS Config を段階的に試す作者特定ロジックを同期パイプラインに組み込むことで、指摘から Asana タスク作成までを自動化し、担当者割り当てまで含めて処理できるようになった。

完全自動ではない部分(team アカウントへのロール作成、account-map のメンテナンス)は残る。
それでも、毎日発生する HIGH / CRITICAL 指摘ごとに CloudTrail を手で調べるより、運用負荷は大幅に下がる。

タグ付けの習慣と account-map の更新をチーム運用に組み込めば、「誰が作ったか分からない」状態はかなり減らせる。

この記事をシェア