- 1 1. なぜIAM棚卸し自動化が必要か — Vol1-2の延長線・運用フェーズの新たな壁
- 2 2. 棚卸し対象の俯瞰 — User/Role/Policy/Permission Set 4分類とリスク特性
- 3 3. IAM Access Analyzer Policy Generation 実践
- 4 4. IAM Last Accessed × Access Advisor 実践 — 未使用権限の検出と削除サイクル
- 5 5. CloudTrail Lake によるIAM活動分析 — SQLクエリで権限利用状況を可視化する
- 6 6. 詰まりポイント7選 図解
- 7 7. 棚卸し自動化アーキテクチャ実装 — EventBridge + Lambda + Terraform IaC化
- 8 8. まとめ + Vol4予告 + 落とし穴10選
1. なぜIAM棚卸し自動化が必要か — Vol1-2の延長線・運用フェーズの新たな壁
IAM権限は「作って終わり」ではない。プロジェクト開始時に設計した最小権限は、時間の経過とともに少しずつ侵食される。退職者のアカウントが残り、プロジェクト終了後のロールが削除されず、「とりあえず動かしたい」の一言で付与した広権限が恒久化していく。
IAMポリシー設計入門 (Vol1) では「どう正しく作るか」を学んだ。複数アカウント時代のIAM設計 (Vol2) では「組織全体にどうガードレールを張るか」を学んだ。本記事は第3フェーズ——「作ったものを正しく保つ」——を扱う。
1-1. 3つの運用詰まりシーン
IAMを実際に運用していると、設計時には想定していなかった3つの「詰まり」に直面する。いずれも単一アカウント・複数アカウントを問わず、組織規模が大きくなるほど深刻になる。
詰まりシーン①: IAMリソースが時間と共に肥大化する
あるスタートアップは AWSアカウントを開設した時点では IAM User が5名、IAM Role が10本程度だった。1年後、その数は User 50名・Role 200本を超えていた。
原因は単純だ。人が増え、プロジェクトが増え、試験環境が作られ、そのたびにIAMリソースが積み重なる。問題は「削除」がほとんど行われないことだ。
- 退職した社員の IAM User が残留している
- 終了したプロジェクトの EC2 用 Role が誰も使っていないのに残っている
- CI/CD パイプライン用の Role が複数世代分存在し、最新版がどれか分からない
- 「管理者に聞けば分かるはず」の人が退職してドキュメントが消えた
このような状態が続くと、「誰が何に何権限を持っているか」を把握することが不可能になる。IAM User/Role の棚卸しをしようにも対象が多すぎて手動確認が追いつかず、「全体像が見えない」という負のスパイラルに陥る。
最小権限の維持どころか、現在のアクセス権限の把握すらできない状態だ。これが IAM棚卸し自動化が必要になる第1の理由だ。
詰まりシーン②: 最小権限が徐々に侵食される
IAM権限の設計を正しく行っても、運用の中で少しずつ広がっていく。
典型的な侵食パターン:
1. Lambda 関数のデプロイ時に「動かないから」とポリシーを緩める
2. 緩めた権限で動作確認したまま本番リリース
3. 「後で絞る」が先送りになり、そのまま恒久化
4. 半年後: 「このRoleに何でこんな権限があるの?」
この「権限の侵食」は単発では小さな問題でも、積み重なると全体のセキュリティポスチャを悪化させる。IAM Access Analyzer や Last Accessed といった分析ツールの存在は多くのエンジニアが知っている。しかし「具体的にどう使うか」「出力をどう解釈して削除を判断するか」で詰まる。
Vol1で学んだ最小権限特定法は設計時の知識だ。運用フェーズでは「今現在、実際に使われている権限はどれか」を継続的に計測する仕組みが必要になる。Access Analyzer Policy Generation はまさにこの問いに答えるツールだが、「90日間のCloudTrailログを読み込んで最小権限ポリシーを自動生成する」という動作原理を知らなければ使いこなせない。
詰まりシーン③: 定期棚卸しが手動で続かない
多くの組織では、IAM権限の棚卸しを「四半期に1回、スプレッドシートに手動でまとめる」形で実施している。このアプローチには構造的な限界がある。
手動棚卸しの限界:
- IAM User/Role 数が100を超えると棚卸しに1週間かかる
- コンソールの画面を見ながら手入力するため人的ミスが避けられない
- 棚卸し結果が陳腐化するのが早い (翌週には変わっている)
- 担当者が異動・退職すると棚卸しプロセス自体が消滅する
- 「棚卸し中に発見した過剰権限をいつ削除するか」の判断基準がない
EventBridge + Lambda で自動化したいと思っても、「CloudTrail のイベントをどう拾うか」「Access Analyzer をどう呼び出すか」「結果をどこに出力して誰が確認するか」というアーキテクチャの全体像が見えない段階では着手できない。
本記事はこの「手動棚卸しからの脱却」を一気通貫で解説する。
1-2. Vol1-2からの架橋
本記事は IAM入門シリーズの第3弾として、前2作の知識を前提にしつつ「運用フェーズ」に焦点を当てる。
Vol1「IAMポリシー設計入門」からの接続
Vol1 では、IAMの評価ロジック (Explicit Deny → SCP → Permission Boundary → Identity-based → Resource-based) と最小権限の特定法を学んだ。設計時の「どう作るか」をカバーしている。
本記事の棚卸し自動化は、Vol1で学んだ「正しいポリシーの形」を維持するための運用ループだ。Access Analyzer Policy Generation は Vol1の最小権限原則を「現実の利用実績」から算出し、常に正しいポリシー状態に近づけるための仕組みだ。
Vol1 → Vol3 の接続:
Vol1: 「ポリシーをどう設計するか」 (静的設計フェーズ)
Vol3: 「設計した最小権限をどう維持するか」 (動的運用フェーズ)
具体的な対応:
Vol1で学ぶ: 最小権限特定法 (Access Advisor + 手動絞り込み)
Vol3で学ぶ: Policy Generation による自動最小権限算出 + 継続削減サイクル
Vol2「複数アカウント時代のIAM設計」からの接続
Vol2 では、Organizations × SCP × Permission Boundary × IAM Identity Center による組織横断のアクセス管理を学んだ。
複数アカウント環境では、棚卸し対象に Permission Set が加わる。Identity Center の Permission Set を棚卸しするには、単一アカウントの IAM Role 棚卸しとは異なるアプローチが必要だ。本記事§4では、マルチアカウント環境での Last Accessed 活用も扱う。
Vol2 → Vol3 の接続:
Vol2: 「Organizations規模でSCP/Permission Boundaryを設計する」
Vol3: 「設計したPermission Setの過剰権限を継続的に検出・修正する」
棚卸し対象の拡張:
単一アカウント: IAM User / Role / Policy
マルチアカウント: + Permission Set (Identity Center)
1-3. 本記事のゴールと対象読者
- (a) 棚卸し対象の体系化: User / Role / Policy / Permission Set の4分類とリスク特性を理解し、棚卸し優先順位を判断できる
- (b) Access Analyzer Policy Generation 実践: CloudTrail 起点で「実際に使った権限のみ」を含む最小権限ポリシーを自動生成し、本番適用できる
- (c) 未使用権限の検出と削除: IAM Last Accessed × Access Advisor で未使用権限を特定し、安全に削除するサイクルを回せる
- (d) CloudTrail によるIAM活動可視化: SQL クエリで「誰が何のAPIを呼んだか」を分析し、異常権限利用を早期発見できる
- (e) 棚卸し自動化サイクル構築: EventBridge + Lambda + Terraform IaC化で定期棚卸しサイクルを自動で回せる
前提: IAMポリシー設計入門 (Vol1) と 複数アカウント時代のIAM設計 (Vol2) を読んでいると理解が深まります。
次のシリーズ: Vol4 では Organizations 環境の監査運用と証跡管理を扱う予定です。
対象読者
本記事は以下のエンジニアを対象とする:
| 対象 | 状況 |
|---|---|
| IAM設計経験者 | Vol1-2を読んだ / IAM権限設計の基礎がある |
| 棚卸し未自動化 | まだスプレッドシートや手動確認で棚卸しを実施している |
| 自動化着手前 | EventBridge + Lambda を組み合わせたいが全体像が見えていない |
| マルチアカウント環境 | Organizations + Identity Center を利用しており Permission Set の棚卸しにも対応したい |
本記事を通じて、IAM権限の「作って終わり」から「作って、計測して、絞り込む」継続運用サイクルへの移行を実現する。Vol1-2と組み合わせることで、IAMのライフサイクル全体——設計・ガードレール・運用——を一気通貫で習得できる。
2. 棚卸し対象の俯瞰 — User/Role/Policy/Permission Set 4分類とリスク特性
IAM権限の棚卸しを「何から始めるか」に迷う原因の多くは、棚卸し対象が混在していることです。IAMリソースには種類ごとに 異なるリスク特性と棚卸しポイント があります。4分類を整理してからツールと手順を選ぶことで、棚卸しの効率が大幅に上がります。
2-1. 棚卸し対象4分類の全体像
IAM権限の棚卸し対象は以下の4分類に整理できます。
| 分類 | 概要 | 認証情報の種類 |
|---|---|---|
| IAM User | 人間またはアプリケーションに紐付く長期認証情報を持つエンティティ | パスワード / アクセスキー (永続) |
| IAM Role | 一時認証情報 (AssumeRole) ベースの権限単位。EC2 / Lambda / クロスアカウント等で利用 | STS 一時トークン (有効期限あり) |
| IAM Policy | 権限を定義するJSONドキュメント。エンティティにアタッチして使用 | N/A (権限定義そのもの) |
| Permission Set | IAM Identity Center が管理するアクセス配信テンプレート。複数アカウントに一括展開 | Identity Center ポータル経由の一時認証 |
この4分類はリスク特性が根本的に異なるため、棚卸し手順とツールをそれぞれ使い分ける必要があります。
4分類を意識する理由
IAM User は認証情報 (アクセスキー) が永続的であるため、未使用でも認証情報が存在し続けます。退職者のアカウント残存や、CI/CDに埋め込まれた古いアクセスキーは、IAM Userのリスクの代表例です。現在の AWS ベストプラクティスは「人間ユーザーへの IAM User 発行を廃止し、IAM Identity Center 経由の一時認証に移行する」です。IAM User が残っている環境では、その存在理由を明確化することが棚卸しの第一歩になります。
IAM Role は一時認証情報を使うため、IAM Userより安全ですが、権限が肥大化しやすいという問題があります。特に 信頼ポリシー (Trust Policy) の Principal が広すぎる場合 (例: "AWS": "*")、意図しないアカウントやサービスにロールを引き受けられるリスクがあります。また、Lambda 関数や EC2 インスタンスに付与された実行ロールは、アプリケーションの要件が変わっても権限がそのまま残るケースが多く、定期的な Last Accessed チェックが有効です。
IAM Policy は単体では害がなく、エンティティにアタッチされて初めてリスクになります。しかし未使用のポリシーが増えると管理が困難になり、誤アタッチのリスクが上がります。Action: "*" や Resource: "*" を含むポリシーは棚卸し最優先対象です。カスタマー管理ポリシーは AWS Managed Policy と異なり、バージョン管理や変更履歴の追跡を自前で行う必要があるため、定期的な内容レビューが重要です。
Permission Set はマルチアカウント環境特有の棚卸し対象です。1つの Permission Set が複数アカウントに展開されているため、見直しの影響範囲が広くなります。実際に使用されているアカウントとアサイン数を把握することが重要です。Identity Center の Account Assignment レポートを使うと、「どの Permission Set がどのアカウントのどのユーザー/グループに割り当てられているか」を一覧化できます。
棚卸しの前提: IAM Credential Report を取得する
4分類の棚卸しを始める前に、まず現状把握のベースラインを作ります。IAM Credential Report は、アカウント内の全 IAM User の認証情報状態を CSV 形式で出力するレポートです。
# Credential Report の生成をリクエスト (初回は生成に数秒かかる)
aws iam generate-credential-report
# 生成完了後にダウンロード (Base64 エンコードされた CSV)
aws iam get-credential-report --query 'Content' --output text | base64 --decode > credential-report.csv
Credential Report に含まれる主な列:
| 列名 | 内容 |
|---|---|
user | IAM ユーザー名 (root ユーザーは <root_account>) |
password_enabled | コンソールパスワードの有効/無効 |
password_last_used | コンソールパスワードの最終使用日時 |
mfa_active | MFA の有効/無効 |
access_key_1_active | アクセスキー1の有効/無効 |
access_key_1_last_used_date | アクセスキー1の最終使用日時 |
access_key_1_last_rotated | アクセスキー1の作成/ローテート日時 |
このレポートを pandas や awk で処理し、「90日以上未使用のユーザー」「MFA未設定のコンソールユーザー」「180日以上未ローテートのアクセスキー」を抽出するスクリプトを §7 で実装します。
IAM Role の Trust Policy を一括確認する
IAM Role の棚卸しでは Permission Policy (何ができるか) だけでなく、Trust Policy (誰がこのロールを引き受けられるか) の確認が欠かせません。以下のワンライナーで全 Role の Trust Policy に含まれる Principal を一覧化できます。
# 全ロールの Trust Policy から Principal を抽出
aws iam list-roles --query 'Roles[*].[RoleName,AssumeRolePolicyDocument]'--output json | python3 -c "
import sys, json
roles = json.load(sys.stdin)
for name, doc in roles:
stmts = doc.get('Statement', [])
for s in stmts:
p = s.get('Principal', {})
print(f'{name}: {p}')
" | grep -v 'amazonaws.com'
grep -v 'amazonaws.com' で AWS サービスプリンシパル (lambda / ec2 / etc.) を除外し、外部アカウントや広すぎる Principal ("*") を絞り込みます。出力に "*" や想定外の AWS アカウント ID が現れた場合は高優先度で調査します。
棚卸し作業の推奨サイクル
| 作業 | 推奨頻度 | 担当 |
|---|---|---|
| IAM Credential Report 取得・分析 | 月次 | 運用担当者 / 自動化スクリプト |
| Last Accessed / Access Advisor チェック | 四半期 | セキュリティ担当者 |
| Trust Policy の外部 Principal 確認 | 月次 | セキュリティ担当者 |
| Permission Set アサイン整理 | 四半期 | Identity Center 管理者 |
| ワイルドカード権限ポリシーの見直し | 半年次 | セキュリティ担当者 |
継続的な棚卸しを定着させるために、§7 ではこれらの確認を Lambda + EventBridge で自動スケジュール実行するアーキテクチャを実装します。
2-2. 4分類 × リスク特性マトリクス
各分類のリスク特性・棚卸しポイント・推奨ツールを整理します。
| 分類 | 主なリスク | 棚卸しポイント | 推奨ツール |
|---|---|---|---|
| IAM User | 認証情報漏洩・長期アクセスキー未ローテート | 未使用ユーザー / アクセスキー90日以上未使用 | Access Analyzer 未使用アクセス / IAM Credential Report |
| IAM Role | 権限肥大化・Trust Policy の Principal 過剰 | 未使用ロール / Last Used が90日以上前 | Last Accessed / Access Advisor |
| IAM Policy | ワイルドカード権限・不要な Managed Policy 残存 | 未使用ポリシー / *:* アクション含む | Access Analyzer Policy Validator / Policy Generation |
| Permission Set | マルチアカウント横断の過剰権限 | 未使用 Permission Set / 不要な Account Assignment | Identity Center Activity Log / Last Accessed |
IAM User のリスク詳細
IAM User は「作って放置」になりやすいリソースです。IAM Credential Report (aws iam generate-credential-report) を定期的に取得することで、以下を一括確認できます。
password_last_used: コンソールパスワードの最終使用日access_key_1_last_used_date/access_key_2_last_used_date: アクセスキーの最終使用日mfa_active: MFA の有効/無効状態
Credential Report はアカウント単位での全ユーザー一覧を CSV 形式で出力します。このデータを定期的にスクリプトで解析し、90日以上未使用のユーザーをアラートする仕組みを §7 で実装します。
IAM Role の Trust Policy リスク
IAM Role の棚卸しで見落とされやすいのが Trust Policy (信頼ポリシー) です。Trust Policy は「どのエンティティがこのロールを AssumeRole できるか」を定義します。
危険なパターン例:
{
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "sts:AssumeRole"
}
]
}
この Trust Policy は全 AWS アカウント・全エンティティに AssumeRole を許可します。たとえ Identity-based Policy が厳しく絞られていても、Trust Policy が過剰な場合は外部からのロール引き受けが可能になります。Trust Policy の棚卸しは Access Analyzer の外部アクセス検出機能で自動化できます。
2-3. 4分類の棚卸し診断コマンド早見表
棚卸しを始める際に、まず現状の規模感を把握するために使う CLI コマンドをまとめます。
# === IAM User 棚卸し ===
# アカウント内の全 IAM User 一覧 (ユーザー数確認)
aws iam list-users --query 'Users[*].[UserName,CreateDate]' --output table
# アクセスキーが有効な User を抽出
aws iam list-users --query 'Users[*].UserName' --output text | tr '\t' '\n' | while read user; do
aws iam list-access-keys --user-name "$user" --query "AccessKeyMetadata[?Status=='Active'].[UserName,AccessKeyId,CreateDate]" --output text
done
# === IAM Role 棚卸し ===
# 全 IAM Role 一覧 (件数とロール名)
aws iam list-roles --query 'Roles[*].[RoleName,CreateDate]' --output table | wc -l
# Last Accessed 情報の取得 (ロール単位)
# (job-id を取得してから get-service-last-accessed-details を呼ぶ)
aws iam generate-service-last-accessed-details --arn <ROLE_ARN>
# === IAM Policy 棚卸し ===
# カスタマー管理ポリシーの一覧 (アタッチ数付き)
aws iam list-policies --scope Local --query 'Policies[*].[PolicyName,AttachmentCount,UpdateDate]' --output table
# アタッチ数が 0 の未使用ポリシー
aws iam list-policies --scope Local --query 'Policies[?AttachmentCount==\`0\`].[PolicyName,UpdateDate]' --output table
これらのコマンドはアカウント単位でのスポット確認に有効です。複数アカウントに横断して実行する場合は、AWS Organizations と AWS CLI プロファイルを組み合わせてアカウントをループ処理します。§7 で自動化スクリプトとして実装します。
2-4. 棚卸し優先度の判断フレームワーク
棚卸し対象が多い場合、全件を同等に扱うのは非効率です。「リスク × 影響範囲」 の2軸で優先度を判定します。
高優先度 (即時対応)
以下に該当するリソースは、発見次第即時に対応します。
| 条件 | 対象分類 | 理由 |
|---|---|---|
| アクセスキーが90日以上未使用 | IAM User | 長期間使用されない認証情報は攻撃者に悪用されるリスクが高い |
| Last Used が90日以上前 | IAM Role | 不要なロールが存在することで攻撃対象となる可能性がある |
ワイルドカード権限 (Action: "*") を含む | IAM Policy | 最小権限の原則に直接違反。意図しない操作が可能な状態 |
| 本番・管理アカウントへの過剰 Permission Set | Permission Set | 影響範囲が最大。本番データへのアクセスリスクが高い |
Trust Policy に Principal: "*" | IAM Role | 外部エンティティからのロール引き受けが可能な状態 |
中優先度 (定期対応: 月次 or 四半期)
| 条件 | 対象分類 | 対応例 |
|---|---|---|
| 未使用の Customer Managed Policy (30日以上アタッチなし) | IAM Policy | デタッチ後に削除候補としてマーク |
| Inline Policy の存在 | IAM User / Role | Customer Managed Policy に移行して管理を一元化 |
| 開発/Sandbox アカウントの権限肥大化 | IAM Role / Permission Set | 影響範囲は限定的だが放置すると本番構成のベースラインになりやすい |
| MFA 未設定の IAM User | IAM User | コンソールアクセスがある場合は必須化 |
低優先度 (年次見直し)
| 条件 | 対応 |
|---|---|
| AWS Managed Policy のバージョン確認 | AWS 更新に追従しているか確認。古いバージョンは機能が不足または過剰になる場合がある |
| 組織外 Principal への Resource-based Policy | S3 バケットポリシー等で外部アカウントに付与した権限を年次で棚卸し |
| Permission Set のアサイン先アカウント数 | 組織規模が変化した際に不要になったアサインを整理 |

