Claude CodeでS3のパブリックアクセスを点検 — 読み取りだけで公開バケットを洗う
全バケットのBlock Public Access、ポリシーの公開判定、Principalが「*」のAllowを問い合わせて1枚の表にする読み取り専用スクリプトの作り方。Claude Codeに許す範囲と表の読み違えやすい点も示します。
S3バケットが公開状態かどうかは、1つの設定を見ても決まりません。Block Public Accessの4項目、バケットポリシーの中身、ACLの3か所が重なって結果を決めるためです。バケットが数十を超えると、コンソールで1つずつ開く方法は現実的でなくなります。
Claude Codeに任せられるのは、全バケットに同じ問い合わせを回して表にするスクリプトを書き、結果を読んで「どれが本当に危ないか」を仕分けるところです。AWS側の設定変更は任せません。この記事では、読み取り系のAPIだけを呼ぶ構成にして、権限ルールとIAMの両方で書き込みを塞ぐ手順を順に示します。
公開判定は4つの問い合わせに分けられる
S3の公開状態は、次の4つの問い合わせに分けると見通しが立ちます。それぞれ見えるものが違い、1つだけでは判定が漏れます。
| 問い合わせ | 必要な権限 | 分かること | 分からないこと |
|---|---|---|---|
get-public-access-block | 必要な権限s3:GetBucketPublicAccessBlock | 分かることバケット単位の4項目の設定 | 分からないことアカウント側の設定、実際に公開かどうか |
get-bucket-policy-status | 必要な権限s3:GetBucketPolicyStatus | 分かることポリシーが公開と判定されるか(IsPublic) | 分からないことどの文が原因か |
get-bucket-policy | 必要な権限s3:GetBucketPolicy | 分かることポリシー本文、Principalが"*"の文 | 分からないことS3がその文を公開と判定するか |
get-bucket-acl | 必要な権限s3:GetBucketAcl(ACL側の権限はREAD_ACP) | 分かることACLの付与先 | 分からないことポリシー経由の公開 |
get-public-access-blockの説明には、返るのはバケットレベルの設定だけで、実際の挙動を知るにはアカウントレベルの設定(組織レベルのポリシーを引き継ぐことがある)も合わせて見る必要があると書かれています。S3は、バケットとバケット所有者のアカウントの設定を比べて、厳しい側の組み合わせを使います。バケット単位が「無効」でも、アカウント側が有効なら保護されます。
アカウント単位の設定はaws s3control get-public-access-block --account-id <ID>で取ります。必要な権限はs3:GetAccountPublicAccessBlockです。バケットを回すスクリプトの外に1回だけ出せば足ります。
Block Public Accessの4項目が止めるもの
4項目は独立していて、組み合わせは自由です。何を止めて何を止めないかを押さえると、表の読み方が変わります。
BlockPublicAcls: 公開ACLを付ける呼び出し(PutBucketAcl、PutObjectAcl、公開ACL付きのPutObject)を失敗させるIgnorePublicAcls: すでに付いている公開ACLを無視させる。ACL自体は消えないBlockPublicPolicy: 公開になるバケットポリシーをPutBucketPolicyで入れる呼び出しを拒否するRestrictPublicBuckets: 公開ポリシーを持つバケットへのアクセスを、AWSのサービスプリンシパルと所有者アカウントの許可済みユーザーに絞る
ここで大事なのは、BlockPublicAclsとBlockPublicPolicyは「これから入れる設定」を止めるだけで、既存のポリシーやACLは変えないことです。ドキュメントには、設定を外すと公開ポリシーやACLを持つバケットが再び公開されるとも書かれています。点検で4項目がオンになっているバケットに公開ポリシーが残っていることは、実際に起こり得ます。
Claude Codeに許す範囲を先に決める
AWSに触る作業では、Claude Codeに何を勝手に実行させないかを先に決めます。権限ルールは「拒否、確認、許可」の順に評価され、最初に当たったもので結果が決まります。広い拒否ルールは、狭い許可ルールで例外にできません。
この性質を使って、書き込み系を拒否に、今回のスクリプトが呼ぶ読み取り系を許可に置きます。
{
"permissions": {
"allow": [
"Bash(aws s3api list-buckets *)",
"Bash(aws s3api get-public-access-block *)",
"Bash(aws s3api get-bucket-policy-status *)",
"Bash(aws s3api get-bucket-policy *)",
"Bash(aws s3api get-bucket-acl *)",
"Bash(aws s3control get-public-access-block *)",
"Bash(aws sts get-caller-identity *)",
"Bash(bash scripts/audit-s3-public.sh *)"
],
"deny": [
"Bash(aws s3api put-*)",
"Bash(aws s3api delete-*)",
"Bash(aws s3control put-*)",
"Bash(aws s3control delete-*)",
"Bash(aws s3 rm *)",
"Bash(aws s3 rb *)"
]
}
}*の位置には決まりがあります。許可ルールは、サブコマンドまで書いてから*を置きます。Bash(aws *)のように手前で*を使うと、書き込み系まで通ってしまうためです。
このルールが守らないもの
ルールが照合するのは、Claudeが書いたコマンドの文字列です。bash scripts/audit-s3-public.shを許可すると、スクリプトの中で呼ばれるawsの個々の行は、ルールの照合には出てきません。スクリプトを許可したあとに中身が書き換わると、ルールを素通りして別のAPIが呼ばれます。
同じ理由で、拒否ルールにも抜け道があります。/usr/local/bin/aws s3api put-bucket-policy ...やbash -c '...'のような書き方は、Bash(aws s3api put-*)に一致しません。ドキュメントにも、拒否ルールは同じプログラムの別の呼び方までは止めないと書かれています。
そこで、最後の壁はAWS側に置きます。点検用に専用のプロファイルを作り、読み取りの権限だけを付けます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPolicy",
"s3:GetBucketAcl",
"s3:GetAccountPublicAccessBlock"
],
"Resource": "*"
}
]
}s3:ListAllMyBucketsはlist-bucketsに必要なアクションです。このプロファイルなら、Claudeがルールをすり抜けたコマンドを書いても、変更系の呼び出しはAWSが拒否します。Claude Codeの権限ルールの考え方は、IAM最小権限をAccess Analyzerから作る手順でも同じ型で扱っています。
全バケットを回すスクリプト
次のスクリプトをscripts/audit-s3-public.shとして置きます。Claude Codeに「上の4つの問い合わせを全バケットに回して、TSVに出す読み取り専用のシェルを書いて」と頼めば、近い形が返ります。出てきたコードは、呼んでいるAPIがget-とlist-だけかを目で読んでから許可してください。
#!/usr/bin/env bash
# S3の公開状態を点検する。呼ぶのは get-/list- 系の読み取りAPIだけ。
set -u
out="${1:-s3-public-audit.tsv}"
printf 'bucket\tbpa\tpolicy_is_public\tallow_star\tpublic_acl\n' > "$out"
s3api() { aws s3api "$@" 2>/dev/null; }
bpa_filter='[.PublicAccessBlockConfiguration[]]
| if length == 4 and all then "all-on" else "partial" end'
star_filter='
def star: . == "*" or (type == "array" and any(.[]; . == "*"));
(.Statement | if type == "array" then . else [.] end)
| map(select(.Effect == "Allow" and (
((.Principal | type) == "string" and (.Principal | star)) or
((.Principal | type) == "object" and (.Principal.AWS | star)))))
| map((.Sid // "-") + "("
+ (if .Condition then "条件あり" else "条件なし" end) + ")")
| join(";")'
acl_filter='[.Grants[]?.Grantee.URI // empty
| select(endswith("/AllUsers") or endswith("/AuthenticatedUsers"))]
| length'
aws s3api list-buckets --query 'Buckets[].Name' --output text \
| tr '\t' '\n' |
while read -r b; do
[ -z "$b" ] && continue
# 1. バケット単位のBlock Public Access(4項目すべてtrueか)
if cfg=$(s3api get-public-access-block --bucket "$b" --output json); then
bpa=$(jq -r "$bpa_filter" <<<"$cfg")
else
bpa="none-or-denied"
fi
# 2. バケットポリシーが公開と判定されるか
pol=$(s3api get-bucket-policy-status --bucket "$b" \
--query 'PolicyStatus.IsPublic' --output text) || pol="no-policy-or-denied"
# 3. Principal が "*" の Allow 文
if p=$(s3api get-bucket-policy --bucket "$b" --query Policy --output text); then
stars=$(jq -r "$star_filter" <<<"$p")
[ -z "$stars" ] && stars="-"
else
stars="no-policy-or-denied"
fi
# 4. ACL の AllUsers / AuthenticatedUsers
if acl=$(s3api get-bucket-acl --bucket "$b" --output json); then
pacl=$(jq -r "$acl_filter" <<<"$acl")
else
pacl="denied"
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$b" "$bpa" "$pol" "$stars" "$pacl" >> "$out"
done
# アカウント単位の設定は別ファイルに残す
acct=$(aws sts get-caller-identity --query Account --output text)
aws s3control get-public-access-block --account-id "$acct" --output json \
2>/dev/null | jq -c '.PublicAccessBlockConfiguration' > s3-account-bpa.json設計上の判断が4つあります。
get-bucket-policyの出力は--output textで受ける。PolicyはJSONを文字列として持つ項目で、textで取るとエスケープが外れたJSONがそのまま出る。jqに渡す前にfromjsonは要らないStatementが配列でない場合を拾う。ポリシーのStatementは、1文のときオブジェクトで書けるため、if type == "array"で包んでいるPrincipalの書き方を2通り見る。"Principal": "*"と"Principal": {"AWS": "*"}(配列の場合も含む)は、同じ意味で別の形になるConditionの有無だけを表に出す。条件の中身までは判定しない。次の節で触れるように、条件があるから安全とは言えない
実行は次の1行です。
AWS_PROFILE=s3-audit-readonly bash scripts/audit-s3-public.sh s3-public-audit.tsvAWS_PROFILE=を前置きした形は、Bash(bash scripts/audit-s3-public.sh *)の許可ルールには一致しない場合があります。許可ルールが読み飛ばせる環境変数の代入は限られており、それ以外の変数を前置きすると許可は効きません。プロファイルは、Claudeを起動するシェルの環境で先に設定しておく運用が確実です。
表の読み方 — 3バケットのダミー応答で見る
ダミーの応答を返すawsコマンドに差し替えて、このスクリプトを流したときの出力が次の表です。実際の環境ではバケット名と値が変わります。
| bucket | bpa | policy_is_public | allow_star | public_acl |
|---|---|---|---|---|
| logs-bkt | bpaall-on | policy_is_publicFalse | allow_star- | public_acl0 |
| web-assets | bpapartial | policy_is_publicTrue | allow_starPublicRead(条件なし);-(条件あり) | public_acl0 |
| legacy-data | bpanone-or-denied | policy_is_publicno-policy-or-denied | allow_starno-policy-or-denied | public_acl1 |
1行ずつ読むと、次のように分かれます。
- logs-bkt: 4項目がすべてオンで、ポリシーも公開ではない。点検の対象外にできる
- web-assets: 4項目の一部がオフで、ポリシーが公開と判定されている。
Principalが"*"の文が2つあり、1つ目は条件なしなので、公開の原因はこの文 - legacy-data: 設定が取れない。ACLに
AllUsersかAuthenticatedUsersの付与が1つある。これが最も要確認の行
読む順序は、policy_is_publicがTrueの行、public_aclが0でない行、bpaがall-onでない行の順が効率的です。web-assetsのように条件なしの"*"が見つかった行は、静的サイトの配信用に意図して公開しているのか、誰かが緩めたのかを、作成者に確認する段階に進みます。
条件ありの"*"は公開と限らず、非公開とも限らない
web-assetsの2つ目の文は、Principalが"*"でもaws:SourceVpcの条件が付いています。S3が公開と見なすかどうかは、ドキュメントの「公開」の定義で決まります。バケットポリシーは、まず公開と仮定して評価が始まり、固定値の条件キーで絞られていれば非公開と判定されます。
非公開と判定される条件キーには、aws:SourceVpc、aws:SourceVpce、aws:SourceArn、aws:SourceAccount、aws:SourceIp(CIDRブロック)などがあります。値にワイルドカードやポリシー変数が入ると固定値と見なされません。また、aws:SourceIpが0.0.0.0/1のように広すぎる範囲を指すものは、公開として扱われます。
つまり、条件の有無だけで安全を判断できません。このため、スクリプトはpolicy_is_publicをS3自身の判定として別の列に残しています。allow_starは原因の文を探す手がかり、policy_is_publicは結論という役割分担です。
結果をClaudeに読ませて、対処案を作らせる
表ができたら、TSVをClaude Codeに読ませます。得意なのは、行ごとの原因の切り分けと、変更前に人間が確かめる項目の一覧化です。次のような依頼文が出発点になります。
s3-public-audit.tsv と s3-account-bpa.json を読んで、次を表にしてください。
- policy_is_public が True の行は、allow_star の「条件なし」の文を原因として挙げる
- public_acl が 0 でない行は、IgnorePublicAcls が効いていないかを分けて書く
- bpa が none-or-denied の行は、「設定が無い」と「権限が足りない」を区別できないと明記する
- AWS の設定を変更するコマンドは実行せず、案としてだけ書く3つ目の指示が必要なのは、スクリプトがnone-or-deniedで2つの状態を束ねているからです。バケットにBlock Public Accessの設定が無い場合と、呼び出しが権限不足で失敗した場合は、2>/dev/nullでエラーを捨てているため、表では区別がつきません。気になる行は、aws s3api get-public-access-block --bucket <名前>を単独で実行して、エラーの文面を見ます。
対処案のうち、Block Public Accessをオンにする操作には、実行前の下調べが要ります。ドキュメントは、設定を入れる前にアプリケーションが公開アクセス無しで動くかを確かめるよう案内しています。静的サイトのホスティングのように公開が必要な用途では、必要な設定だけを個別に調整します。
変更の実行は人が行う分担にします。拒否ルールにput-*を入れてあるので、Claudeが変更コマンドを書いても止まります。put-public-access-blockを実行する前には、対象バケットの現在の設定をget-public-access-blockで保存しておくと、戻す先が残ります。
回したあとに起きやすい取りこぼし
- 別アカウントのバケットは出ない。
list-bucketsは呼び出したアカウントのバケットだけを返す。アカウントが複数あるなら、アカウントごとにプロファイルを替えて回す - アクセスポイント経由の公開は見ていない。アクセスポイントはバケットとは別にBlock Public Accessの設定とポリシーを持ち、操作は
s3controlで行う。アクセスポイントを使っているアカウントでは、別途確認が要る - オブジェクト単位のACLは対象外。今回の4つはバケットレベルの問い合わせで、個々のオブジェクトの公開は拾えない
- ディレクトリバケットは別扱い。
get-public-access-blockとget-bucket-policy-statusは、ディレクトリバケットでは使えない - AWSの他の仕組みと見解が割れることがある。IAM Access Analyzer for S3は、ACL、バケットポリシー、アクセスポイントのポリシーを見て、公開または共有されているバケットを知らせる。S3のBlock Public Accessと評価が食い違うことがあり、サポートされないアクションを含むポリシーでその差が出る例が挙げられている
最後の点から、このスクリプトはAccess Analyzerの代わりではなく、手元で全体を一覧し、原因の文に当たりをつけるための道具だと位置付けるのが妥当です。継続的な監視は、組織レベルのBlock Public Accessと合わせてAWS側の仕組みに任せ、スクリプトは棚卸しの定期実行に使う分担が考えられます。
点検を繰り返すときの運用
一度きりの点検で終わらせず、差分を見る運用にすると効果が上がります。出力のTSVを日付付きで保存しておけば、diffで前回との変更を見られます。diffはClaude Codeが確認なしで実行できる読み取りコマンドの1つです。
diff s3-public-audit-prev.tsv s3-public-audit.tsv差分に新しく出たpolicy_is_publicのTrueは、前回の点検以降にバケットポリシーが緩められた手がかりになります。ライフサイクルルールの設計など、バケットのコスト側の作業はS3ライフサイクルルールをStorage Lensから書く手順で扱っています。公開バケットを洗い出したあとに、不要なデータの削除設計へ進む流れです。
出力のTSVにはバケット名が並びます。社内向けのリポジトリでも、コミットの対象には含めない運用が安全です。.gitignoreにs3-public-audit*.tsvとs3-account-bpa.jsonを足しておくと、誤ってコミットする事故を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/permissions
- docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-acce…
- docs.aws.amazon.com/cli/latest/reference/s3api/get-public-access-block.html
- docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-policy-status.html
- docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-policy.html
- docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-acl.html
- docs.aws.amazon.com/cli/latest/reference/s3control/get-public-access-block.html