S3 で「バケット内のリスト」だけを無効化し、個別オブジェクトの public-read 配信は維持する
まとめ
- バケット ACL を
public-readにすると、誰でもバケット直下に対するListObjectsV2が叩けてしまう(オブジェクトキー・サイズ・LastModified が匿名で取得できる情報漏洩リスク) - 個別オブジェクトの
public-read配信(CarrierWave の asset_sync / Rails アセット等)は維持しつつ、 バケットレベルのリストだけを拒否 したい場合の Terraform 構成 - ポイントは
aws_s3_bucket_ownership_controlsをObjectWriterで入れた上で、aws_s3_bucket_public_access_blockのblock_public_acls=falseを維持すること - 検証は
curlで匿名 ListBucket(?list-type=2)を叩いて HTTP 403 になり、個別オブジェクトの匿名 GET は 200 のままであることを確認する
背景: バケット ACL public-read の落とし穴
バケット ACL を public-read にすると、「匿名ユーザーへの READ 権限」がバケット全体に対して付与される。READ 権限は オブジェクトの読み取り だけでなく バケット内のオブジェクト一覧 にも適用されるため、以下が成立してしまう:
curl -s "https://${BUCKET}.s3.${REGION}.amazonaws.com/?list-type=2&max-keys=10"
# → HTTP 200 + <ListBucketResult>... が返り、全オブジェクトキーが匿名で取得できるオブジェクトキー命名にユーザー ID やランダム ID が含まれていれば、それ自体がメタ情報の漏洩になる。プライベートな署名付き URL 配信用の private オブジェクトであっても、 キーの存在自体は List で露呈する 。
一方、アプリケーション側(CarrierWave + asset_sync / Rails Webpacker など)が「個別オブジェクトには public-read ACL を付けて配信」する運用は維持したい。
Terraform 構成
resource "aws_s3_bucket" "main" {
bucket = "${var.bucket_name}"
}
# バケット ACL は private に絞る
resource "aws_s3_bucket_acl" "main" {
bucket = aws_s3_bucket.main.id
acl = "private"
depends_on = [aws_s3_bucket_ownership_controls.main]
}
# Ownership Controls: BucketOwnerEnforced ではなく ObjectWriter にする
resource "aws_s3_bucket_ownership_controls" "main" {
bucket = aws_s3_bucket.main.id
rule {
object_ownership = "ObjectWriter"
}
}
# Public Access Block: 個別オブジェクトの public-read ACL は許可したいので
# block_public_acls / ignore_public_acls は false のまま
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}各リソースの役割
aws_s3_bucket_acl を private にする
バケット全体への匿名 READ 権限を取り消す。これによって匿名 ListBucket は AccessDenied になる。depends_on = [aws_s3_bucket_ownership_controls.main] を付けないと、Ownership Controls が未設定の状態で ACL を変更しようとして API がエラーを返すことがある。
aws_s3_bucket_ownership_controls を ObjectWriter にする
S3 はデフォルトで Ownership Controls が BucketOwnerEnforced(ACL 自体を無効化する設定)になる場合がある。 BucketOwnerEnforced だと個別オブジェクトの ACL も丸ごと無効化されるため、CarrierWave 等が public-read ACL を付けて upload するコードが動かなくなる 。
ObjectWriter を明示することで「アップロード元(IAM ユーザー / ロール)がオブジェクトに ACL を付与できる」状態を保てる。
aws_s3_bucket_public_access_block で各フラグを false 維持
Public Access Block は 4 つの抑止フラグを持つ:
| フラグ | 役割 | 今回の値 |
|---|---|---|
block_public_acls | 新規 ACL に Public 付与を禁止 | false (個別 ACL を許可したい) |
ignore_public_acls | 既存の Public ACL を無視 | false (既存の公開オブジェクトを配信したい) |
block_public_policy | バケットポリシーに Public 付与を禁止 | false |
restrict_public_buckets | Public バケットへのアクセスを制限 | false |
「Public Access Block を入れる = すべて true にする」というセキュリティ強化のテンプレ運用が広まっているが、 今回は「バケットレベルの List だけ拒否したい」のが目的 なので、ACL 関連フラグは false を維持する必要がある。すべて true にすると個別オブジェクトの public-read 配信が壊れる。
明示的にリソースを作成しておくことで、「false を意図的に選んでいる」ことが Terraform コードから読み取れる(デフォルト挙動に頼らない)。
検証
1. 匿名 ListBucket が拒否されることを確認
curl -s -o /tmp/anon-list.xml \
-w "HTTP: %{http_code}\n" \
"https://${BUCKET}.s3.${REGION}.amazonaws.com/?list-type=2&max-keys=2"
head -c 200 /tmp/anon-list.xml期待:
HTTP: 403
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code>...2. 認証付き ListBucket が引き続き機能することを確認
aws s3api head-bucket --bucket "$BUCKET" --profile "$PROFILE" && echo "OK"HeadBucket は s3:ListBucket 権限を要求するので、成功すれば「認証付きで List できる権限がある」確認になる。
3. 個別オブジェクトの匿名 GET が引き続き機能することを確認
asset_sync / アセット配信用のプレフィックスから 1 件キーを取って匿名 curl:
PUBKEY="assets/some-fingerprinted-asset.woff"
curl -sI "https://${BUCKET}.s3.${REGION}.amazonaws.com/${PUBKEY}" | head -3
# → HTTP/1.1 200 OK4. プレフィックス別の傾向(一括確認)
for prefix in "assets/" "packs/" "uploads/"; do
echo "=== prefix: $prefix ==="
aws s3api list-objects-v2 --bucket "$BUCKET" \
--prefix "$prefix" --max-keys 3 --profile "$PROFILE" \
--query 'Contents[*].Key' --output text | tr '\t' '\n' | while read k; do
[ -z "$k" ] && continue
code=$(curl -s -o /dev/null -w "%{http_code}" \
"https://${BUCKET}.s3.${REGION}.amazonaws.com/${k}")
echo "[$code] $k"
done
done公開アセット(assets/ packs/)は 200、private なユーザーアップロード(uploads/)は 403 のまま、という傾向が変わらなければOK。
落とし穴メモ
- Public Access Block を「すべて true」にしてはいけない : 個別オブジェクトの
public-read配信が動かなくなる BucketOwnerEnforcedを選んではいけない : ACL 自体が無効化され、アプリ側の ACL 付与 upload が失敗するaws_s3_bucket_aclのdepends_on: Ownership Controls 未設定状態で ACL 変更すると API エラー- CloudFront 経由の場合 : CloudFront は個別オブジェクトに対する GetObject しか叩かないため、バケット List 拒否の影響は受けない(通常)
「全部 private にして全部署名付き URL」が理想だが、CarrierWave 等のアプリ側仕様で個別 ACL 配信が前提になっている場合の現実解として有用な構成。