- 基準1: 未使用期間。Last Accessed / Credential Report で90日以上未使用のリソースは即時棚卸し対象。期間が長いほどリスクが高い。
- 基準2: 権限の広さ。ワイルドカード (
*:*) を含む、または AdministratorAccess 相当のポリシーが付与されたリソースは最優先で見直す。 - 基準3: 影響範囲。本番アカウントや管理アカウントのリソースは開発/Sandbox より優先する。Permission Set は複数アカウントに一括影響するため特に注意。
3. IAM Access Analyzer Policy Generation 実践
「最小権限が大事だと分かっていても、実際にどの権限を削れるか判断できない」——これが実務でよく聞く声です。IAM Access Analyzer の Policy Generation は、CloudTrail に記録された実際のAPIコール履歴を分析し、「実際に使った権限のみ」を含むIAMポリシーを自動生成する機能です。
3-1. Policy Generation の仕組みと前提条件
仕組みの概要
Policy Generation は以下の流れで動作します。
- CloudTrail ログの収集: 指定期間(最大90日)のAPIコール履歴をスキャン
- 対象プリンシパルの特定: 指定した IAM User または IAM Role が実際に呼び出したAPIを抽出
- ポリシー草案の生成: 実際に使われたアクションのみを含む IAM ポリシー JSON を出力
- 手動補完の提示: Resource ARN が特定できない箇所は
"Resource": "*"としてフラグ付け
Policy Generation は「使った権限の草案」を生成するだけであり、最終的な絞り込みと検証は人間が行う必要があります。草案をそのまま適用するのではなく、3ステップのループで精度を高めていきます。
前提条件の確認
| 前提条件 | 詳細 |
|---|---|
| CloudTrail の有効化 | S3への証跡記録または CloudTrail Lake が有効であること |
| 分析期間 | 最大90日(デフォルト)。90日より古いログは対象外 |
| 対象リソース | IAM User または IAM Role(グループは直接指定不可) |
| 必要な権限 | 実行者に access-analyzer:StartPolicyGeneration 等の権限が必要 |
| リソースARN | CloudTrail がリソースレベルの ARN を記録しない場合は "Resource": "*" になる |
Access Analyzer の有効化
# 有効化済みの Analyzer を一覧表示
aws accessanalyzer list-analyzers \
--query 'analyzers[*].{name:name,type:type,status:status}'
# 未作成の場合は新規作成(アカウントレベル)
aws accessanalyzer create-analyzer \
--analyzer-name "account-analyzer" \
--type ACCOUNT \
--region ap-northeast-1
Organizations を使っている場合は --type ORGANIZATION で組織全体のアナライザーを作成できます。ただし組織レベルのアナライザーは管理アカウントまたは委任管理者アカウントでのみ作成可能です。
3-2. 3ステップ実践ループ
Policy Generation を効果的に使うには「生成 → 検証 → 適用」の3ステップを繰り返すことが重要です。

