IAMは「許可を足す」前に評価順を見る|権限が通らない原因の切り分け

IAMは「許可を足す」前に評価順を見る|権限が通らない原因の切り分け

IAMで実際に困るのは、ポリシーの書き方そのものより「許可を足したのに動かない」「消したはずの権限で動けてしまう」という状況です。原因はたいてい、評価の順序を把握していないことにあります。

この記事では、評価順の整理から始めて、アクセスキーを配らないための構成、そして書き方を間違えると事故になるポリシーの実例をまとめます。

目次

まず評価順を把握する

明示的なDeny・SCP・アイデンティティベースポリシー・アクセス許可境界・セッションポリシーの順に評価されるフローチャート
同一アカウント内でのリクエストの評価順。1つでも通らなければ拒否される

重要な性質が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 / LambdaIAMロール一時的(自動更新)
GitHub Actions などのCIOIDC連携 + 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 で広げるのではなく、値を配列で列挙するほうが安全です。

無人の駅コンコースに並ぶ5基の自動改札を、列の正面から撮った写真
通れない理由は、たいてい手前のゲートにある

最小権限は手で書かない

「最小権限で」と言われて手作業でアクションを列挙すると、必ず抜けが出て、結局 * に戻ります。実際に使われたアクションからポリシーを生成するのが現実的です。

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 を忘れない

次に読む記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

わどこんのアバター わどこん

実務12年のバックエンド・インフラエンジニア。バックエンド開発からクラウド・インフラの設計・構築・運用まで担当しています。主要言語は Java・Kotlin・PHP・Python。運用の現場で拾った知見を、再現できる手順に落として残すのがこのブログのテーマです。

目次