2026年6月2日
2026年6月2日
AWS S3のアクセス制御とバケットポリシー|WordPressで使うバケットを安全に公開・制限する書き方
S3 のアクセス制御は、バケットポリシーだけで決まるわけではありません。「パブリックアクセスのブロック」「オブジェクト所有者(ACL の扱い)」「IAM ポリシー」「バケットポリシー」が重なって判定され、どれか1つに明示的な拒否があればアクセスは通りません。この記事では、WordPress のメディア配信やバックアップ保存に使うバケットを例に、それぞれの層の役割、目的別のバケットポリシーの書き方、403(AccessDenied)が出たときの調べ方までを説明します。読み終えると、「公開したいものだけを、決めた経路からだけ読ませる」設定を自分で組み立てられるようになります。
S3 の既定値やコンソールの表記は更新されることがあります。ここでは AWS の公開ドキュメント(2026年10月現在)に沿って説明しますが、作業前に公式ドキュメントでも確認し、本番バケットを変更する前に現在のポリシーを保存しておいてください。
アクセスを決める4つの層
S3 へのリクエストは、おおむね次の層を通って許可・拒否が決まります。
- パブリックアクセスのブロック:アカウント単位とバケット単位で設定できる安全装置です。有効にすると、誰にでも読ませるようなバケットポリシーや ACL を付けようとしても拒否されるか、無視されます。
- オブジェクト所有者(Object Ownership):ACL を使うかどうかを決めます。「バケット所有者の強制」にすると ACL は無効になり、アクセス制御はポリシーだけで行います。
- IAM ポリシー(アイデンティティベース):IAM ユーザーやロールに付けて、「この人(このサーバー)が何をできるか」を決めます。
- バケットポリシー(リソースベース):バケットに付けて、「このバケットに誰が何をできるか」を決めます。別アカウントや CloudFront などの AWS サービスに許可を出すときはこちらを使います。
新しく作るバケットは、パブリックアクセスのブロックが有効で、ACL も無効な状態が既定になっています。以前の解説記事にある「ファイルごとに ACL で公開する」手順は、この既定のままでは通りません。現在は、ACL は無効のまま、ポリシーで必要な経路だけを開ける考え方が基本です。
判定のルール
同じアカウント内のアクセスでは、IAM ポリシーかバケットポリシーのどちらかで許可されていれば通ります。別アカウントからのアクセスでは、相手側の IAM ポリシーと、こちらのバケットポリシーの両方で許可が必要です。そして、どこか1か所でも "Effect": "Deny" に当てはまれば、ほかでいくら許可していても拒否されます。
もう1つ見落としやすいのが暗号化です。オブジェクトが SSE-KMS(KMS キーによる暗号化)で保存されている場合、S3 の権限に加えて KMS キーの kms:Decrypt などの権限も必要です。S3 の権限は合っているのに読めない、という場合はキーポリシーも確認します。
まず現状を確認する
設定を変える前に、今の状態を CLI で書き出しておきます。your-bucket-name は自分のバケット名に置き換えてください。
# パブリックアクセスのブロック
aws s3api get-public-access-block --bucket your-bucket-name
# オブジェクト所有者(ACL の有効・無効)
aws s3api get-bucket-ownership-controls --bucket your-bucket-name
# 現在のバケットポリシー(無い場合はエラーになる)
aws s3api get-bucket-policy --bucket your-bucket-name \
--query Policy --output text > policy-backup.json
policy-backup.json は、変更後に問題が起きたときに戻すための控えです。オブジェクト所有者が BucketOwnerEnforced なら ACL は無効、パブリックアクセスのブロックの4項目がすべて true なら、ポリシーで公開しようとしても止められる状態です。
目的別のバケットポリシー
ポリシーの Resource には、バケットそのものを表す ARN(arn:aws:s3:::your-bucket-name)と、中のオブジェクトを表す ARN(arn:aws:s3:::your-bucket-name/*)の2種類があります。s3:ListBucket はバケットに、s3:GetObject や s3:PutObject はオブジェクトに対する操作なので、ARN を取り違えると「書いたのに効かない」状態になります。いちばん多い書き間違いなので、先に覚えておくと調査が早くなります。
CloudFront からだけ読ませる(メディア配信)
WordPress のメディアを S3 に置いて配信するなら、バケットは非公開のままにして CloudFront のオリジンアクセスコントロール(OAC)経由で読ませる構成が安全です。パブリックアクセスのブロックも有効のままで動きます。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOACRead",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EXAMPLEDISTID"
}
}
}
]
}
Condition の AWS:SourceArn で、自分のディストリビューションからのリクエストだけに絞っています。これを省くと、ほかのアカウントの CloudFront からも読めてしまうおそれがあるため、削らないでください。アカウント ID とディストリビューション ID は例なので、自分の値に置き換えます。CloudFront のコンソールで OAC を設定すると、この形の雛形をコピーできることもあります。
HTTPS 以外の通信を拒否する
どの用途のバケットにも足しておきたいのが、暗号化されていない通信を拒否する文です。
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::your-bucket-name",
"arn:aws:s3:::your-bucket-name/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
これは上の OAC の文と同じ Statement 配列に並べて使います。AWS CLI や SDK、CloudFront からの通信は通常 HTTPS なので影響はありませんが、古いツールや自作スクリプトが HTTP で接続していると止まるので、適用後に各処理の動作を確認してください。
WordPress のバックアップ保存先として使う
バックアップ用のプラグインやスクリプトに権限を渡すときは、バケットポリシーよりも、WordPress サーバーの IAM ロール(EC2 ならインスタンスプロファイル)に付ける IAM ポリシーで絞るほうが管理しやすくなります。次は wp-backups/ 以下だけを読み書きできるようにする例です。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBackupPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::your-bucket-name",
"Condition": {
"StringLike": { "s3:prefix": ["wp-backups/*"] }
}
},
{
"Sid": "ReadWriteBackupObjects",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::your-bucket-name/wp-backups/*"
}
]
}
古い世代を自動で消す機能を使うプラグインなら s3:DeleteObject も必要になりますが、サーバーが乗っ取られたときにバックアップまで消される危険が増えます。削除は S3 のライフサイクルルールに任せ、サーバーには削除権限を渡さない構成も検討してください。必要な権限はプラグインごとに違うので、プラグインのドキュメントで確認し、足りないものだけを追加します。アクセスキーを使う場合も、キーを wp-config.php に書くならファイルの権限を絞り、Git などに含めないようにします。
ポリシーを適用する
作ったポリシーは、適用前に IAM Access Analyzer で検証できます。書式の誤りや、意図より広い許可になっていないかの警告が出ます。
aws accessanalyzer validate-policy \
--policy-document file://policy.json \
--policy-type RESOURCE_POLICY \
--validate-policy-resource-type AWS::S3::Bucket
aws s3api put-bucket-policy --bucket your-bucket-name --policy file://policy.json
put-bucket-policy は既存のポリシーを丸ごと置き換えます。追記のつもりで一部だけを送ると、それまでの許可が消えるので、現在のポリシー全体に文を足したファイルを送ってください。
Deny を含むポリシーは、書き方によっては自分自身も締め出します。たとえば特定の VPC エンドポイント以外を拒否する文を入れると、コンソールや手元の PC からも操作できなくなります。締め出されたときは、アカウントのルートユーザーでバケットポリシーを削除して戻せますが、ルートユーザーを使う手順は手間がかかるので、強い Deny はステージング用のバケットで試してから本番に入れるのが無難です。
403(AccessDenied)が出たときの調べ方
同じ 403 でも、原因の層によって直す場所が変わります。可能性の高い順に確認します。
- Resource の ARN の取り違え:一覧取得だけ失敗するならバケット ARN、読み書きだけ失敗するなら
/*付きの ARN を見直します。 - ACL を付けてアップロードしている:ACL が無効なバケットに、
public-readなどの ACL 付きでアップロードすると失敗します。エラーコードがAccessControlListNotSupportedなら、プラグインや CLI の設定で ACL を付けない(またはバケット所有者の強制に対応する)設定に変えます。 - パブリックアクセスのブロック:誰でも読める前提の設定(
Principal: "*"の許可など)は、ブロックが有効だと効きません。公開を止めるか、CloudFront 経由に切り替えます。 - 明示的な拒否:HTTPS 必須の文や、組織の SCP(AWS Organizations のポリシー)による拒否に当たっていないか確認します。
- KMS キーの権限:SSE-KMS で暗号化されたオブジェクトなら、キーポリシーと IAM 側の KMS 権限を確認します。
- CloudFront の設定:CloudFront 経由だけ 403 になる場合は、オリジンに OAC が付いているか、ポリシーのディストリビューション ARN が合っているかを見ます。存在しないキーを要求しても、
s3:ListBucketの許可が無いと 404 ではなく 403 が返るため、パスの誤りも疑ってください。
CloudTrail を有効にしていれば、拒否されたリクエストの呼び出し元やエラー内容をイベント履歴から確認できます。原因が分からないときは、まず「誰が」「どの ARN に」「何の操作を」しようとしたかを特定すると、どの層を見ればよいかが絞れます。
まとめ
- S3 のアクセスは、パブリックアクセスのブロック・オブジェクト所有者・IAM ポリシー・バケットポリシーの組み合わせで決まり、どこかに明示的な拒否があれば通らない。
- 新しいバケットはパブリックアクセスのブロックが有効で ACL も無効が既定。公開はポリシーと CloudFront の OAC で行う。
- バケット ARN とオブジェクト ARN(
/*)を正しく使い分ける。 - バックアップ用の権限は IAM ロールで接頭辞単位に絞り、削除権限は必要な場合だけ渡す。
- 適用前に現行ポリシーを保存し、Access Analyzer で検証してから置き換える。403 は層ごとに順に切り分ける。