ステップ1: Policy Generation を開始する
コンソールから操作する場合は、IAM コンソール → アクセス管理 → Access Analyzer → ポリシーの生成 → 新しいポリシーを生成 と進みます。
CLIからの操作:
aws accessanalyzer start-policy-generation \
--policy-generation-details '{
"principalArn": "arn:aws:iam::123456789012:role/MyAppRole"
}' \
--cloud-trail-details '{
"trails": [
{
"cloudTrailArn": "arn:aws:cloudtrail:ap-northeast-1:123456789012:trail/MyTrail",
"regions": ["ap-northeast-1"],
"allRegions": false
}
],
"accessRole": "arn:aws:iam::123456789012:role/AccessAnalyzerServiceRole",
"startTime": "2026-02-07T00:00:00Z",
"endTime": "2026-05-07T00:00:00Z"
}'
accessRole に指定するロールは、Access Analyzer が CloudTrail の S3 バケットにアクセスするためのロールです。
生成ジョブの状態確認:
aws accessanalyzer list-policy-generations \
--principal-arn "arn:aws:iam::123456789012:role/MyAppRole" \
--query 'policyGenerations[*].{jobId:jobId,status:status,startedOn:startedOn}'
生成には数分〜数十分かかります。status が SUCCEEDED になったら次のステップに進みます。
ステップ2: 生成されたポリシーを検証する
生成結果の取得:
aws accessanalyzer get-generated-policy \
--job-id "<生成ジョブID>" \
--query 'generatedPolicyResult.generatedPolicies[0].policy' \
--output text | python3 -m json.tool
生成結果の例(S3 と CloudWatch Logs を使うロールの場合):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "*"
}
]
}
検証すべき3つのポイント:
- Resource ARN の補完:
"Resource": "*"をより具体的な ARN に絞り込む
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
]
}
過剰なアクションの削除: 分析期間中にテストで使ったが、本番では不要なアクションがないか確認する
不足しているアクションの追加: 90日間のログに含まれない低頻度パス(月次バッチ等)の権限が抜けていないか確認する
iam:SimulatePrincipalPolicy で動作検証:
aws iam simulate-principal-policy \
--policy-source-arn "arn:aws:iam::123456789012:role/MyAppRole" \
--action-names "s3:GetObject" "s3:PutObject" "s3:ListBucket" \
--resource-arns "arn:aws:s3:::my-app-bucket/*"
EvalDecision: allowed が返れば正常です。implicitDeny が返る場合はポリシーに不足があることを意味します。
ステップ3: テスト環境で検証してから本番適用
Terraform での実装例:
resource "aws_iam_policy" "my_app_minimal" {
name = "MyAppMinimalPolicy"
description = "Minimal policy derived from Access Analyzer Policy Generation"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "S3Access"
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
]
},
{
Sid= "CloudWatchLogs"
Effect= "Allow"
Action= ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/my-app:*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "my_app_minimal" {
role = aws_iam_role.my_app.name
policy_arn = aws_iam_policy.my_app_minimal.arn
}
本番適用前のチェックリスト:
- [ ] テスト環境で全ユースケース(正常系・エラー系・バッチ処理等)が動作すること
- [ ]
iam:SimulatePrincipalPolicyで必要アクションが全てallowedになること - [ ] 既存の広範囲なポリシーを先に外してから新ポリシーを適用すること
- [ ] Terraform の
planで変更内容を必ずレビューすること
3-3. 運用上の注意点と継続的な権限最適化
継続的な権限最適化サイクル:
初期導入
↓
(1) Policy Generation で現在の使用状況を分析(90日分)
↓
(2) Resource ARN 補完 + SimulatePrincipalPolicy 検証
↓
(3) テスト環境で全ユースケース検証
↓
(4) 本番適用(Terraform)
↓
(5) 四半期ごとに (1) に戻り権限ドリフトを検出
注意点4選:
| # | 注意点 | 対処法 |
|---|---|---|
| 1 | 90日間のログに含まれない権限は生成されない | 本番適用前に全機能パスを実行してからログを分析する |
| 2 | "Resource": "*" のままにしない | 必ず具体的な ARN に絞り込む(最小権限の目的が失われる) |
| 3 | 複数ポリシーが存在する場合は合算で生成される | 不要なポリシーを先に整理してから Policy Generation を実行する |
| 4 | 定期的な再生成が必要 | 四半期ごとに再実行し、不要になった権限を削除する |
Organizations 環境での考慮事項:
組織全体のアナライザーを使う場合、クロスアカウントアクセスのパターンも分析対象になります。
# 全リージョンの CloudTrail ログを対象にした Policy Generation
aws accessanalyzer start-policy-generation \
--policy-generation-details '{
"principalArn": "arn:aws:iam::111222333444:role/CrossAccountRole"
}' \
--cloud-trail-details '{
"trails": [
{
"cloudTrailArn": "arn:aws:cloudtrail:ap-northeast-1:111222333444:trail/OrgTrail",
"regions": ["ap-northeast-1"],
"allRegions": true
}
],
"accessRole": "arn:aws:iam::111222333444:role/AccessAnalyzerServiceRole",
"startTime": "2026-02-07T00:00:00Z",
"endTime": "2026-05-07T00:00:00Z"
}'
"allRegions": true を指定することで、全リージョンのCloudTrailログを分析対象にできます。マルチリージョン構成のアプリケーションに対して特に有効です。
よくあるトラブルと対処法
Policy Generation を実際に運用する際によく遭遇するトラブルパターンと対処法をまとめます。
トラブル1: 生成ジョブが FAILED になる
原因として最も多いのは、accessRole に設定したロールが CloudTrail の S3 バケットへのアクセス権限を持っていないケースです。
# accessRole に必要な権限を確認
aws iam simulate-principal-policy \
--policy-source-arn "arn:aws:iam::123456789012:role/AccessAnalyzerServiceRole" \
--action-names "s3:GetObject" "s3:ListBucket" \
--resource-arns "arn:aws:s3:::my-cloudtrail-bucket/*"
対処法: AccessAnalyzerServiceRole に CloudTrail バケットへの読み取り権限を持つインラインポリシーを追加します。
トラブル2: 生成されたポリシーに "Resource": "*" が多すぎる
CloudTrail がリソースレベルの ARN を記録しないサービスや、複数のリソースにまたがるアクションで発生します。CloudTrail のイベント履歴を直接確認し、実際にアクセスしたリソース ARN を特定してください。
# CloudTrail イベントからリソース ARN を特定
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=my-app-role \
--start-time "2026-02-07T00:00:00Z" \
--end-time "2026-05-07T00:00:00Z" \
--query 'Events[*].{EventName:EventName,Resources:Resources}' \
--output json | python3 -m json.tool
トラブル3: 適用後にアプリケーションが動かない
Policy Generation は「過去に使った権限」を生成するため、「まだ使っていないが本番では必要な権限」が含まれません。アプリケーションの全機能パス(正常系・エラーハンドリング・バッチ処理・メンテナンス操作)を事前にリストアップし、分析期間中にすべて実行してから Policy Generation を実行することが根本的な対処法です。
Policy Generation と §4 の連携
§4 で扱う IAM Last Accessed(アクセスアドバイザー) と組み合わせることで、より精度の高い最小権限設計が可能になります。
- Policy Generation → 「実際に使った権限」を起点に最小権限ポリシーを生成
- Last Accessed → 生成したポリシー適用後も「使われていない権限」がないか継続監視
この2つのサイクルを組み合わせることで、初期導入後も権限ドリフトを継続的に検出・修正するループが完成します。
- 鉄則1: CloudTrail 分析期間は最大90日。90日間使わなかった権限は生成結果に含まれない。月次バッチ・四半期処理・低頻度のオペレーション手順など、全ユースケースを事前に洗い出し、本番適用前に実行してからログを分析する。
- 鉄則2:
"Resource": "*"を必ず手動補完する。CloudTrail はリソースレベルの ARN を常に記録しない。生成結果の"Resource": "*"をそのままにすると最小権限の目的が果たせない。iam:SimulatePrincipalPolicyで ARN を絞った状態で検証してから適用する。 - 鉄則3: テスト環境での全ユースケース検証を必ず実施する。生成されたポリシーが既存の権限と矛盾しないか、正常系・エラー系・バッチ処理すべてのパスで動作確認してから本番適用する。Terraform の
planでレビューし、段階的にロールアウトする。
4. IAM Last Accessed × Access Advisor 実践 — 未使用権限の検出と削除サイクル
IAM ポリシーは付与した時点では適切でも、時間の経過とともに「使われなくなった権限」が蓄積する。Last Accessed (アクセスアドバイザー) と IAM Access Analyzer の未使用アクセス検出 を組み合わせることで、実際のログに基づいた客観的な棚卸しが可能になる。本章では検出手順・削除サイクルの設計・自動化パターンを解説する。
AWS における権限棚卸しは「一度やれば完了」ではない。アプリケーションの変更、人員異動、新機能の追加など、権限の使用状況は常に変化する。月次または四半期ごとの定期棚卸し を組織のルーティンとして確立することが、長期的な最小権限の維持につながる。
4-1. Last Accessed と Access Analyzer 未使用検出の違い
コンソール上の「アクセスアドバイザー」タブと、IAM Access Analyzer の「未使用アクセス」検出は、それぞれ異なる視点から未使用権限を見つける。
| 比較項目 | アクセスアドバイザー (Last Accessed) | Access Analyzer 未使用検出 |
|---|---|---|
| 対象エンティティ | IAM User / Role / Group / Policy | IAM Role / IAM User (パスワード / アクセスキー) / 権限 |
| 情報の粒度 | サービスレベル (例: S3 に最後にアクセスした日時) | サービスレベル + 未使用のアクセスキー・パスワードも含む |
| 確認場所 | IAM コンソール → エンティティ詳細 → アクセスアドバイザー | Access Analyzer コンソール → [所見] |
| CLI コマンド | generate-service-last-accessed-details | list-findings (findingType でフィルタ) |
| 主な用途 | 「使っていないサービス権限を削除」するための初手調査 | 未使用の権限・アクセスキー・パスワードをまとめて検出 |
| 保持期間 | 最大 400 日 | アナライザーが有効な期間中継続 |
使い分けの指針:
- まず アクセスアドバイザー でサービスレベルの棚卸しを行う。
- Access Analyzer 未使用検出 で、アクセスアドバイザーでは見つけにくい未使用アクセスキーや未使用ロール全体を把握する。
- 2 つを組み合わせることで、権限の棚卸しの網羅性が大幅に向上する。
未使用期間の閾値設定
AWS は 90 日を推奨閾値としているが、組織の運用特性に合わせて調整する。
| 未使用期間 | 対応方針 |
|---|---|
| 90〜180日 | 監視継続。四半期バッチの可能性あり。チームに確認 |
| 180〜365日 | 削除候補としてリストアップ。Deny で一時無効化して様子見 |
| 365日以上 | 積極的に削除。年次バッチでも使っていない可能性が高い |
「四半期に 1 回しか使わないバッチ処理」や「障害時のみ使うデバッグ用権限」は 90 日の閾値でも引っかかるため、チームへのヒアリングと組み合わせて判断する。
4-2. 未使用権限の検出手順
コンソール操作 — アクセスアドバイザーの確認
IAM コンソール → [ロール] → 対象ロールを選択
→ [アクセスアドバイザー] タブ
→ 各サービスの「最終アクセス」列を確認
確認ポイント:
- 「最終アクセス」列が 90日以上前 または 「—」(未使用) のサービスは削除候補。
- ソート機能を使って「最も古い最終アクセス」順に並べ替えると効率的。
- サービス名の横の展開ボタンをクリックすると、そのサービス内でどの API アクションが使用されたかをより詳細に確認できる (API レベル情報はリージョンとサービスによっては利用不可)。
CLI での一括確認 — generate-service-last-accessed-details
ロールが多い環境では CLI で一括取得して集計する。
# Step 1: 非同期ジョブを開始 (ARN を指定)
JOB_ID=$(aws iam generate-service-last-accessed-details--arn "arn:aws:iam::123456789012:role/MyAppRole"--query 'JobId'--output text)
# Step 2: ジョブ完了を待機 (STATUS が COMPLETED になるまで)
aws iam get-service-last-accessed-details--job-id "$JOB_ID"--query 'JobStatus'
# Step 3: 未使用サービス (TotalAuthenticatedEntities=0) を抽出
aws iam get-service-last-accessed-details--job-id "$JOB_ID"--query 'ServicesLastAccessed[?TotalAuthenticatedEntities==`0`].ServiceName'--output text
TotalAuthenticatedEntities が 0 のサービスは「分析期間内に一度も使われていない」ことを意味する。LastAuthenticatedRegion および LastAuthenticated フィールドも組み合わせて確認すると、使用された最後の日時とリージョンが把握できる。
CLI での一括確認 — Access Analyzer 未使用アクセス検出
# Access Analyzer で未使用アクセスの所見を一覧取得
aws accessanalyzer list-findings--analyzer-name "account-analyzer"--filter '{"findingType": {"eq": ["UnusedIAMRole", "UnusedIAMUserPassword", "UnusedIAMUserAccessKey", "UnusedPermission"]}}'--query 'findings[].{Id:id,Type:findingType,Resource:resource,UpdatedAt:updatedAt}'--output table
findingType の種類:
| findingType | 意味 |
|---|---|
UnusedIAMRole | 90日以上使用されていない IAM ロール |
UnusedIAMUserPassword | 90日以上使用されていない IAM ユーザーのパスワード |
UnusedIAMUserAccessKey | 90日以上使用されていないアクセスキー |
UnusedPermission | 90日以上使用されていない権限 (アクション単位) |
多数ロールの一括スキャン
大規模環境では複数ロールを一括でスキャンするスクリプトが有効だ。
#!/usr/bin/env bash
# 全ロールのアクセスアドバイザー結果を取得して未使用サービスを集計
OUTPUT_FILE="unused_services_$(date +%Y%m%d).csv"
echo "RoleName,UnusedService,LastAccessed" > "$OUTPUT_FILE"
for ROLE_ARN in $(aws iam list-roles --query 'Roles[?!contains(RoleName, `AWSReservedSSO`)].Arn' --output text); do
ROLE_NAME=$(basename "$ROLE_ARN")
JOB_ID=$(aws iam generate-service-last-accessed-details --arn "$ROLE_ARN" --query 'JobId' --output text 2>/dev/null) || continue
sleep 3 # 非同期ジョブの完了を待機
aws iam get-service-last-accessed-details --job-id "$JOB_ID" --query 'ServicesLastAccessed[?TotalAuthenticatedEntities==`0`].[ServiceName,LastAuthenticated]' --output text | while IFS=$'\t' read SVC LAST; do
echo "$ROLE_NAME,$SVC,${LAST:-never}" >> "$OUTPUT_FILE"
done
done
echo "Saved: $OUTPUT_FILE"
出力された CSV をもとに「どのロールがどのサービス権限を使っていないか」を一覧化し、削除候補リストとして活用する。AWSReservedSSO プレフィックスを除外しているのは、Identity Center が自動生成するロールを誤って棚卸し対象にしないためだ。
4-3. 削除サイクルの設計
未使用権限を発見してもいきなり削除するのは危険だ。安全な削除サイクルを設計することで、アプリケーション障害のリスクを最小化しながら権限を整理できる。
推奨削除サイクル (5ステップ):
- 棚卸しリスト作成: CLI/コンソールで未使用権限を一覧化 (月次推奨)
- 影響評価: 削除対象の権限が他のロールや Lambda / EC2 に共有されていないか確認
- Deny ステートメントで一時無効化: いきなり削除せず Deny で無効化して 2 週間様子見する
- 本番削除: 2 週間以上問題がなければ権限を削除 (ポリシー更新・Terraform PR)
- 削除記録: 削除した権限・日時・担当者を IaC のコミットメッセージや PR に残す
Terraform での Deny 追加例 (段階的削除)
# Step 1: まず Deny で無効化 (影響確認フェーズ / 2週間)
resource "aws_iam_role_policy" "unused_deny" {
name = "UnusedServicesDeny"
role = aws_iam_role.my_app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyUnusedServices"
Effect = "Deny"
Action = [
"ec2:*",
"rds:*"
]
Resource = "*"
}
]
})
}
# Step 2: 2週間後に問題がなければ以下を実施:
#1. aws_iam_role_policy "unused_deny" リソースを削除
#2. 元の aws_iam_policy から ec2:* / rds:* の Statement も削除
#3. terraform apply → PR を作成・記録
Deny 無効化期間中の確認コマンド — 削除した権限が実際に呼ばれていないかを CloudTrail で確認する:
# 直近14日間に ec2:* または rds:* が対象ロールから呼ばれたか確認
aws cloudtrail lookup-events--lookup-attributes AttributeKey=Username,AttributeValue=MyAppRole--start-time "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)"--query 'Events[?contains(EventName, `ec2`) || contains(EventName, `rds`)].{Event:EventName,Time:EventTime}'--output table
呼び出しが 0 件であれば本削除に進む。1 件でもあれば Deny を解除して権限を残す判断をする。
4-4. mermaid01: Last Accessed × Access Analyzer 統合運用サイクル
アクセスアドバイザーと Access Analyzer を組み合わせた月次棚卸しサイクルのフローを示す。
flowchart TD
A[定期実行<br>毎月1回] --> B[Access Analyzer<br>未使用アクセス検出]
B --> C{未使用リソース<br>あり?}
C -- No --> Z[棚卸し完了<br>記録]
C -- Yes --> D[Last Accessed / Access Advisor<br>詳細確認]
D --> E{90日以上<br>未使用?}
E -- No --> F[監視継続]
E -- Yes --> G[影響評価<br>他ロールとの共有確認]
G --> H[Deny ステートメント追加<br>2週間様子見]
H --> I{問題<br>発生?}
I -- Yes --> J[Deny 解除<br>権限を残す]
I -- No --> K[権限を削除<br>Terraform更新]
K --> L[削除記録<br>PRコメントに残す]
L --> Z
このサイクルの重要なポイントは Deny ステートメントによる段階的削除 だ。本番環境で気づかずに使われていた権限をいきなり削除すると、夜間バッチや低頻度の定期処理がエラーになる可能性がある。Deny で無効化してアラートが出ないことを確認してから本削除することで、安全性を大幅に向上させられる。
4-5. 定期棚卸しの自動化パターン
月次の棚卸しを手動で実施するだけでなく、Lambda を使って検出レポートを自動生成する構成が有効だ。EventBridge Scheduler で月 1 回 Lambda を起動し、全ロールの Last Accessed 情報を S3 にレポートとして保存することで、棚卸し漏れを防げる。
自動化フローの概要:
- EventBridge Scheduler — 毎月 1 日に Lambda をトリガー
- Lambda — 全 IAM ロールに対して
generate-service-last-accessed-detailsを実行し、90日以上未使用のサービスをリストアップ - S3 — JSON レポートを日付付きで保存
- SNS 通知 — 未使用件数が閾値を超えた場合にアラートを送信 (オプション)
Lambda 実装例 (Python):
import boto3
import json
import time
from datetime import datetime, timezone, timedelta
iam = boto3.client('iam')
s3 = boto3.client('s3')
AUDIT_BUCKET = 'my-iam-audit-bucket'
UNUSED_DAYS = 90
def lambda_handler(event, context):
roles = iam.list_roles()['Roles']
unused = []
threshold = datetime.now(timezone.utc) - timedelta(days=UNUSED_DAYS)
for role in roles:
# Identity Center が自動生成するロールはスキップ
if 'AWSReservedSSO' in role['RoleName']:
continue
role_arn = role['Arn']
job_id = iam.generate_service_last_accessed_details(
Arn=role_arn)['JobId']
# 非同期ジョブの完了を待機 (最大 30 秒)
for _ in range(15):
details = iam.get_service_last_accessed_details(JobId=job_id)
if details['JobStatus'] == 'COMPLETED':
break
time.sleep(2)
else:
continue # タイムアウト → スキップ
for svc in details['ServicesLastAccessed']:
last = svc.get('LastAuthenticated')
if last is None or last < threshold:
unused.append({
'role': role['RoleName'],
'service': svc['ServiceName'],
'last_accessed': last.isoformat() if last else 'never'
})
# S3 にレポートを保存
report_key = f"iam-unused-{datetime.now().strftime('%Y%m%d')}.json"
s3.put_object(
Bucket=AUDIT_BUCKET,
Key=report_key,
Body=json.dumps(unused, ensure_ascii=False, indent=2),
ContentType='application/json'
)
return {'report_key': report_key, 'unused_count': len(unused)}
Lambda の IAM ロールには iam:ListRoles、iam:GenerateServiceLastAccessedDetails、iam:GetServiceLastAccessedDetails、s3:PutObject の最小権限を付与する。ロール数が多い環境では list-roles の --max-items および --starting-token でページネーションを処理すること。
- 鉄則1: いきなり削除しない。まず Deny ステートメントで無効化し、2週間様子見してから削除する。急いで削除するとアプリケーション障害につながる。
- 鉄則2: 90日を閾値にする。Last Accessed が90日以上前のサービス権限は積極的に削除候補にする。AWS の推奨とベストプラクティスに準拠。
- 鉄則3: 削除記録を IaC に残す。Terraform のコミットメッセージや PR に「削除した権限・日時・理由」を記録する。後から「なぜ削除したか」を追跡できるようにする。
IAM アクセスアドバイザー 公式ドキュメント → 確認する
5. CloudTrail Lake によるIAM活動分析 — SQLクエリで権限利用状況を可視化する
IAMの権限棚卸しにおいて「誰が実際に何のAPIを呼んだか」を把握することが最小権限設計の出発点となる。§4ではAccess Analyzerを使った未使用権限の検出を解説したが、本章ではより詳細なIAM活動分析が可能なCloudTrail LakeのSQLクエリ手法を紹介する。さらに、2026年5月以降の新規利用制限に備えたAmazon Athenaへの代替手順も併記する。
5-1. CloudTrail Lake の概要と新規利用制限
CloudTrail Lake は CloudTrail イベントを SQL で直接クエリできる分析サービスだ。S3 への証跡エクスポートや別途 Athena テーブルの定義が不要で、マネジメントコンソールまたは CLI からすぐにクエリを実行できる。
主な特徴:
- CloudTrail イベントを最大 7 年間保持(保持期間は設定可能)
- SQL(ANSI SQL 準拠)でネストされたフィールド(
userIdentity.arnなど)を直接クエリ可能 - クエリ結果は S3 に自動保存され、大量データの分析に適している
- マルチリージョン・組織全体のイベントを単一クエリで横断分析できる
⚠️ 重要: 2026年5月31日以降の新規利用制限
2026年5月31日以降、CloudTrail Lake の新規イベントデータストア作成が制限される。
- 既存のイベントデータストアは継続利用可能
- 新規アカウントや新規リージョンでの CloudTrail Lake 利用には Amazon Athena(S3 + CloudTrail 証跡) での代替が推奨される
- 本章では CloudTrail Lake SQL 実例を提供しつつ、Athena での等価クエリを併記する
CloudTrail Lake イベントデータストアの作成(2026/5/31 以前の環境向け):
aws cloudtrail create-event-data-store--name "IAMActivityAnalysis"--retention-period 90--no-multi-region-enabled--no-organization-enabled
既存のイベントデータストアの ARN は以下で確認する。
aws cloudtrail list-event-data-stores--query 'EventDataStores[*].[Name,EventDataStoreArn]'--output table
5-2. SQLクエリ実例3件
クエリ1: 特定ロールが過去30日間に呼んだAPIを一覧化
誰かのロールに「使われていない権限がある」と疑われる場合、まずそのロールが実際に呼んだAPIの全一覧を取得する。
CloudTrail Lake クエリ:
SELECT
eventTime,
eventName,
eventSource,
requestParameters,
errorCode
FROM
<event-data-store-arn>
WHERE
userIdentity.sessionContext.sessionIssuer.arn = 'arn:aws:iam::123456789012:role/MyAppRole'
AND eventTime > DATE_ADD('day', -30, NOW())
ORDER BY
eventTime DESC
LIMIT 100
Athena(S3 + CloudTrail 証跡)での等価クエリ:
SELECT
eventtime,
eventname,
eventsource,
requestparameters,
errorcode
FROM
cloudtrail_logs_123456789012
WHERE
useridentity.sessioncontext.sessionissuer.arn = 'arn:aws:iam::123456789012:role/MyAppRole'
AND eventtime > to_iso8601(current_timestamp - interval '30' day)
ORDER BY
eventtime DESC
LIMIT 100;
Athena クエリでは列名がすべて小文字になる点に注意が必要だ。また DATE_ADD の代わりに current_timestamp - interval '30' day を使う。テーブル名はCloudTrail証跡をS3に保存し、Athena テーブルを作成した際に指定した名前(例: cloudtrail_logs_<account_id>)を使う。
クエリ2: 未使用 Permission Set を検出する
Identity Center の Permission Set が過去90日間で一度も使われていないかを確認する。未使用の Permission Set はセキュリティリスクとなるため、定期的に棚卸しが必要だ。
CloudTrail Lake クエリ:
SELECT
userIdentity.principalId,
userIdentity.arn,
COUNT(*) AS event_count,
MAX(eventTime) AS last_used
FROM
<event-data-store-arn>
WHERE
userIdentity.arn LIKE '%AWSReservedSSO_ReadOnlyAccess_%'
AND eventTime > DATE_ADD('day', -90, NOW())
GROUP BY
userIdentity.principalId,
userIdentity.arn
ORDER BY
last_used ASC
LIKE '%AWSReservedSSO_ReadOnlyAccess_%' の部分を調査したい Permission Set 名に変えることで、任意の Permission Set の利用状況を確認できる。last_used が古いエントリが棚卸し候補だ。
Athena での等価クエリ:
SELECT
useridentity.principalid,
useridentity.arn,
COUNT(*) AS event_count,
MAX(eventtime) AS last_used
FROM
cloudtrail_logs_123456789012
WHERE
useridentity.arn LIKE '%AWSReservedSSO_ReadOnlyAccess_%'
AND eventtime > to_iso8601(current_timestamp - interval '90' day)
GROUP BY
useridentity.principalid,
useridentity.arn
ORDER BY
last_used ASC;
クエリ3: クロスアカウント AssumeRole を検出する
外部アカウントから自アカウントのロールが引き受けられている(クロスアカウント AssumeRole)場合を検出する。意図しない信頼関係が設定されていないか定期的に確認することで、不正アクセスの早期発見につながる。
CloudTrail Lake クエリ:
SELECT
eventTime,
userIdentity.accountId AS caller_account,
requestParameters.roleArn AS assumed_role,
sourceIPAddress
FROM
<event-data-store-arn>
WHERE
eventName = 'AssumeRole'
AND userIdentity.accountId != '123456789012'
AND eventTime > DATE_ADD('day', -30, NOW())
ORDER BY
eventTime DESC
userIdentity.accountId != '123456789012' で自アカウントからの AssumeRole を除外し、外部アカウントからの呼び出しのみを抽出する。caller_account と assumed_role の組み合わせを確認し、想定外の組み合わせがあればアクセス経路の調査が必要だ。
Athena での等価クエリ:
SELECT
eventtime,
useridentity.accountid AS caller_account,
requestparameters,
sourceipaddress
FROM
cloudtrail_logs_123456789012
WHERE
eventname = 'AssumeRole'
AND useridentity.accountid != '123456789012'
AND eventtime > to_iso8601(current_timestamp - interval '30' day)
ORDER BY
eventtime DESC;
5-3. CloudTrail Lake / Athena クエリ実行シーケンス
以下のシーケンス図は、CloudTrail Lake と Athena それぞれでクエリを実行する際のフローを示している。2026年5月以降の新規環境では下半分(Athena フロー)を使う。
sequenceDiagram
autonumber
participant Operator as 運用担当
participant Lake as CloudTrail Lake
participant S3 as S3 (証跡ストア)
participant Athena as Amazon Athena
Operator->>Lake: SQLクエリ送信 (start-query)
Lake-->>Operator: クエリID返却
Operator->>Lake: get-query-results (クエリID)
Lake-->>Operator: 結果返却 (JSONまたはCSV)
Note over Operator,Athena: 2026/5/31以降の新規環境では Athena を使用
Operator->>Athena: SQLクエリ送信 (start-query-execution)
Athena->>S3: CloudTrail証跡ログを参照
S3-->>Athena: ログデータ返却
Athena-->>Operator: クエリ結果 (S3に保存)
Operator->>S3: 結果ファイルを取得 (awscli / コンソール)
CloudTrail Lake はマネジメントコンソールの「CloudTrail → Lake → クエリ」から直接実行できる。CLI の場合は aws cloudtrail start-query コマンドを使い、返却されたクエリIDで aws cloudtrail get-query-results を実行する。
Athena の場合は事前にCloudTrail証跡のS3バケットをデータソースとするテーブルを作成する必要がある。AWS公式ドキュメントの「Athena を使用した CloudTrail ログファイルのクエリ実行」に従い、CREATE EXTERNAL TABLE でテーブルを定義する。
Athena テーブル作成の基本手順:
- CloudTrail コンソールで S3 への証跡を有効化する(既存の証跡がある場合はスキップ)
- Athena コンソールで「クエリエディタ」を開き、AWS が提供するテンプレート SQL でテーブルを作成する
- テーブル作成後、上記の等価クエリを実行できる
なお、Athena クエリの実行結果は S3 に保存されるため、大量データの分析後も結果を参照し続けることが可能だ。クエリのコストはスキャンしたデータ量に比例するため、eventtime によるパーティションフィルタ(WHERE eventtime > ...)を必ず指定して不要なスキャンを抑制する。パーティション列を使うことでスキャン量を数十分の一に抑えられるケースもあり、コスト最適化と分析速度向上の両面で重要な対策だ。
Athena と CloudTrail Lake のどちらを使う場合も、クエリ結果を定期的にエクスポートして棚卸しレポートとして保管することを推奨する。月次の権限棚卸しサイクルに組み込むことで、継続的な最小権限維持が実現できる。
2026年5月31日以降、CloudTrail Lake の新規イベントデータストア作成が制限されます。新規アカウントや新規リージョンでの分析には Amazon Athena + S3 CloudTrail証跡 を使用してください。本章の SQL クエリは Athena 向けに等価変換できます(上記の等価クエリを参照)。既存のイベントデータストアは引き続き利用可能です。
6. 詰まりポイント7選 図解

