IAM Access AnalyzerのカスタムポリシーチェックをCI/CDで活用する際の設計と工夫
こんにちは。サービス開発室の武田です。
IAM Access Analyzerのカスタムポリシーチェック機能を使ったことはあります?DevelopersIOでも紹介されている機能のひとつです。
- IAM Access Analyzerにて自動推論によるカスタムポリシーチェックが追加されました
- IAM Access Analyzerのカスタムポリシーチェックでパブリックアクセスと重要リソースアクセスのチェックが追加されました
これらのAPIを実際にCI/CDパイプラインに組み込んで運用してみたので、そのときに工夫した点も含めて紹介します。
この記事で書くこと
- CheckAccessNotGrantedとCheckNoNewAccessを両方使う理由
- AWS公式のGitHub Actionsではなくboto3で自前実装した理由
- 実運用で発生する誤検知への対処方法(抑制機能)
- CheckNoNewAccessで何が追加されたかを可視化する工夫
- GitHub Actionsでの自動化とPRコメントへのレポート出力
この記事で書かないこと
- APIの基本的な使い方(既存記事を参照してください)
カスタムポリシーチェックAPIの概要
まずIAM Access Analyzerが提供するAPIを整理しておきます。IAMポリシーを検証するためのAPIは4つあります。
| API | 用途 | 対象 | 料金 |
|---|---|---|---|
| CheckAccessNotGranted | 指定したアクションが許可されていないことを確認 | アイデンティティ/リソース | $0.002/回 |
| CheckNoNewAccess | 既存ポリシーと比較して新しい権限が追加されていないことを確認 | アイデンティティ/リソース | $0.002/回 |
| CheckNoPublicAccess | リソースポリシーがパブリックアクセスを許可していないことを確認 | リソースのみ | $0.002/回 |
| ValidatePolicy | ポリシーの文法とベストプラクティスを検証 | アイデンティティ/リソース | 無料 |
今回はIAMポリシー(アイデンティティポリシー)のCI/CDチェックに適したCheckAccessNotGrantedとCheckNoNewAccessを中心に扱います。CheckNoPublicAccessはS3バケットポリシーなどのリソースポリシー向けなので対象外とし、ValidatePolicyは補助的に使用します。
なぜCheckAccessNotGrantedとCheckNoNewAccessを両方使うのか
CheckAccessNotGrantedとCheckNoNewAccessは、片方だけでは足りません。
CheckAccessNotGrantedだけの場合
CheckAccessNotGrantedは、禁止リストに含まれないアクションの追加は検出できません。致命的な権限の追加を見逃してしまう可能性があります。
例:禁止リストに「s3:GetObject」があるとして...
- s3:GetObject の追加 → ✅ 検出できる
- ec2:RunInstances の追加 → ❌ 検出できない(リストにないため)
CheckNoNewAccessだけの場合
CheckNoNewAccessは、既存ポリシーにすでに含まれている問題を検出できません。既存ポリシーとの「差分」しか見ないため、もともと問題があるポリシーはスルーされてしまいます。
両方使って補完する
| 観点 | CheckAccessNotGranted | CheckNoNewAccess |
|---|---|---|
| 禁止アクションの使用を検出 | ✅ | ❌(禁止リストの概念なし) |
| 既存ポリシーの問題を検出 | ✅ | ❌(差分しか見ない) |
| リスト外のアクション追加を検出 | ❌ | ✅ |
というわけで、両方使うことでお互いの弱点を補えます。
なぜboto3で自前実装したか
AWSはカスタムポリシーチェックをCI/CDに組み込むための公式ツールを提供しています。
このActionはCloudFormationテンプレートやTerraformプランからIAMポリシーを抽出し、IAM Access Analyzerでチェックしてくれます。便利そうですね。
ユースケースに合わなかった
ただ私たちのプロジェクトでは、スタンドアローンのJSONポリシーファイルをリポジトリで管理し、Lambdaから動的にIAMロールへアタッチする構成を取っていました。
repository/
├── policies/
│ ├── AdminPolicy.json # スタンドアローンのポリシーファイル
│ ├── DeveloperPolicy.json
│ └── DenyListPolicy.json # Denyのみのポリシー
└── lambda/
└── create_role.py # ポリシーを読み込んでロールにアタッチ
AWS公式のActionだと次の点で合いませんでした。
| 観点 | AWS公式Action | 私たちの要件 |
|---|---|---|
| 入力形式 | CloudFormation/Terraform | スタンドアローンJSON |
| 禁止アクションリスト | 静的に指定 | 別のポリシーファイルから動的に読み込みたい |
| 抑制機能 | リソース名単位 | アクション+リソースパターンで細かく抑制したい |
| レポート形式 | 固定 | PRコメントにカスタム形式で出力したい |
そのためboto3で自前実装する方針としました。以降では実装時の工夫した点をまとめていきます。
実運用での工夫1:許容ケースの抑制
困ったこと
CheckAccessNotGrantedの禁止リストは アクション単位 でしか指定できません。そのため、特定のリソースに限定して許可したいケースでも、アクション自体が禁止リストに含まれていればFAILになります。
たとえばssm:GetParameterを禁止アクションに含めているとしましょう。でもAWSサービスが提供するパブリックパラメーター(/aws/service/配下)へのアクセスは許可したいケースがありますよね。
{
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": "arn:aws:ssm:*:*:parameter/aws/service/*"
}
これは顧客データへのアクセスではないので、許容したいところです。
解決策:抑制設定ファイルを作る
そこでアクションとリソースパターンの組み合わせで「許容するケース」を定義できるようにしました。
{
"suppressed_actions": [
{
"action": "ssm:GetParameter",
"resource_pattern": "arn:aws:ssm:*:*:parameter/aws/service/*",
"reason": "AWSサービス提供のパブリックパラメータのみ参照可能"
}
]
}
チェックスクリプト側でFAILしたアクションがこの設定にマッチするかを確認します。マッチすれば「抑制済み」として警告から除外するしくみです。
def is_action_suppressed(action: str, resource: str, suppression_rules: list) -> bool:
"""アクションが抑制対象かどうかを判定"""
for rule in suppression_rules:
# アクションがマッチするか(ワイルドカード対応)
if not fnmatch.fnmatch(action, rule.action):
continue
# リソースがパターンにマッチするか
if fnmatch.fnmatch(resource, rule.resource_pattern):
return True
return False
レポート出力では「抑制された件数」も表示して、透明性を確保しています。
**Summary**: ✅ 22 passed, ⚠️ 1 failed, 🔇 3 suppressed
実運用での工夫2:追加されたアクションの可視化
困ったこと
CheckNoNewAccess APIは結果として「PASS」または「FAIL」と、FAILの場合は「どのStatementが原因か」を返します。でも具体的にどのアクションが追加されたかは教えてくれません。
{
"result": "FAIL",
"message": "The modified permissions grant new access compared to your existing policy.",
"reasons": [
{
"description": "...",
"statementIndex": 0
}
]
}
PRレビューで「FAILしたけど、何が追加されたの?」と毎回確認するのは手間です。
解決策:自前で差分を計算
既存ポリシーと新ポリシーからAllowステートメントのアクションを抽出し、差分を取ります。
def get_added_actions(existing_policy: str, new_policy: str) -> list[str]:
"""既存ポリシーと新ポリシーを比較し、追加されたアクションを返す"""
existing_actions = extract_actions_from_policy(existing_policy)
new_actions = extract_actions_from_policy(new_policy)
added = new_actions - existing_actions
return sorted(added)
レポートでは次のように表示します。
- `path/to/policy.json`
- The modified permissions grant new access compared to your existing policy.
- **追加されたアクション**: `ec2:DescribeInstances`, `ec2:DescribeSecurityGroups`
これによりレビュアーは一目で「何が追加されたか」を把握できます。
GitHub Actionsでの自動化
PRでIAMポリシーファイルが変更されたときにチェックを実行し、結果をPRコメントとして投稿します。
name: IAM Policy Check
on:
pull_request:
paths:
- 'path/to/policies/**/*.json'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
check-iam-policies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # CheckNoNewAccessでmainとの比較に必要
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }}
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-role
aws-region: ap-northeast-1
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install dependencies
run: pip install boto3
- name: Run IAM Policy Check
continue-on-error: true # FAILしてもCIを止めない(情報提供目的)
env:
BASE_REF: ${{ github.base_ref }}
run: python scripts/check_iam_policies.py > results.md
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = fs.readFileSync('results.md', 'utf8');
// 既存コメントを更新、なければ新規作成
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('IAM Access Analyzer Check Results')
);
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: results
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: results
});
}
ポイント
fetch-depth: 0- CheckNoNewAccessでmainブランチのポリシーと比較するために必要
continue-on-error: true- FAILしてもCIを停止しない(情報提供目的のため)
- 既存コメントの更新
- 同じPRで複数回チェックを実行した場合、コメントが増え続けないように更新する
レポート出力例
PRコメントとして次のようなレポートを出力します。
## 🔐 IAM Access Analyzer Check Results
### 1. CheckAccessNotGranted(禁止アクションのチェック)
**Summary**: ✅ 22 passed, ⚠️ 1 failed, 🔇 1 suppressed
<details><summary>⚠️ Failed Checks</summary>
- `policies/AdminPolicy.json`
- The policy grants access to the restricted actions.
- Statement[0] の問題アクション: `s3:GetObject`
- ℹ️ DenyListPolicyでDenyされている場合は実効的に問題ありません
</details>
### 2. CheckNoNewAccess(権限拡大のチェック)
**Summary**: ✅ 20 passed, ⚠️ 1 failed, ⏭️ 2 skipped
<details><summary>⚠️ Failed Checks (New access detected)</summary>
- `policies/AdminPolicy.json`
- The modified permissions grant new access compared to your existing policy.
- **追加されたアクション**: `ec2:DescribeInstances`, `ec2:DescribeSecurityGroups`
</details>
### 3. ValidatePolicy(文法・ベストプラクティスチェック)
**Summary**: ✅ 23 passed, ⚠️ 0 warnings
---
> **Note**: このチェックはIAM Access Analyzerを使用しています。
> FAILの場合でもCIは停止しませんが、権限の変更内容を確認してください。
料金について
| API | 単価 | 想定利用量 | 月額 |
|---|---|---|---|
| CheckAccessNotGranted | $0.002/回 | 23ポリシー × 20PR | $0.92 |
| CheckNoNewAccess | $0.002/回 | 23ポリシー × 20PR | $0.92 |
| ValidatePolicy | 無料 | - | $0 |
チェックするポリシーファイルの数とPR回数で変動はしますが、おおむね月額$2程度という試算となりました。
まとめ
IAM Access AnalyzerのカスタムポリシーチェックをCI/CDに組み込む際のTipsを紹介しました。
- AWS公式Actionsはユースケースに合わなかった
- スタンドアローンJSON + カスタム要件には自前実装が必要
- CheckAccessNotGrantedとCheckNoNewAccessは両方使う
- それぞれの弱点を補完
- 抑制機能を実装する
- ノイズを減らし、本当に対処が必要なものにフォーカス
- 追加アクションを可視化する
- レビュアーの負担を軽減
- PRコメントとして記録
- レビューの証跡として活用
カスタムポリシーチェックはIAMポリシーのガバナンスを自動化する強力なツールです。公式ツールが合わない場合でもboto3で比較的簡単に実装できますので、ぜひ試してみてください。








