IAMで実際に困るのは、ポリシーの書き方そのものより「許可を足したのに動かない」「消したはずの権限で動けてしまう」という状況です。原因はたいてい、評価の順序を把握していないことにあります。
この記事では、評価順の整理から始めて、アクセスキーを配らないための構成、そして書き方を間違えると事故になるポリシーの実例をまとめます。
まず評価順を把握する

重要な性質が2つあります。
- 明示的な Deny は絶対に覆らない
他のポリシーがどれだけ Allow を並べても、どこか1か所に Deny があれば拒否されます - 何も書かなければ拒否
IAMのデフォルトは「暗黙の拒否」で、明示的に Allow されたものだけが通ります
この2つを踏まえると、トラブルシュートの順番が決まります。「権限が足りない」ときは、追加する前に、どこかで Deny されていないかを先に疑うことです。管理者権限を持っているのに操作できない場合、ほぼSCPかアクセス許可境界です。
アクセス許可境界(Permissions boundary)は特に誤解されやすい機能です。これは権限を与えるものではなく、上限を決める枠です。境界に s3:* と書いても、アイデンティティベースのポリシーに何も書かれていなければ何もできません。
ルートユーザーは封印する
アカウント作成時のルートユーザーは、SCPでも制限できない唯一の存在です。やることは決まっています。
- MFAを設定する(できればハードウェアキー)
- アクセスキーが発行されていたら即削除する
ルートのアクセスキーが必要な場面は通常ありません - 日常業務では一切使わない
請求設定などルートでしかできない操作のときだけ使う - 連絡先メールアドレスを、個人ではなく共有のものにしておく
# ルートのアクセスキーが存在しないことを確認(0 なら無し)
aws iam get-account-summary \
--query 'SummaryMap.AccountAccessKeysPresent'
アクセスキーを配らない
IAMの事故の多くは、ポリシーの書き方ではなく長期のアクセスキーが漏れることで起きます。GitHubへの誤コミット、退職者のキーの消し忘れ、共有のためのSlack送信。どれも運用でどうにかするより、そもそもキーを発行しないほうが確実です。
| 誰が使うか | 使うもの | 認証情報の寿命 |
|---|---|---|
| 人(コンソール・CLI) | IAM Identity Center | 一時的(セッション単位) |
| EC2 / ECS / Lambda | IAMロール | 一時的(自動更新) |
| GitHub Actions などのCI | OIDC連携 + IAMロール | 一時的(ジョブ単位) |
| 外部SaaS | 外部ID付きのIAMロール | 一時的 |
| 上記で対応できないもの | IAMユーザー + アクセスキー | 無期限 |
最後の行に落ちるケースは、実際にはかなり少ないはずです。「アクセスキーを作る」という判断をしたときは、なぜ上の4つで足りないのかを説明できるかを確認すると、余計なキーが減ります。
GitHub Actions は OIDC でつなぐ
CIからAWSを触るためにアクセスキーをSecretsに置く構成は、OIDC連携で置き換えられます。ロールの信頼ポリシーはこうなります。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}
]
}
ここで事故が起きやすいのが sub の条件です。次のような書き方をすると、意図した範囲を大きく超えます。
StringLikeでrepo:my-org/*→ 組織内のどのリポジトリからでもこのロールを取れます
フォークやテスト用リポジトリからも通りますrepo:my-org/my-repo:*→ そのリポジトリのどのブランチ・どのプルリクエストからでも通ります
外部からのPRでデプロイ用ロールを取られる可能性がありますsubの条件を書き忘れる → GitHub全体の誰からでも通ります
ブランチやEnvironmentまで含めて StringEquals で固定するのが基本です。複数ブランチを許可したい場合も、StringLike で広げるのではなく、値を配列で列挙するほうが安全です。

最小権限は手で書かない
「最小権限で」と言われて手作業でアクションを列挙すると、必ず抜けが出て、結局 * に戻ります。実際に使われたアクションからポリシーを生成するのが現実的です。
IAM Access Analyzer は、CloudTrailのログを読んで、その期間に実際に呼ばれたアクションだけを含むポリシーを生成できます。
aws accessanalyzer start-policy-generation \
--policy-generation-details '{
"principalArn": "arn:aws:iam::123456789012:role/MyAppRole"
}' \
--cloud-trail-details file://trail.json
注意点は、生成されるのは「その期間に使われたもの」だけということです。月次バッチや障害時にしか呼ばれないアクションは含まれません。生成結果をそのまま適用すると、月末に落ちます。
実務では、生成したポリシーをいきなり本番に当てず、まず検証環境で当てて、CloudTrailでアクセス拒否が出ないか一定期間観察するという手順を踏みます。Access Analyzer の「未使用のアクセス」検出も併用すると、逆方向(付けたまま使われていない権限)も見つかります。
条件で絞る|書き方を間違えると壊れる2つ
Condition による制限は効果が大きい一方、書き方を間違えると想定外の範囲を壊します。よく使う2つを、落とし穴つきで挙げます。
リージョンを東京に限定する
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideTokyo",
"Effect": "Deny",
"NotAction": [
"iam:*", "sts:*", "organizations:*",
"cloudfront:*", "route53:*", "support:*", "budgets:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": "ap-northeast-1" }
}
}
]
}
ポイントは NotAction の除外リストです。IAM・STS・CloudFront・Route 53 などのグローバルサービスは、リージョンを持たないか us-east-1 として扱われます。単純に「東京以外を全部Deny」と書くと、これらが軒並み使えなくなり、最悪ログインすらできなくなります。
MFAなしの操作を禁止する
{
"Sid": "DenyAllExceptMFASetupWhenNoMFA",
"Effect": "Deny",
"NotAction": [
"iam:ChangePassword",
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": { "aws:MultiFactorAuthPresent": "false" }
}
}
ここには落とし穴が2つあります。
BoolではなくBoolIfExistsを使うaws:MultiFactorAuthPresentは、そもそもMFAの概念がないリクエスト(ロールを引き受けたサービスからの呼び出しなど)ではキー自体が存在しません。Boolで書くと条件が一致せず、意図しない挙動になります- MFA登録に必要なアクションを除外する
これを忘れると、MFA未設定のユーザーがMFAを設定できなくなり、誰も何もできない状態になります
どちらの例も、自分自身を締め出す方向に間違えやすいポリシーです。適用前に、別の管理者アカウントでログインできる状態を確保しておくのが安全です。
監査を仕組みにする
IAMは作って終わりにできません。人の入れ替わりや一時的な権限付与が積み重なって、いつのまにか誰も把握していない状態になります。定期的に見るのではなく、検知される仕組みにします。
- AWS Config ルール
MFA未設定のユーザー、一定日数使われていないアクセスキー、ルートのアクセスキー存在などを自動検出する - IAM Access Analyzer
外部アカウントに公開されているリソースと、使われていない権限を検出する - Security Hub
CIS Benchmark などの基準に沿って横断的にチェックする - 認証情報レポート
aws iam generate-credential-reportで全ユーザーのMFA・キーの状態をCSVで取得できる。棚卸しに使える
まとめ
- 明示的なDenyは覆らない
権限が足りないときは、足す前にDenyを疑う - アクセス許可境界は上限であって権限ではない
境界だけ書いても何もできない - アクセスキーを発行しない構成を先に検討する
人はIdentity Center、CIはOIDC、実行環境はロール - OIDCの
subは必ずブランチまで固定する
ワイルドカードは組織全体への穴になる - 最小権限は実績から生成する
ただし生成対象の期間に含まれない処理に注意する - Conditionは自分を締め出す方向に間違えやすい
グローバルサービスの除外とBoolIfExistsを忘れない
次に読む記事