IAM の権限棚卸しを実践しようとすると、ツールの制約・データの遅延・マルチアカウント特有の制限など、
さまざまな詰まりポイントにぶつかる。7つのパターンを体系化し、それぞれの原因と対処法を整理した。
詰まり①: CloudTrail の記録反映に遅延がある
事象: Access Analyzer の Policy Generation を実行しても直近の API コールが反映されない。
「さっき実行したのに生成されたポリシーにアクションが含まれていない」という状況。
原因: CloudTrail のイベントログ配信はリアルタイムではなく、最大15分の遅延がある。
さらに S3 バケットへのログ配信から Access Analyzer が読み込むまでにも時間がかかる。
対処法:
– Policy Generation は「過去90日分の安定したトレース」に対して実行する。テスト直後にすぐ実行しても反映されない。
– 実行後に aws accessanalyzer get-generated-policy --job-id <ID> でステータスを確認し、SUCCEEDED になってから結果を確認する。
– 新機能のテスト後は1時間以上待ってから Policy Generation を実行するのが安全。
- Policy Generation の入力は「過去90日分の安定したトレース」。直近15分以内の操作は反映されない。
- ジョブ完了後にステータスが
SUCCEEDEDになるまで待ってから結果を取得する。 - 新機能テスト → 1時間以上経過 → Policy Generation 実行、の手順を守る。
詰まり②: Service Last Accessed の更新が遅い
事象: IAM コンソールのアクセスアドバイザー (Last Accessed) が実際の利用状況を反映していない。
「昨日使ったのに90日未使用と表示される」「実際には使っていないのに最終アクセス日が更新されている」。
原因: Last Accessed データは最大4時間の遅延で更新される。
また、一部のサービス (グローバルサービス: IAM / STS 等) は Last Accessed の更新対象外になるものがある。
対処法:
– 当日のアクセス状況をリアルタイムで確認するのには Last Accessed を使わない。CloudTrail を使う。
– 棚卸し判断には「前日以前」のデータを参照する。月次棚卸しなら先月末時点のデータが安全。
– GenerateServiceLastAccessedDetails API でデータ生成をトリガーしてから、GetServiceLastAccessedDetails で取得する。
# Last Accessed 生成をトリガー
JOB_ID=$(aws iam generate-service-last-accessed-details \
--arn "arn:aws:iam::123456789012:role/MyRole" \
--query 'JobId' --output text)
# 完了を待って結果を取得
aws iam get-service-last-accessed-details --job-id "$JOB_ID"
- Last Accessed は最大4時間の遅延がある。当日の棚卸し判断には CloudTrail を使う。
GenerateServiceLastAccessedDetailsでデータ生成をトリガーしてから取得する。- IAM / STS 等グローバルサービスは Last Accessed の対象外になることがある。
詰まり③: Cross-Service 呼び出しで権限が見えない
事象: Lambda が S3 を呼び出しているが、Lambda ロールの Last Accessed に S3 が表示されない。
「S3 の権限を削除したら Lambda が動かなくなったが、棚卸し時は未使用と判定されていた」。
原因: Lambda が S3 を呼び出す際、Lambda サービス自身 (lambda.amazonaws.com) として記録される場合がある。
Lambda の実行ロールが直接 S3 を呼んでいる記録としては残らないため、Last Accessed に S3 が現れない。
対処法: Last Accessed だけでなく CloudTrail で実際の呼び出しを確認する。
# CloudTrail で Lambda 経由の S3 呼び出しを検索
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=s3.amazonaws.com \
--start-time "2024-01-01T00:00:00Z" \
--max-results 100 \
| jq '.Events[] | select(.CloudTrailEvent | fromjson | .userIdentity.invokedBy == "lambda.amazonaws.com")'
- Lambda→S3 等の Cross-Service 呼び出しは Last Accessed に反映されない場合がある。
- CloudTrail で
eventSource=s3.amazonaws.comANDuserIdentity.invokedBy=lambda.amazonaws.comで検索して実績を確認する。 - 棚卸し前に「このロールはサービス経由で使われていないか」を CloudTrail で必ず確認する。
詰まり④: SCP で許可されているのに Access Analyzer が「未使用」と判定する
事象: SCP で許可しているサービスが Access Analyzer の未使用アクセス検出に引っかかる。
「SCP で Allow しているのに未使用と判定されて削除しないでいいか迷う」。
原因: Access Analyzer はアクセス実績 (CloudTrail の呼び出しログ) を見る。
SCP で許可されていても実際に呼び出していなければ「未使用」と判定される。
SCP の設定状況と実際の利用状況は独立して管理されている。
対処法:
– 「SCP で許可 = 実際に使っている」ではない。権限棚卸しには実績ベースの判断が必要。
– SCP で許可しているが実績がないアクションは、Identity-based ポリシーから削除を検討する。
– ただし、緊急時のみ使う権限 (Break Glass など) は実績がなくても残す判断もある。
- Access Analyzer の「未使用」判定はアクセス実績ベース。SCP の設定状況とは無関係。
- 「未使用」でも削除が不適切なケース (緊急時権限・Break Glass 等) は除外リストを作って管理する。
- 未使用権限の削除前に「本当に不要か」をチームレビューしてから実施する。
詰まり⑤: Resource-based Policy のアクションが棚卸し対象から漏れる
事象: S3 バケットポリシーで特定ロールを許可しているが、IAM 側の棚卸しツールが検出しない。
「バケットポリシーで許可しているロールの権限を削除したら S3 にアクセスできなくなった」。
原因: Access Analyzer の未使用アクセス検出と Last Accessed は Identity-based Policy を対象とする。
S3 バケットポリシー・KMS キーポリシー等の Resource-based Policy は別途確認が必要。
対処法: Access Analyzer の外部アクセス検出 (External Access Analyzer) を追加で使う。
# 外部アクセス検出 Analyzer を作成
aws accessanalyzer create-analyzer \
--analyzer-name "external-access-analyzer" \
--type "ACCOUNT"
# 外部公開されているリソースを一覧表示
aws accessanalyzer list-findings \
--analyzer-arn "arn:aws:accessanalyzer:ap-northeast-1:123456789012:analyzer/external-access-analyzer" \
| jq '.findings[] | {resource: .resource, resourceType: .resourceType, principal: .principal}'
- Last Accessed / 未使用アクセス検出は Identity-based Policy のみ対象。Resource-based Policy は別途確認が必要。
- Access Analyzer の外部アクセス検出 (External Access Analyzer) で S3/KMS/SQS 等のリソースポリシーを棚卸しする。
- 棚卸しの対象に「Resource-based Policy」を明示的に含めてプロセスを定義する。
詰まり⑥: Session Policy の権限縮小が棚卸し漏れになる
事象: AssumeRole 時に Session Policy を渡していて実際の権限がさらに絞られているが、
ロールのポリシーを見ても Session Policy による縮小は分からない。
「ロールの Identity-based ポリシーだけを棚卸ししたが、実際の実行時権限より広い判断をしていた」。
原因: Session Policy は一時的なもので IAM コンソールには表示されない。aws iam list-attached-role-policies や aws iam get-role-policy では Session Policy は確認できない。
対処法: CloudTrail で AssumeRole イベントの requestParameters.policy を確認する。
# AssumeRole 時の Session Policy を CloudTrail から確認
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--start-time "2024-01-01T00:00:00Z" \
| jq '.Events[].CloudTrailEvent | fromjson | select(.requestParameters.roleArn | contains("MyRole")) | .requestParameters.policy'
- Session Policy は IAM コンソール・CLI では確認できない。CloudTrail の AssumeRole イベントを確認する。
- CI/CD パイプラインや GitHub Actions の OIDC では Session Policy が埋め込まれていることがある。
- 棚卸し対象のロールが「AssumeRole 経由で使われているか」を先に把握してから Identity-based Policy を評価する。
詰まり⑦: 委任管理アカウントから Permission Set の変更ができない
事象: 委任管理アカウントから Identity Center の Permission Set を変更しようとするとエラーになる。
「委任管理アカウントで棚卸し後に Permission Set を更新しようとしたが失敗する」。
原因: 委任管理の2制限:
1. 委任管理アカウント自身への Account Assignment 配信ができない
2. 委任管理アカウントから管理アカウントの Permission Set を変更できない
棚卸しの確認 (読み取り) は委任管理アカウントで可能だが、変更 (書き込み) は管理アカウントで実行する必要がある。
対処法: 棚卸しフローを「確認 (委任管理アカウント) → 承認 → 変更 (管理アカウント)」に分割する。
# 委任管理アカウントで Permission Set を確認 (読み取りは可能)
aws sso-admin list-permission-sets \
--instance-arn "arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxxx"
# 変更は管理アカウントで実行 (Terraform の場合は provider の assume_role を切り替える)
- Permission Set の確認は委任管理アカウントで可能。変更は管理アカウントで実行する。
- Terraform で Identity Center を管理する場合は、操作種別ごとに provider の
assume_roleを分けて設計する。 - 棚卸しプロセスに「どのアカウントで実行するか」を明記しておく。
7. 棚卸し自動化アーキテクチャ実装 — EventBridge + Lambda + Terraform IaC化
7-1. 自動化アーキテクチャ全体像
手動で月次棚卸しを続けると、担当者の異動や繁忙期によって棚卸しが止まってしまう。
棚卸しサイクルを自動化することで、継続的な権限管理を実現する。

