エラーの内容
いつものような S3 コマンドを実行すると、次のエラーが返ってきます:
$ aws s3 cp s3://my-bucket/data/report.csv .
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden
または boto3 からの場合:
botocore.exceptions.ClientError: An error occurred (403) when calling the HeadObject operation: Forbidden
バケットは存在する。オブジェクトも存在する。コンソールでも確認できる。それでも SDK はアクセスを拒否する。
原因
S3 のアクセス制御は単一のゲートではなく、四つのゲートがあります。IAM アイデンティティポリシー、バケットポリシー、ACL、そしてパブリックアクセスブロック設定がそれぞれ独立して評価され、そのうちのどれか一つでもリクエストを拒否できます。厄介なのは、SDK の視点から見ると Allow がないことと明示的な Deny が全く同じように見えることです。
よくある原因:
- バケットポリシーが IAM プリンシパルまたは IP レンジに対して明示的な
Denyを設定している - IAM ロール/ユーザーに
s3:GetObjectまたはs3:HeadObject権限がない - オブジェクト ACL がプライベートに設定されており、バケットオーナーの強制が無効になっている
- バケットが別の AWS アカウントに存在し、クロスアカウントの信頼関係が設定されていない
- S3 パブリックアクセスブロックが有効な状態で、バケットポリシーがパブリックアクセスを許可しようとしている
- サーバーサイド暗号化に、ロールが復号できない KMS キーが使用されている
手順ごとの解決方法
手順 1 — IAM アイデンティティの確認
ポリシーを変更する前に、実際にリクエストを発行している IAM プリンシパルを確認します:
aws sts get-caller-identity
出力例:
{
"UserId": "AROAXXXXXXXXXXXXXXXXX:session",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:role/my-app-role"
}
その ARN を控えておいてください。次の手順でポリシーのステートメントと照合します。
手順 2 — バケットポリシーの確認
aws s3api get-bucket-policy --bucket my-bucket | python3 -m json.tool
自分の ARN に該当する "Effect": "Deny" ステートメントを探します。aws:SourceIp や aws:PrincipalOrgID のような条件にも注意してください。プリンシパルが正しく見えても、これらの条件によって静かにブロックされることがあります。
自分のロールを含む Deny が見つかった場合は、それを削除するか、その上に明示的な Allow を追加します:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAppRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/my-app-role"
},
"Action": ["s3:GetObject", "s3:HeadObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
適用する:
aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
手順 3 — IAM 権限の確認
バケットポリシーだけが全てではありません。同一アカウント内のアクセスでは、バケットポリシーまたはアイデンティティポリシーのどちらかがアクションを許可していれば十分です。クロスアカウントアクセスはより厳格で、両方が明示的に許可している必要があります。
IAM ポリシーシミュレーターを実行して、何が起きているかを正確に確認します:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/my-app-role \
--action-names s3:GetObject s3:HeadObject \
--resource-arns arn:aws:s3:::my-bucket/data/report.csv
"EvalDecision": "implicitDeny" または "explicitDeny" が返ってきた場合は、アイデンティティポリシーの更新が必要です。次のようなポリシーをアタッチしてください:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:HeadObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
手順 4 — ACL の確認(該当する場合)
ACL が影響するのは、オブジェクトオーナーシップが バケットオーナー強制 に設定されていない場合のみです。ACL が有効な場合、オブジェクトレベルの ACL が他の設定を上書きすることがあります:
aws s3api get-object-acl --bucket my-bucket --key data/report.csv
別のアカウントからアップロードされたオブジェクトで ACL からアクセスが除外されている場合は、リセットします:
aws s3api put-object-acl \
--bucket my-bucket \
--key data/report.csv \
--acl bucket-owner-full-control
さらに良い方法は、ACL を完全に廃止することです:
aws s3api put-bucket-ownership-controls \
--bucket my-bucket \
--ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'
手順 5 — KMS 暗号化の確認
カスタマーマネージド KMS キーは見落としやすいレイヤーを追加します。head-object を実行して、KMS が関与しているか確認します:
aws s3api head-object --bucket my-bucket --key data/report.csv
レスポンスに ServerSideEncryption: aws:kms と SSEKMSKeyId が含まれている場合は、IAM ポリシーに KMS 権限を追加します:
{
"Effect": "Allow",
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}
修正の確認
完了とする前に、簡単なサニティチェックを実行します:
# HeadObject — メタデータのみ、高速
aws s3api head-object --bucket my-bucket --key data/report.csv
# 実際のダウンロード
aws s3 cp s3://my-bucket/data/report.csv /tmp/report.csv
echo $? # 0 = 成功
boto3 から確認する場合:
import boto3
s3 = boto3.client('s3')
try:
resp = s3.head_object(Bucket='my-bucket', Key='data/report.csv')
print('OK:', resp['ContentLength'], 'bytes')
except Exception as e:
print('Still failing:', e)
クロスアカウントのシナリオ
バケットがアカウント A にあり、ロールがアカウント B にある場合、両方が明示的にアクションを許可する必要があります。回避策はありません。アカウント A のバケットポリシーで、外部プリンシパルを直接指定する必要があります:
{
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_B_ID:role/cross-account-role"
},
"Action": ["s3:GetObject", "s3:HeadObject"],
"Effect": "Allow",
"Resource": "arn:aws:s3:::my-bucket/*"
}
アカウント B のアイデンティティポリシーでも、対象バケットへの s3:GetObject を許可する必要があります。どちらか一方でも欠けると、403 エラーに戻ります。
ヒント
- CloudTrail でどのポリシーが発動したか確認できます。
s3.amazonaws.comのイベントでerrorCode: AccessDeniedをフィルタリングしてください。イベントの詳細に拒否を引き起こしたポリシーが明記されており、ポリシーを手探りで読むよりはるかに速く原因を特定できます。 - IAM ポリシーシミュレーターは最大の味方です。 変更前後に
aws iam simulate-principal-policyを実行して変化を確認しましょう。推測せず、シミュレートしてください。 - S3 サーバーアクセスログを有効にすると、誰がいつどの IP からアクセスしたかの永続的な監査証跡が残ります。
- s3fs で S3 バックのファイルシステムをマウントする EC2 インスタンスプロファイルを使用している場合は、ToolCraft Unix Permissions Calculator が Linux 側のファイルレベル権限のクロスチェックに役立ちます。ブラウザ上で完全動作し、データがマシン外に出ることはありません。
- 新しいバケットでは ACL を廃止しましょう。 ACL はレガシーの仕組みです。バケットポリシーと混在させると、このような 403 エラーが発生します。追跡が難しく、修正も面倒です。バケットオーナー強制モードとシンプルなバケットポリシーの組み合わせの方が、シンプルで監査もしやすいです。

