このAccessDenied、末尾まで読んだことはありますか。
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:iam::123456789012:user/dev-tanaka is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::example-bucket/report.csv" with an explicit deny in a service control policy
注目してほしいのは末尾の「with an explicit deny in a service control policy」の部分です。調べてみたら、IAMのアクセス拒否メッセージは2021年9月から段階的に導入された改善で、どの種類のポリシーに弾かれたのかを文末に含めるようになっていました。つまりAccessDeniedは、メッセージを最後まで読んだ時点で原因の半分が特定できます。
それでも「AdministratorAccessを付けているのに拒否される」「Allowをいくら足しても通らない」という詰まり方が起きるのは、IAMポリシーの評価ロジックが「Allowの足し算」ではないからです。今回は公式ドキュメントの評価論理に沿って、リクエスト1回がどういう順序で評価されるのか、そして「暗黙のDeny」と「明示的なDeny」が何を指すのかを整理します。
AccessDeniedの原因は2種類しかない
IAMの評価結果が「拒否」になるパターンは、突き詰めると次の2つだけです。
- 暗黙のDeny(implicit deny)——リクエストを許可するAllowステートメントがどこにも見つからなかった
- 明示的なDeny(explicit deny)——
"Effect": "Deny"のステートメントに一致した
先ほどのエラーメッセージの末尾は、この2つを区別できる形で書かれています。「because no identity-based policy allows the s3:GetObject action」なら暗黙のDeny、「with an explicit deny in a service control policy」なら明示的なDeny、という具合です。どちらなのかで対処がまったく変わるので、トラブルシューティングはまずここを見るのが最短ルートだと思います。
評価ロジックの全体像は、IAMユーザーガイドの「Policy evaluation logic(ポリシーの評価論理)」という章にまとまっています。その中の「How AWS enforcement code logic evaluates requests to allow or deny access」というセクションの冒頭に、要約が3行だけ書かれています。
・デフォルトでは、すべてのリクエストは暗黙的に拒否される(フルアクセスを持つAWSアカウントのrootユーザーだけが例外)
・許可されるには、ポリシーによって明示的にAllowされる必要がある
・明示的なDenyは、明示的なAllowを上書きする
要するにIAMはホワイトリスト方式で、その上に「Denyは何よりも強い」というルールが1本乗っている構造です。ユーザー・グループ・ロール・ポリシーという登場人物の関係は前回の記事で整理したので、今回はその続きとして、書いたポリシーが評価される側の仕組みに絞ります。
暗黙のDenyと明示的なDenyの違い
暗黙のDeny——Allowがどこにも見つからない状態
作成した直後の、ポリシーを何もアタッチしていないIAMユーザーでS3の一覧を叩くと、こう返ってきます。
An error occurred (AccessDenied) when calling the ListBuckets operation: User: arn:aws:iam::123456789012:user/new-user is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action
これが暗黙のDenyです。誰かが「Denyと書いたから」拒否されたのではなく、Allowが1つも見つからなかったので、デフォルトの拒否がそのまま残ったという状態です。公式ドキュメントには「明示的な拒否と暗黙的な拒否の違い」というページが独立して用意されているくらいで、この区別が評価論理を理解する土台になります。
明示的なDeny——Effect: Denyのステートメントに一致した状態
一方の明示的なDenyは、どこかのポリシーに書かれた"Effect": "Deny"のステートメント(Action・Resource・Conditionの条件すべて)にリクエストが一致した状態です。発生源はアイデンティティベースのポリシーとは限らず、SCP(サービスコントロールポリシー)・リソースベースのポリシー・アクセス許可境界・セッションポリシー——どこに書かれたDenyでも同じように効きます。
決定的な違いは「Allowで覆せるかどうか」
2つの違いを表にするとこうなります。
| 暗黙のDeny | 明示的なDeny | |
|---|---|---|
| 発生条件 | 一致するAllowが1つもない | Effect: Denyのステートメントに一致 |
| Allowの追加で解消できるか | できる | できない(DenyがAllowより優先) |
| エラーメッセージの手がかり | because no … policy allows | with an explicit deny in … |
| 対処の方向 | 必要なAllowを付与する | Denyステートメント自体を見直す |
AdministratorAccessの実体は"Action": "*", "Resource": "*"のAllowにすぎないので、明示的なDenyが1つでも一致すれば普通に負けます。冒頭の「AdministratorAccessなのにAccessDenied」の正体はこれで、いくら強いAllowを積んでも解決しません。Denyが書かれている場所を探しに行く必要があります。
評価順序を図で追う——最初に走るのはDenyチェック
では、リクエスト1回はどんな順序で評価されるのか。公式ドキュメントのフローチャートを、同一アカウント内のリクエストに絞って描き直したのが次の図です。
Denyのチェックは横断的で、順序を持たない
この図でいちばん大事なのは、ステップ1のDenyチェックだけがすべてのポリシータイプを横断するという点です。「アイデンティティベースのDenyとSCPのDenyはどちらが強いのか」という疑問を最初持っていたのですが、調べてみたらこの問い自体が成立しませんでした。Denyは書かれた場所に関係なく最初にまとめて検索され、1つでも一致すればその時点で拒否が確定します。評価の「順序」が意味を持つのは、ステップ2以降のAllow探しだけです。
Allow側は5段階を順番に通過する
明示的なDenyがなければ、次の順序でAllowが探されます。
- RCP / SCP——AWS Organizationsを使っている場合のガードレール。Allowがなければその時点で拒否
- リソースベースポリシー——S3バケットポリシーやKMSキーポリシーなど、リソース側に付くポリシー
- アイデンティティベースポリシー——ユーザー・グループ・ロールに付くポリシー。ここにAllowがなければ暗黙のDenyで確定
- アクセス許可境界——設定している場合のみ。境界が許可していなければ暗黙のDeny
- セッションポリシー——AssumeRole時などに渡した場合のみ評価される
図の「※」はリソースベースポリシー特有の挙動です。公式ドキュメントの同セクションでは、次のように説明されています。
- 原則——同一アカウント内でリソースベースポリシーがリクエスト元のプリンシパルを直接Allowしていれば、その時点で許可が確定する。アイデンティティベースポリシーやアクセス許可境界に暗黙のDenyがあっても影響しない
- 分岐点——プリンシパルの指定がユーザーARN・ロールARN・ロールセッションARNのどれかによって、アクセス許可境界やセッションポリシーの暗黙のDenyが効くかどうかが変わる
- 例外——IAMロールの信頼ポリシーとKMSキーポリシーは、リソース側での明示的なAllowが必須
このあたりはかなり深いので、基礎としては「リソースベースポリシーは他と合成のされ方が違う」とだけ覚えておけば十分だと思います。
ポリシータイプが複数絡むとき——和集合と積集合
フローを覚えなくても、「どのポリシーとどのポリシーがどう合成されるか」だけで大半のケースは判断できます。合成のパターンは2つしかありません。
アイデンティティベース×リソースベースは和集合
同一アカウント内では、この2つは和集合(union)で評価されます。ユーザー側のポリシーとバケットポリシーのどちらか一方にAllowがあれば許可されます。S3バケットポリシーでアクセスを許可すれば、ユーザー側にS3のAllowを書かなくても読める、というのはこの仕組みによるものです。
SCP・アクセス許可境界・セッションポリシーは積集合
一方、SCP・RCP・アクセス許可境界・セッションポリシーは積集合(intersection)で効きます。アイデンティティベースポリシーにAllowを書いても、これらのどれかがAllowしていなければ通りません。「Allowを書いたのに暗黙のDenyで弾かれる」ときは、この積集合のどこかに穴があります。なお、SCPはプリンシパル側・RCPはリソース側のガードレールという守備範囲の違いがありますが、公式ドキュメントの「Evaluating identity-based policies with AWS Organizations SCPs or RCPs」では、どちらもアイデンティティベースポリシーとの積集合として説明されています。
| 組み合わせ | 合成のされ方 | 許可に必要な条件 |
|---|---|---|
| アイデンティティベース + リソースベース | 和集合 | どちらか一方にAllowがあればよい |
| アイデンティティベース + アクセス許可境界 | 積集合 | 両方にAllowが必要 |
| アイデンティティベース + SCP / RCP | 積集合 | すべてにAllowが必要 |
| アイデンティティベース + セッションポリシー | 積集合 | 両方にAllowが必要 |
エラーメッセージの末尾に「because no identity-based policy allows …」ではなく「due to a permissions boundary」や「service control policy」系の文言が出ていたら、アイデンティティベースポリシーをいくら眺めても原因は見つからない、と判断できます。
明示的なDenyの使いどころ
アンチパターン——「Allowを書いていない」は防御ではない
「本番のS3バケットを消されたくないので、削除のAllowは誰にも付けていません」という状態は、一見安全そうで防御になっていません。暗黙のDenyは「今はまだAllowがない」だけの状態なので、管理者権限を持つ誰かがAllowを足した瞬間に通ります。AdministratorAccessを持つメンバーが1人でもいれば、その人には最初から効いていません。
Condition付きDenyでガードレールを敷く
後から何をAllowされても覆らないルールを作りたいときが、明示的なDenyの出番です。たとえば特定のIPレンジ以外からのアクセスを塞ぐ場合はこう書きます。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideOfficeIP",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["203.0.113.0/24"]
}
}
}
]
}
ただしこの形には有名な罠があって、CloudFormationのようにAWSのサービスがユーザーの代理でAPIを呼ぶ経路も「指定IPの外」として弾いてしまいます。公式ドキュメントではaws:ViaAWSService条件キーと組み合わせて回避する形が案内されているので、実際に入れるときはそちらを確認してからのほうが安全です。
白状すると、以前このIP制限Denyを検証用に自分のユーザーへ付けたまま消し忘れて、翌日別の場所から「with an explicit deny in an identity-based policy」を食らったことがあります。エラーメッセージを末尾まで読む習慣は、このときに身につきました。Denyは強力なぶん、付けた本人ごと巻き込むという実例です。
simulate-custom-policyで評価判定を再現する
ここまでの挙動は、AWS CLIのaws iam simulate-custom-policyで手元で再現できます。IAM自体に課金はなく、このシミュレーションAPIも無料で、実リソースへのアクセスは一切発生しません。まず検証用のポリシーを用意します。
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "s3:*", "Resource": "*" },
{ "Effect": "Deny", "Action": "s3:DeleteObject", "Resource": "*" }
]
}
これをpolicy.jsonとして保存し、3つのアクションで評価をシミュレートします。iam:CreateUserはポリシーのどこにも書かれていないアクションです。
aws iam simulate-custom-policy \
--policy-input-list file://policy.json \
--action-names s3:GetObject s3:DeleteObject iam:CreateUser \
--query 'EvaluationResults[].[EvalActionName,EvalDecision]' \
--output table
----------------------------------------
| SimulateCustomPolicy |
+---------------------+----------------+
| s3:GetObject | allowed |
| s3:DeleteObject | explicitDeny |
| iam:CreateUser | implicitDeny |
+---------------------+----------------+
explicitDenyとimplicitDenyが区別されて返る
結果のEvalDecisionはallowed・explicitDeny・implicitDenyの3値です(SimulateCustomPolicy APIのPolicyEvaluationDecisionType)。ここまで説明してきた「拒否は2種類」という区別が、APIの戻り値のレベルでそのまま分かれて返ってくるのが分かります。s3:DeleteObjectはAllowにもDenyにも一致していますが、結果はexplicitDeny——Denyが勝つルールもこの通り確認できます。
コンソールのポリシーシミュレータとの使い分け
simulate-custom-policyはJSONを直接渡して「書きかけのポリシー」を試すコマンドです。既存のユーザーやロールにアタッチ済みのポリシー一式で試したい場合は、aws iam simulate-principal-policyか、ブラウザのIAMポリシーシミュレータを使います。コンソール版の使い方は前回の記事で触れたので、今回はCLI版だけ紹介しました。書き捨てのJSONで評価順序を確かめるには、CLI版のほうが速くて向いていると思います。
まとめ
- 拒否は2種類だけ——Allowが見つからない「暗黙のDeny」と、
Effect: Denyに一致した「明示的なDeny」 - 明示的なDenyはどんなAllowでも覆せない。評価の最初に全ポリシータイプを横断してチェックされ、Deny同士に順序や強弱はない
- Allow側の評価順序は RCP / SCP → リソースベース → アイデンティティベース → アクセス許可境界 → セッションポリシー
- アイデンティティベースとリソースベースは和集合、SCP・アクセス許可境界・セッションポリシーは積集合で効く
- AccessDeniedのエラーメッセージは末尾に原因のポリシータイプが書かれている(2021年9月以降)ので、まず最後まで読む
- ガードレールを敷くなら暗黙のDenyに頼らず、Condition付きの明示的なDenyを書く。検証は
simulate-custom-policyなら無料で試せる
評価論理が頭に入ると、次に気になるのは「ロールを引き受ける(AssumeRole)とき、この評価がどう変わるのか」です。セッションポリシーやロールの信頼ポリシーが絡んでくる部分なので、次はそこを追う予定です。