自動化アーキテクチャの構成要素:
| コンポーネント | 役割 |
|---|---|
| EventBridge Schedule | 月次で Lambda を起動 (毎月1日 09:00 JST) |
| Lambda (Python) | Access Analyzer の未使用アクセス検出結果を収集・集計 |
| S3 | 月次レポートを蓄積 (経時変化の追跡) |
| SNS | 未使用リソース検出時に運用チームへ通知 |
重要な設計原則: 検出と削除を分離する。Lambda は未使用リソースを「検出・通知」するのみで、
削除は人間が判断してから手動または IaC で実行する。自動削除は本番障害のリスクがある。
7-2. Lambda 実装例 (Python)
import boto3
import json
from datetime import datetime, timezone
def lambda_handler(event, context):
analyzer = boto3.client('accessanalyzer')
s3 = boto3.client('s3')
sns = boto3.client('sns')
unused_resources = []
# Access Analyzer の未使用アクセス検出結果を収集
paginator = analyzer.get_paginator('list_findings')
for page in paginator.paginate(
analyzerArn=event.get('analyzerArn'),
filter={
'findingType': {
'eq': ['UnusedIAMRole', 'UnusedPermission', 'UnusedIAMUserAccessKey']
}
}
):
for finding in page['findings']:
unused_resources.append({
'type': finding['findingType'],
'resource': finding['resource'],
'createdAt': finding['createdAt'].isoformat(),
'status': finding['status']
})
# 月次レポートを S3 に保存 (経時変化の追跡)
report_date = datetime.now(timezone.utc).strftime('%Y-%m-%d')
report_key = f"iam-audit/{report_date}/unused-access-report.json"
s3.put_object(
Bucket=event.get('reportBucket'),
Key=report_key,
Body=json.dumps(unused_resources, ensure_ascii=False, indent=2),
ContentType='application/json'
)
# 未使用リソースがあれば SNS で通知
if unused_resources:
message = (
f"IAM権限棚卸し: {len(unused_resources)}件の未使用アクセスを検出しました。\n"
f"レポート保存先: s3://{event.get('reportBucket')}/{report_key}\n\n"
f"内訳 (先頭10件):\n" +
"\n".join([f"- [{r['type']}] {r['resource']}" for r in unused_resources[:10]])
)
sns.publish(
TopicArn=event.get('notifyTopicArn'),
Subject=f"IAM棚卸しレポート {report_date}: {len(unused_resources)}件検出",
Message=message
)
return {
'unusedCount': len(unused_resources),
'reportKey': report_key,
'reportDate': report_date
}
7-3. Terraform IaC実装例
# EventBridge Schedule (毎月1日 09:00 JST)
resource "aws_scheduler_schedule" "iam_audit_monthly" {
name = "iam-audit-monthly"
schedule_expression = "cron(0 0 1 * ? *)"
schedule_expression_timezone = "Asia/Tokyo"
target {
arn= aws_lambda_function.iam_audit.arn
role_arn = aws_iam_role.scheduler_role.arn
input = jsonencode({
analyzerArn = aws_accessanalyzer_analyzer.account.arn
reportBucket= aws_s3_bucket.audit_reports.id
notifyTopicArn = aws_sns_topic.iam_audit_notify.arn
})
}
flexible_time_window {
mode = "OFF"
}
}
# Lambda 関数
resource "aws_lambda_function" "iam_audit" {
function_name = "iam-audit-monthly"
role = aws_iam_role.lambda_audit_role.arn
handler = "lambda_function.lambda_handler"
runtime = "python3.12"
timeout = 300
filename= data.archive_file.lambda_zip.output_path
}
# Lambda 実行ロール (最小権限)
resource "aws_iam_role" "lambda_audit_role" {
name = "lambda-iam-audit-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "lambda_audit_permissions" {
name = "IAMAuditPermissions"
role = aws_iam_role.lambda_audit_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"accessanalyzer:ListFindings",
"accessanalyzer:GetAnalyzer",
"iam:GenerateServiceLastAccessedDetails",
"iam:GetServiceLastAccessedDetails",
"s3:PutObject",
"sns:Publish",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "*"
}
]
})
}
# レポート保存用 S3 バケット (バージョニング有効)
resource "aws_s3_bucket" "audit_reports" {
bucket = "iam-audit-reports-${data.aws_caller_identity.current.account_id}"
}
resource "aws_s3_bucket_versioning" "audit_reports" {
bucket = aws_s3_bucket.audit_reports.id
versioning_configuration {
status = "Enabled"
}
}
# 通知用 SNS トピック
resource "aws_sns_topic" "iam_audit_notify" {
name = "iam-audit-monthly-notify"
}
resource "aws_sns_topic_subscription" "iam_audit_email" {
topic_arn = aws_sns_topic.iam_audit_notify.arn
protocol = "email"
endpoint = var.notify_email
}
7-4. 実装後の動作確認
Terraform を適用してインフラが起動したら、以下の手順で動作確認を行う。
EventBridge Schedule のトリガーを手動実行することで、月次実行を待たずに検証できる。
Step 1: Access Analyzer にアナライザーが作成されているか確認
aws accessanalyzer list-analyzers \
--query 'analyzers[*].[name,status,arn]' \
--output table
status が ACTIVE になっていれば正常。CREATING の場合は数分待ってから再確認する。
Step 2: Lambda を手動実行してテスト
# Lambda をテストイベントで手動実行
aws lambda invoke \
--function-name "iam-audit-monthly" \
--payload '{
"analyzerArn": "arn:aws:accessanalyzer:ap-northeast-1:123456789012:analyzer/account-analyzer",
"reportBucket": "iam-audit-reports-123456789012",
"notifyTopicArn": "arn:aws:sns:ap-northeast-1:123456789012:iam-audit-monthly-notify"
}' \
--cli-binary-format raw-in-base64-out \
response.json && cat response.json
unusedCount が返ってくれば Lambda が正常に実行された。
Step 3: S3 にレポートが保存されているか確認
aws s3 ls s3://iam-audit-reports-123456789012/iam-audit/ --recursive
iam-audit/YYYY-MM-DD/unused-access-report.json が作成されていれば S3 保存も正常。
Step 4: SNS 通知が届いているか確認
未使用アクセスが検出されていれば、SNS サブスクリプションのメールアドレスに通知が届く。
未使用アクセスが 0 件の場合は SNS 通知は送信されない (正常動作)。
7-5. 棚卸し自動化設計の3ポイント
- ポイント1: 検出と削除を分離する。Lambda は未使用リソースを「検出・通知」するのみ。削除は人間が判断してから手動または IaC で行う。自動削除は本番障害のリスクがある。
- ポイント2: 結果を S3 に蓄積する。月次レポートを S3 に保存し、経時変化を追跡できるようにする。過去レポートと比較することで「権限が増えているか減っているか」を可視化できる。
- ポイント3: IaC (Terraform) で全体管理する。棚卸し自動化システム自体の権限・構成も Terraform で管理する。「棚卸しシステムの権限が肥大化する」本末転倒を防ぐ。
8. まとめ + Vol4予告 + 落とし穴10選
8-1. 本記事のまとめ
本記事では IAM の権限棚卸し自動化と継続運用について、以下4本柱で解説した。
| 本柱 | 学んだこと |
|---|---|
| ① Policy Generation | CloudTrail の90日トレースを起点に Access Analyzer が最小権限ポリシーを自動生成。Resource ARN の絞り込みまで行うことが重要 |
| ② Last Accessed × Access Advisor | 未使用権限を90日閾値で検出・削除する手順。IAM ロール・ユーザー・Permission Set の棚卸しフロー |
| ③ CloudTrail SQL クエリ | Athena を使って「誰が何の API を呼んだか」を大規模ログから効率的に可視化する方法 |
| ④ EventBridge + Lambda 自動化 | 棚卸しサイクルを月次で自動化する Terraform IaC 実装。検出と削除を分離した安全設計 |
権限棚卸しは「一度やって終わり」ではなく継続的なプロセスだ。
自動化によって人手に依存せず定期実行し、レポートを蓄積することで「権限の経時変化」を追跡できる。
IAM 権限は放置すると肥大化し続ける。意識的に棚卸しサイクルを設計しなければ、
「誰も使っていない権限が大量に残る」状態になり、最小権限原則を掲げていても形骸化してしまう。
本記事の4本柱を組み合わせることで、アカウント規模が拡大しても管理可能な状態を維持できる。
8-2. 月次棚卸しチェックリスト
棚卸しを実行するたびに確認する項目一覧。Lambda による自動化と並行して、人間が行う判断フローとして使う。
棚卸し実行前の確認 (月1回)
- [ ] CloudTrail がすべてのアカウント/リージョンで有効か
- [ ] Access Analyzer のアナライザーが正常に稼働しているか (
aws accessanalyzer list-analyzers) - [ ] 前月のレポートが S3 に保存されているか
- [ ] SNS 通知先メールアドレスが最新か (担当者変更の確認)
棚卸し実行中の判断フロー
- [ ] 未使用アクセス (UnusedIAMRole / UnusedPermission) の一覧を取得
- [ ] 各リソースについて「本当に不要か」をチームで確認 (除外リストと照合)
- [ ] 90日以上未使用のロールに Deny ステートメントを追加 (2週間様子見)
- [ ] 2週間問題なければ権限を削除 (Terraform PR を作成・記録)
- [ ] Resource-based Policy (S3/KMS 等) を External Access Analyzer で別途確認
棚卸し完了後の記録
- [ ] 削除した権限・ロール・アクセスキーを IaC コミットメッセージに記録
- [ ] 今月の未使用リソース件数を前月と比較 (増えていたら設計レビューが必要)
- [ ] 除外リストの内容が古くなっていないか確認 (Break Glass 権限等)
8-3. 落とし穴10選
IAM 権限棚卸し・自動化でよく遭遇する失敗パターン10項目。設計・運用のチェックリストとして活用してほしい。
- CloudTrail を有効化していない — Policy Generation が使えない。Organizations 環境では全アカウントに CloudTrail を必ず有効化する
- Policy Generation で Resource ARN を
*のまま適用する — 生成されたポリシーはあくまで出発点。Resource ARN を実際のリソース ARN に絞り込んでから本番適用する - Last Accessed データが最大4時間遅延することを知らずに即時判断する — 当日の棚卸し判断には CloudTrail を使う
- Cross-Service 呼び出しで権限が Last Accessed に反映されないと気づかない — Lambda→S3 等は CloudTrail で実績確認が必要
- Resource-based Policy (S3 バケットポリシー等) を棚卸し対象から除外する — Access Analyzer の外部アクセス検出を追加で実施する
- Session Policy による権限縮小を考慮せずに「ロールの権限 = 実際の権限」と判断する — CloudTrail の AssumeRole イベントで Session Policy を確認する
- 委任管理アカウントから Permission Set を変更しようとして失敗する — 確認は委任管理アカウント、変更は管理アカウントで実行する
- 棚卸し Lambda に必要以上の権限を与える — 棚卸しシステム自体が過剰権限になる本末転倒を防ぐ。最小権限で設計する
- 自動削除を実装して本番アプリケーションの権限を誤削除する — 検出と削除は分離する。削除は必ず人間の判断を経る
- Access Analyzer の「未使用」判定を信じて緊急時権限まで削除する — Break Glass 等の緊急権限は除外リストで管理する
8-4. 棚卸し継続のための実践ヒント
棚卸しを「続ける」ことが最も難しい。自動化だけでは解決できない、運用面のヒントをまとめた。
チームへの定着方法:
– 月次棚卸し結果をチームの定例会で5分間共有する。「今月は N 件の未使用権限を削除した」という数字を継続的に報告することで、最小権限設計がチームの文化として定着する。
– 削除した権限の Terraform PR をレビューする習慣をつける。権限削除は「何を削ったか」が重要で、チームが把握していないと緊急時に困る。
除外リストの管理:
– Break Glass 権限・監査ログ専用権限・障害対応用権限など、「未使用でも必要な権限」は除外リストで管理する。
– 除外リストは Lambda の環境変数またはパラメーターストア (Systems Manager Parameter Store) に格納し、コードではなく設定として管理する。
– 除外リストも半年に1回は見直し、不要な除外項目を削除する。
スケールアップ時の注意点:
– アカウント数が増えた場合、Lambda を Organizations 全体対応に拡張する。aws organizations list-accounts でアカウント一覧を取得し、各アカウントに対してクロスアカウント AssumeRole で棚卸しを実行する。
– アカウント数が50以上になったら、1つの Lambda で全アカウントを処理するのは非効率。Step Functions や Distributed Map を使った並列実行を検討する。
8-5. IAM入門シリーズ 完走と Vol4予告
- Vol1 IAMポリシー設計入門: 評価ロジック・最小権限特定・アンチパターン演習 → 読む
- Vol2 複数アカウント時代のIAM設計: Organizations × Identity Center × SCP × Permission Boundary → 読む
- Vol3 本記事: 権限棚卸し自動化と継続運用 — Access Analyzer × CloudTrail × Terraform で棚卸しサイクルを自動化
- Vol4 IAM監査運用 (近日公開予定): 組織レベルの監査ログ分析・コンプライアンスレポート自動化 — 複数アカウントの IAM 設定を横断的に可視化する
Vol1〜Vol3 でカバーした「設計 → 多アカウント展開 → 継続棚卸し」の3本柱が揃えば、スケールしても破綻しない IAM 運用基盤が完成する。ぜひシリーズを通して読んでほしい。