AWS S3バケット、うっかり公開は命取り!パブリックアクセスブロックとACL無効化で鉄壁の守りを築こう
おい、お前たち。今日も今日とて、コードとにらめっこしながらプロダクトを成長させていることだろう。素晴らしいことだ。だが、忘れるな。我々エンジニアの仕事は、ただ動くものを作るだけじゃない。「安全に」、そして「継続的に」動かし続けること。これが、セキュリティチーフとしての俺たちの責務だ。
今日は、特に多くの現場で「うっかり」が発生しがちな、AWS S3バケットの公開設定について、実戦的な話をしていく。小手先のテクニックじゃない。攻撃者がどうやってS3の「穴」を見つけ出し、どんな悲劇が起こりうるのか。そして、それをどうすれば二度と起こらないようにできるのか。具体的なコードと設定例を交えて、徹底的に解説しよう。
なぜS3バケットの公開がそんなにヤバいのか?攻撃者の視点に立ってみよう
まず、なぜ「S3バケットの公開」がそれほど危険視されるのか、攻撃者の目線で考えてみよう。
S3バケットは、Webサイトの静的ホスティング、ログの保存、バックアップ、アプリケーションのアセット置き場など、AWS上で最も汎用的に使われるストレージサービスだ。その手軽さゆえに、設定を誤ると、意図せず機密情報がインターネット上に丸裸になりかねない。
典型的な攻撃シナリオはこうだ。
1. 偵察(Reconnaissance):
攻撃者は、公開されているIPアドレス、ドメイン名、SSL証明書情報などから、ターゲットのAWS環境を推測する。そこから、S3バケット名や、バケット名になりそうな単語(my-company-prod-data, customer-uploads-2023, backup-logsなど)を推測して、公開されているS3バケットを探し出す。
2. スキャンと列挙(Scanning & Enumeration):
s3.amazonaws.comやs3.といったエンドポイントに対して、推測したバケット名を片っ端から試していく。あるいは、Shodanのようなインターネット検索エンジンや、公開されているGitHubリポジトリ、過去のデータ漏洩情報などを漁り、S3バケットのURLや名前を収集する。
例えば、こんな感じのURLが見つかることがある。
http://my-company-prod-data.s3.amazonaws.com/
http://customer-uploads-2023.s3.ap-northeast-1.amazonaws.com/
これらのURLにアクセスし、バケットの中身が見えるか、ファイルリストが取得できるかを確認する。
3. データ窃取(Data Exfiltration):
もしバケットが公開されており、かつオブジェクトのリスト表示(List Objects)やオブジェクトの取得(GetObject)が許可されている場合、攻撃者はバケット内の全ファイルをダウンロードできる。
- 機密情報: 顧客データ、認証情報(APIキー、パスワード)、ソースコード、設定ファイル、秘密鍵などが含まれている可能性がある。
- 個人情報: 氏名、住所、電話番号、メールアドレス、クレジットカード情報などが含まれている場合、個人情報保護法違反やGDPR違反に問われ、巨額の罰金や信用失墜につながる。
- 知的財産: 未公開の製品情報、設計図、マーケティング戦略などが漏洩すると、競争優位性を失う。
4. 悪用(Malicious Use):
公開されたバケットが、Webサイトのホスティング設定になっている場合、攻撃者はバケットを乗っ取り、マルウェア配布サイトやフィッシングサイトとして悪用することも可能だ。これは、あなたの会社のブランドイメージを著しく損なう。
【PoC例】手軽に試せる「攻撃者目線」のS3バケット発見ツール
攻撃者がどのようにS3バケットを探すのか、その一端を体験するために、簡単なPythonスクリプトを紹介しよう。※これはあくまで学習・検証目的であり、許可なく他者のAWS環境に対して実行することは絶対にしないでください。
import boto3
import requests
from botocore.exceptions import ClientError
攻撃者が推測しそうなバケット名のリスト
POTENTIAL_BUCKET_NAMES = [
“company-name-prod”,
“company-name-data”,
“company-name-backup”,
“company-name-logs”,
“customer-data”,
“user-uploads”,
“config-files”,
“secrets”,
“api-keys”,
“private-data”,
“sensitive-info”,
“prod-assets”,
“dev-bucket”,
“test-data”
]
S3クライアントを作成(認証情報なしで実行すると、匿名アクセスを試みる)
s3 = boto3.client(‘s3’)
def check_public_s3_bucket(bucket_name):
“””
指定されたS3バケットが公開されているか(ファイルリスト取得可能か)を確認する。
“””
try:
# バケットのオブジェクトリストを取得しようと試みる
# ListObjectsV2 は、バケット内のオブジェクトを一覧表示するためのAPI
response = s3.list_objects_v2(Bucket=bucket_name)
# レスポンスに ‘Contents’ が含まれている場合、オブジェクトが存在し、リスト表示が可能
if ‘Contents’ in response:
print(f”[+] Bucket Found: s3://{bucket_name} – Objects are listed. Potential data leak!”)
# 具体的なファイル名も表示してみる(最初の数個)
for obj in response.get(‘Contents’, [])[:3]:
print(f” – {obj[‘Key’]}”)
return True
# ‘Contents’ がなくても、Prefix を指定してエンプティバケットでもリスト表示が成功することがある
elif ‘Prefix’ in response:
print(f”[+] Bucket Found: s3://{bucket_name} – Bucket exists and might be listable with specific prefixes.”)
return True
else:
# アクセスはできるが、中身が見えない(例: Public Access Block が有効だが、ACLで一部許可されている場合など)
# この場合、さらに詳細なチェックが必要だが、ここでは「公開」の可能性は低いと判断
pass
except ClientError as e:
# 403 Forbidden は、アクセス権限がないがバケットは存在する可能性
if e.response[‘Error’][‘Code’] == ‘403’:
# print(f”[-] Bucket exists but access denied: s3://{bucket_name} (403 Forbidden)”)
pass
# 404 Not Found は、バケットが存在しない
elif e.response[‘Error’][‘Code’] == ‘404’:
# print(f”[-] Bucket not found: s3://{bucket_name} (404 Not Found)”)
pass
else:
print(f”[!] Error checking bucket s3://{bucket_name}: {e}”)
except Exception as e:
print(f”[!] Unexpected error checking bucket s3://{bucket_name}: {e}”)
return False
def check_bucket_url(bucket_name, region=””):
“””
バケットのURLに直接アクセスして、index.htmlなどが存在するか確認する。
Webホスティングされている場合を想定。
“””
base_url = f”https://{bucket_name}.s3.{region}.amazonaws.com/” if region else f”https://{bucket_name}.s3.amazonaws.com/”
index_url = f”{base_url}index.html”
try:
response = requests.get(index_url, timeout=5)
if response.status_code == 200:
print(f”[+] Bucket Found (Web Hosting): {index_url} – Status code 200 OK.”)
return True
except requests.exceptions.RequestException:
pass # エラーは無視して次のチェックへ
# オブジェクトリスト表示を試みる(GETリクエスト)
try:
response = requests.get(base_url, timeout=5)
# 200 OK で、XML形式のバケットリストが返ってくる場合がある
if response.status_code == 200 and “
print(f”[+] Bucket Found (Listing): {base_url} – XML list detected.”)
return True
except requests.exceptions.RequestException:
pass
return False
if __name__ == “__main__”:
print(“— Checking S3 Buckets for Public Access —“)
# 1. 一般的なS3エンドポイントでバケット名を探す
print(“\n[Phase 1: Checking s3.amazonaws.com and s3.
regions = [“”, “us-east-1”, “us-west-2”, “eu-central-1”, “ap-northeast-1″] # よく使われるリージョン
found_buckets_direct = set()
for bucket_name in POTENTIAL_BUCKET_NAMES:
# 匿名でアクセスできるか試す
if check_public_s3_bucket(bucket_name):
found_buckets_direct.add(bucket_name)
for region in regions:
prefixed_bucket_name = f”{bucket_name}-{region.replace(‘-‘, ”)}” if region else bucket_name # 例: my-company-prod-apnortheast1
if check_public_s3_bucket(prefixed_bucket_name):
found_buckets_direct.add(prefixed_bucket_name)
# regionを明示して試す
if region:
if check_public_s3_bucket(f”{bucket_name}.{region}”):
found_buckets_direct.add(f”{bucket_name}.{region}”)
# 2. URLに直接アクセスしてWebホスティングなどを試す
print(“\n[Phase 2: Checking S3 URLs for Web Hosting]”)
# Phase 1で見つかったバケットを優先的にチェック
for bucket_name in list(found_buckets_direct):
for region in regions:
check_bucket_url(bucket_name, region)
# 公開されている可能性のあるバケットが、より一般的(リージョン名なし)な名前で存在するか再度確認
print(“\n[Phase 3: Re-checking general bucket names with URL access]”)
for bucket_name in POTENTIAL_BUCKET_NAMES:
if bucket_name not in found_buckets_direct: # まだチェックしていないもの
for region in regions:
if check_bucket_url(f”{bucket_name}”, region): # リージョン名なしで試す
found_buckets_direct.add(bucket_name)
if region: # リージョン名ありで試す
if check_bucket_url(f”{bucket_name}-{region.replace(‘-‘, ”)}”, region):
found_buckets_direct.add(f”{bucket_name}-{region.replace(‘-‘, ”)}”)
if check_bucket_url(f”{bucket_name}.{region}”, region):
found_buckets_direct.add(f”{bucket_name}.{region}”)
if not found_buckets_direct:
print(“\n[-] No obviously public S3 buckets found with the tested names and configurations.”)
else:
print(“\n[!] WARNING: Potentially public S3 buckets were detected. IMMEDIATE INVESTIGATION REQUIRED!”)
print(“Review the configuration of the following buckets ASAP:”)
for bucket in sorted(list(found_buckets_direct)):
print(f”- {bucket}”)
print(“\n— Scan Complete —“)
このスクリプトは、よくあるバケット名のパターンを推測し、匿名でS3 API(ListObjectsV2)を叩いたり、直接URLにアクセスしたりして、中身が見えるバケットを探し出す。もし、このスクリプトで「[+] Bucket Found」と表示されたら、それは非常に危険な状態だ。攻撃者も同じような手法で、あなたのバケットを見つけ出している可能性がある。
攻撃を防ぐための「二重の壁」:パブリックアクセスブロックとACL無効化
では、この「うっかり公開」を防ぐにはどうすればいいか?AWSでは、S3バケットの公開を防ぐための強力な仕組みを2つ提供している。これらを組み合わせることで、「意図しない公開」をほぼ不可能にできる。
1. S3バケットの「パブリックアクセスブロック」設定
2. S3バケットの「ACL(Access Control List)」の無効化
これらは、まるで家の鍵と防犯カメラ、警備員のような関係だ。片方だけでは不安だが、両方あれば安心感が格段に増す。
1. S3バケットの「パブリックアクセスブロック」設定:これが基本中の基本
「パブリックアクセスブロック」は、S3バケットやその中のオブジェクトが、インターネット経由で(つまり、AWSアカウントを持たない任何人によって)アクセスされるのを、アカウントレベルまたはバケットレベルでブロックする機能だ。
なぜこれが重要なのか?
- 意図しない公開の防止: バケットポリシーやACLの設定ミスで、誤って公開設定にしてしまっても、このブロック設定があれば、パブリックアクセスは一切拒否される。
- 設定ミスによる情報漏洩の抑制: 開発者や運用担当者が、設定を間違えたとしても、この「安全網」が機能する。
- コンプライアンス要件の遵守: 多くのコンプライアンス基準(PCI DSS, HIPAAなど)では、機密データへのパブリックアクセスを厳しく制限している。パブリックアクセスブロックは、これらの要件を満たすための強力な手段となる。
設定方法(コンソール):
1. AWSマネジメントコンソールにログインし、S3サービスを開く。
2. 対象のバケットを選択する。
3. 「アクセス権限」タブを開く。
4. 「パブリックアクセスブロック」セクションで、「編集」ボタンをクリックする。
5. 以下の4つの設定項目すべてを「ブロック」にチェックを入れる。
- 「すべてのパブリックアクセスをブロック」
- 「パブリックアクセスを許可するバケットACLをブロック」
- 「パブリックアクセスを許可するパブリックアクセス・オリジンACLをブロック」
- 「パブリックアクセスを許可するパブリックアクセス・オリジンポリシーをブロック」
6. 「変更の保存」をクリックし、確認ダイアログで「パブリックアクセスをブロック」と入力して確定する。
設定方法(AWS CLI / SDK):
この設定は、aws s3api put-public-access-block コマンドで設定するのが一般的だ。
【コピペで動くセキュアな実装サンプルコード(AWS CLI)】
!/bin/bash
——————————————————————————
AWS S3 バケットのパブリックアクセスブロック設定スクリプト
——————————————————————————
このスクリプトは、指定されたS3バケットのパブリックアクセスを
全面的にブロックするように設定します。
実行にはAWS CLIがインストールされ、適切な認証情報が設定されている必要があります。
——————————————————————————
— 設定項目 —
BUCKET_NAME=”your-target-bucket-name” # 設定したいS3バケット名に置き換えてください。
—————-
echo “— S3 Public Access Block Configuration —”
echo “Target Bucket: ${BUCKET_NAME}”
パブリックアクセスブロック設定のJSON定義
全てのパブリックアクセスをブロックし、ACLによる公開も禁止します。
PUBLIC_ACCESS_BLOCK_CONFIGURATION=$(cat <your-target-bucket-name を、実際に設定したいバケット名に必ず置き換えてください。また、AWS CLIが正しく設定されていることを確認してください。
2. ACLの無効化:レガシーなアクセス制御を排除する
ACL(Access Control List)は、S3バケットやオブジェクトに対するアクセス権限を細かく設定できる、S3の初期から存在する機能です。しかし、ACLは設定が複雑で、意図しない公開につながりやすいという欠点があります。
なぜACLを無効化すべきなのか?
- 設定の複雑さ: ACLは、ユーザー単位、グループ単位で権限を設定でき、バケットとオブジェクトで別々に管理するため、全体像を把握するのが難しい。
- 意図しない公開の温床: 特に、レガシーな設定や、過去の開発者が残したACL設定が、誤ってパブリックアクセスを許可しているケースが後を絶たない。
- パブリックアクセスブロックとの併用: パブリックアクセスブロック設定の「パブリックアクセスを許可するバケットACLをブロック」と「パブリックアクセスを許可するパブリックアクセス・オリジンACLをブロック」を有効にすると、ACLによるパブリックアクセスはブロックされます。しかし、ACL自体を無効化することで、この「リスクの温床」を根本から排除できます。
- AWSの推奨: AWSは、ACLではなく、バケットポリシーやIAMポリシーによるアクセス制御を推奨しています。ACLは、特定のユースケース(例: クロスアカウントでのオブジェクト書き込み権限委譲)を除き、原則として無効化することが推奨されています。
ACLの無効化方法(コンソール):
1. S3サービスで対象のバケットを選択する。
2. 「アクセス権限」タブを開く。
3. 「ACL」セクションで、「編集」ボタンをクリックする。
4. 「ACLを無効にする」にチェックを入れる。
5. 「変更の保存」をクリックする。
ACLの無効化方法(AWS CLI / SDK):
ACLを無効化するには、バケットの BucketOwnershipControls 設定を BucketOwnerEnforced に設定します。これにより、ACLが無効化され、バケット所有者(つまり、バケットを作成したAWSアカウント)のみがオブジェクトを所有し、ACLによるアクセス権限の変更ができなくなります。
【コピペで動くセキュアな実装サンプルコード(AWS CLI)】
!/bin/bash
——————————————————————————
AWS S3 バケットのACL無効化スクリプト (BucketOwnerEnforced設定)
——————————————————————————
このスクリプトは、指定されたS3バケットのACLを無効化し、
オブジェクトの所有権をバケット所有者に強制します。
これにより、ACLによる意図しないアクセス権限の付与を防ぎます。
実行にはAWS CLIがインストールされ、適切な認証情報が設定されている必要があります。
——————————————————————————
— 設定項目 —
BUCKET_NAME=”your-target-bucket-name” # 設定したいS3バケット名に置き換えてください。
—————-
echo “— S3 ACL Disabled (BucketOwnerEnforced) Configuration —”
echo “Target Bucket: ${BUCKET_NAME}”
BucketOwnerEnforced設定のJSON定義
ACLを無効にし、バケット所有者によるオブジェクト所有を強制します。
BUCKET_OWNERSHIP_CONTROLS=$(cat <your-target-bucket-name を、実際に設定したいバケット名に必ず置き換えてください。
パブリックアクセスブロックとACL無効化の組み合わせが最強!
これらの設定を組み合わせることで、S3バケットの意図しない公開を、ほぼ確実に防ぐことができます。
- パブリックアクセスブロック: 「インターネットからのアクセス」という扉自体を閉める。
- ACL無効化(BucketOwnerEnforced): 「鍵」の管理をバケット所有者に一本化し、複雑な権限設定(ACL)によるミスを防ぐ。
【設定例:NginxでのS3バケットへのアクセス制御(参考)】
もし、S3バケットをWebサイトのバックエンドとして利用しており、特定のWebサーバー(例: Nginx)からのみアクセスさせたい、という要件がある場合。ACLやバケットポリシーでIAMロールなどを用いて制御するのが基本ですが、ここではNginx側でS3へのアクセスを制限する(あるいは、S3側でNginxからのアクセスのみを許可する)ための考え方を示します。
S3バケットポリシー例(Nginxからのアクセスのみ許可):
{
“Version”: “2012-10-17”,
“Statement”: [
{
“Sid”: “AllowNginxAccess”,
“Effect”: “Allow”,
“Principal”: {
“AWS”: “arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ROLE_NAME_USED_BY_NGINX”
},
“Action”: [
“s3:GetObject”,
“s3:ListBucket”
],
“Resource”: [
“arn:aws:s3:::your-target-bucket-name”,
“arn:aws:s3:::your-target-bucket-name/”
]
}
]
}
Nginx設定例(S3へのリクエストをプロキシする際の例):
この例では、NginxがAWS SDKなどを使ってS3にアクセスする際の認証情報(IAMロールなど)が適切に設定されていることを前提とします。
nginx.conf (httpブロック内、またはserverブロック内)
S3バケットへのリクエストをプロキシするための設定
実際には、アプリケーションコードがS3にアクセスする際に
このNginxサーバーを経由するようなアーキテクチャになります。
または、Nginx自体がS3からファイルを読み込む場合。
location /s3_files/ {
# NginxがS3にアクセスするための設定例
# AWS SDK for Nginx (ngx_http_aws_sdk_module など、もしあれば)
# あるいは、AWS CLIやSDKをバックグラウンドで実行してファイルを取得し、
# それをNginxから配信するようなカスタム実装が必要になる場合もあります。
# ここでは、直接S3へアクセスするのではなく、
# アプリケーションがS3から取得したファイルを
# Nginx経由で配信するケースを想定した擬似的な設定です。
# 例: S3から直接取得するのではなく、アプリケーションが取得したファイルを
# ローカルディレクトリに保存し、それをNginxで配信する。
alias /path/to/locally/cached/s3/files/;
autoindex on; # ファイルリストを表示する場合
# 認証やアクセス制御はこのlocationブロック内で行う
# (例: IPアドレス制限、Basic認証など)
# allow 192.168.1.0/24;
# deny all;
# S3バケットポリシーやIAMロールでアクセス制御するのが基本ですが、
# Nginx側でも追加のセキュリティレイヤーを設けることは可能です。
}
補足: S3バケット自体をWebサイトとしてホスティングする場合
この場合、バケットポリシーやACLでパブリックアクセスを許可する必要がありますが、
パブリックアクセスブロック設定を「無効」にし、ACLも「有効」にする必要があります。
しかし、これは非常にリスクが高いため、基本的には推奨されません。
通常は、CloudFrontなどのCDNと組み合わせて、オリジンとしてS3を指定する方が安全です。
※重要: 上記Nginxの設定例は、S3へのアクセス制御を「どのように考えるか」という概念を示すためのものです。実際のS3へのアクセス制御は、AWS IAMポリシーやS3バケットポリシーで行うのが最も安全で推奨される方法です。Nginx自体がS3に直接アクセスする(SDKモジュールなど)場合でも、そのNginxを実行するIAMロールに最小限の権限のみを付与することが鉄則です。
まとめ:今日の教訓
- S3バケットの公開は、攻撃者にとって格好の的。
- 「パブリックアクセスブロック」は、意図しない公開を防ぐための必須設定。必ず全項目をブロックせよ。
- 「ACLの無効化」(BucketOwnerEnforced)は、レガシーな権限設定によるリスクを排除し、管理をシンプルにする。
- この2つの設定を組み合わせることで、S3バケットのセキュリティは格段に向上する。
これらの設定は、一度行えば継続的に効果を発揮する「防御の礎」だ。日々の運用で忙しいのは理解できるが、こうした基本的なセキュリティ対策を怠ることが、後々、取り返しのつかない事態を招く。
さあ、みんなの担当しているS3バケットの設定を見直してくれ。もし、まだこれらの設定が有効になっていないなら、今日すぐにでも対応しよう。不明な点があれば、いつでも俺に聞きに来い。俺たちのプロダクトと、そして何よりも、ユーザーのデータを守り抜くんだ。それが、一流のエンジニアの仕事だ。
コメント