投稿

S3 で「バケット内のリスト」だけを無効化し、個別オブジェクトの public-read 配信は維持する

S3 で「バケット内のリスト」だけを無効化し、個別オブジェクトの public-read 配信は維持する

まとめ

  • バケット ACL を public-read にすると、誰でもバケット直下に対する ListObjectsV2 が叩けてしまう(オブジェクトキー・サイズ・LastModified が匿名で取得できる情報漏洩リスク)
  • 個別オブジェクトの public-read 配信(CarrierWave の asset_sync / Rails アセット等)は維持しつつ、 バケットレベルのリストだけを拒否 したい場合の Terraform 構成
  • ポイントは aws_s3_bucket_ownership_controlsObjectWriter で入れた上で、aws_s3_bucket_public_access_blockblock_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_aclprivate にする

バケット全体への匿名 READ 権限を取り消す。これによって匿名 ListBucket は AccessDenied になる。depends_on = [aws_s3_bucket_ownership_controls.main] を付けないと、Ownership Controls が未設定の状態で ACL を変更しようとして API がエラーを返すことがある。

aws_s3_bucket_ownership_controlsObjectWriter にする

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"

HeadBuckets3: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 OK

4. プレフィックス別の傾向(一括確認)

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_acldepends_on : Ownership Controls 未設定状態で ACL 変更すると API エラー
  • CloudFront 経由の場合 : CloudFront は個別オブジェクトに対する GetObject しか叩かないため、バケット List 拒否の影響は受けない(通常)

「全部 private にして全部署名付き URL」が理想だが、CarrierWave 等のアプリ側仕様で個別 ACL 配信が前提になっている場合の現実解として有用な構成。

トレンドのタグ