PublicなS3バケットとオブジェクトの棚卸しをするスクリプトを作ってみた(バケットポリシー・ACL編)

こんにちは。AWS事業本部トクヤマシュンです。

Publicに公開されているS3バケットとオブジェクトの棚卸しをするPythonスクリプトを作成する機会があったので、紹介します。

前提:調査対象としている公開方法について

2023年6月時点で、S3バケットおよびオブジェクトをパブリックに公開する方法は下記があります。

  • バケットポリシー
  • ACL
    • バケットACLとオブジェクトACLの2つに分かれる
  • アクセスポイントポリシー
  • マルチリージョンアクセスポイントポリシー
  • Object Lambdaアクセスポイントポリシー

今回は利用頻度が高いと想定されるバケットポリシーACL(バケットACLとオブジェクトACL)に絞って調査するスクリプトとしました。
他の公開設定については今後の課題としたいと思います。

スクリプトによる棚卸しの流れ

下記の流れで棚卸しを行います。

  • スクリプト実行
    • 2つの調査結果ファイルが出力されます
      • バケット公開状態一覧ファイル
        • public_bucket_status_list_アカウントID.tsv
      • オブジェクトACLによる公開オブジェクト一覧ファイル
        • public_objects_by_object_acl_list_アカウントID.tsv
  • バケット公開状態一覧ファイル確認
    • バケットポリシー公開状態が「公開中」のものに関して、Publicなバケットポリシーを確認
    • バケットACL公開状態が「公開中」のものに関して、PublicなバケットACLを確認
  • オブジェクトACLによる公開オブジェクト一覧ファイル確認
    • PublicなオブジェクトACLを確認
  • Publicなバケットポリシー、バケットACL、オブジェクトACLの意図を確認

それぞれ説明します。

スクリプト実行

前提

今回、スクリプトの実行にあたって必要なIAM権限は付与されているものとします。

実行するPythonスクリプトの内容

Pythonスクリプトは下記の通りです。

s3_public_status_check.py

import botocore
import boto3
import logging
from typing import List

def get_public_objects_by_acl(bucket_name: str) -> List:
    """対象のバケット内でオブジェクトACLによって公開されているオブジェクト一覧を取得

    :param str bucket_name: 公開オブジェクト一覧取得対象バケット
    :return: 公開オブジェクト一覧を格納したリスト
    """
    #オブジェクト一覧リスト初期化
    public_objects_by_acl = []
    
    s3_resource = boto3.resource("s3")
    bucket = s3_resource.Bucket(bucket_name)
    objects = bucket.objects.all()
    
    #バケット内の全オブジェクトを対象に調査
    for chk_object in objects:
        try:
            #ACLに設定されているGrantsを取得
            object_acl_grants = chk_object.Acl().grants
            
            #TypeにGroupが設定されているものを取得
            acl_type_group = [ x["Grantee"] for x in object_acl_grants if x["Grantee"]["Type"] == "Group"]
            if len(acl_type_group) > 0:
                for chk_acl in acl_type_group:
                    #AllUsersもしくはAuthenticatedUsersに対するACLが存在するオブジェクトをリストにappend
                    if chk_acl["URI"] in ["http://acs.amazonaws.com/groups/global/AllUsers","http://acs.amazonaws.com/groups/global/AuthenticatedUsers"]:
                        public_objects_by_acl.append({"bucket_name": bucket_name, "key": chk_object.key, "acl": object_acl_grants})
                        break
        except botocore.exceptions.ClientError as e:
            logger.info(e)
    return(public_objects_by_acl)

if __name__ == "__main__": 

    #ログ設定
    logging.basicConfig(level=logging.ERROR)
    logger = logging.getLogger()
    
    #アカウントID取得
    account_id = boto3.client("sts").get_caller_identity()["Account"]

    #アカウントのパブリックアクセスブロック設定取得 
    s3_control = boto3.client("s3control")
    account_public_access_block = s3_control.get_public_access_block(
        AccountId = account_id,
    )["PublicAccessBlockConfiguration"]

    #リスト初期化
    bucket_list = []
    chk_public_objects_by_acl = []

    #バケット一覧取得
    s3 = boto3.client("s3")
    try: 
        buckets = s3.list_buckets().get("Buckets")
    except botocore.exceptions as e:
        raise e
    
    #バケットごとに処理
    for bucket in buckets:
        bucket_name = bucket["Name"]

        #バケットのオブジェクト所有者設定取得
        try:
            bucket_ownership_controls = s3.get_bucket_ownership_controls(
                Bucket = bucket_name
            )["OwnershipControls"]["Rules"][0]["ObjectOwnership"]
        except botocore.exceptions.ClientError as e:
            logger.info(e)
            bucket_ownership_controls = "ObjectWriter"

        #バケットのパブリックアクセスブロック設定取得
        try:
            bucket_public_access_block = s3.get_public_access_block(
                Bucket = bucket_name
            )["PublicAccessBlockConfiguration"]
        except botocore.exceptions.ClientError as e:
            logger.info(e)
            bucket_public_access_block = {'BlockPublicAcls': False, 'IgnorePublicAcls': False, 'BlockPublicPolicy': False, 'RestrictPublicBuckets': False}

        #バケットポリシーのIsPublicプロパティ取得
        try:
            bucket_policy_status = s3.get_bucket_policy_status(
                Bucket = bucket_name
            )["PolicyStatus"]["IsPublic"]
        except botocore.exceptions.ClientError as e:
            logger.info(e)
            bucket_policy_status = None

        #バケットポリシー取得
        try:
            bucket_policy = s3.get_bucket_policy(
                Bucket = bucket_name
            )["Policy"]
        except botocore.exceptions.ClientError as e:
            logger.info(e)
            bucket_policy = None

        #バケットACL取得
        try:
            bucket_acl = s3.get_bucket_acl(
                Bucket = bucket_name
            )["Grants"]
        except botocore.exceptions.ClientError as e:
            logger.info(e)
            bucket_acl = None

        # バケットポリシーによるバケットの公開状態を設定
        if account_public_access_block["RestrictPublicBuckets"] == False and bucket_public_access_block["RestrictPublicBuckets"] == False:
            if bucket_policy_status == True:
                public_status_by_policy = "公開中"
            else:
                public_status_by_policy = "公開可能"
        else:
            public_status_by_policy = "非公開"
        
        # バケットACLによるバケットの公開状態を設定
        if bucket_ownership_controls != "BucketOwnerEnforced" and account_public_access_block["IgnorePublicAcls"] == False and bucket_public_access_block["IgnorePublicAcls"] == False:
            public_status_by_acl = "公開可能"
            acl_type_group = [ x["Grantee"] for x in bucket_acl if x["Grantee"]["Type"] == "Group"]
            if len(acl_type_group) > 0:
                for chk_acl in acl_type_group:
                    if chk_acl["URI"] in ["http://acs.amazonaws.com/groups/global/AllUsers","http://acs.amazonaws.com/groups/global/AuthenticatedUsers"]:
                        public_status_by_acl = "公開中"
                        break
        else:
            public_status_by_acl = "非公開"
 
        #バケットと、公開状態などの情報をリストに追加
        bucket_list.append(
            {
                "bucket_name" : bucket_name,
                "public_status_by_policy" : public_status_by_policy,
                "public_status_by_acl" : public_status_by_acl,
                "bucket_ownership_controls" : bucket_ownership_controls,
                "bucket_public_access_block" : bucket_public_access_block,
                "bucket_policy_status" : bucket_policy_status,
                "bucket_policy" : bucket_policy,
                "bucket_acl" : bucket_acl
            }
        )

        #オブジェクトACLで公開されているオブジェクトのバケット名とオブジェクト一覧をリストに結合
        if public_status_by_acl in ["公開中","公開可能"]:
            chk_public_objects_by_acl.extend(get_public_objects_by_acl(bucket_name))

    #バケット名と公開状態を出力
    path_bucket_list = "public_bucket_status_list_" + account_id + ".tsv"
    with open(path_bucket_list,encoding = "utf-8", mode = "w") as f:
        f.write("バケット名\tバケットポリシー公開状態\tバケットポリシー\tバケットACL公開状態\tバケットACL\n")
        for bucket in bucket_list:
            f.write("{}\t{}\t{}\t{}\t{}\n".format(bucket["bucket_name"],bucket["public_status_by_policy"],bucket["bucket_policy"],bucket["public_status_by_acl"], bucket["bucket_acl"]))
    
    #公開オブジェクトを出力
    path_public_objects_by_acl = "public_objects_by_object_acl_list_" + account_id + ".tsv"
    with open(path_public_objects_by_acl,encoding = "utf-8", mode = "w") as f:
        f.write("バケット名\t公開オブジェクトキー\tオブジェクトACL\n")
        for public_objects_by_acl in chk_public_objects_by_acl:
            f.write("{}\t{}\t{}\n".format(public_objects_by_acl["bucket_name"],public_objects_by_acl["key"],public_objects_by_acl["acl"]))

本スクリプトは、アカウント内に存在するすべてのバケットに対し、バケットポリシー・バケットACL・オブジェクトACLの公開状態を判定して出力します。
今回の肝となる公開状態の判定ロジックは下記フローの通りとしています。
(この判定だとAWSのコンソール画面上で表示されるステータスと一部乖離もあるのですが、実際の公開状態をより正確に反映するため下記のロジックとしています)

バケットポリシーによる公開状態の判定フロー

バケットACLによる公開状態の判定フロー

バケットポリシー公開状態とバケットACL公開状態について、各ステータスの意図する内容は下記の通りです。

ステータス バケットポリシー公開状態 バケットACL公開状態 備考
公開中 バケットもしくはオブジェクトに対して、有効かつPublicなバケットポリシーが存在 ・バケットに対してPublicなバケットACLが存在
・オブジェクトは、PublicなオブジェクトACLが存在する場合には公開中
・Publicなバケットポリシーの意図を確認要
・PublicなバケットACLの意図を確認要
公開可能 バケットもしくはオブジェクトに対して、Publicなバケットポリシーは存在しないが、追加すれば公開可能 ・バケットに対してPublicなバケットACLは存在しないが、追加すれば公開可能
・オブジェクトは、PublicなオブジェクトACLが存在する場合には公開中
オブジェクトACLにより公開中のオブジェクトが存在する可能性があるので注意
非公開 バケットもしくはオブジェクトに対して、Publicなバケットポリシーは無効化され、すべて非公開 バケットもしくはオブジェクトに対して、PublicなバケットACL・オブジェクトACLは無効化され、すべ非公開 Publicなバケットポリシー・バケットACLを追加したとしても、バケットやオブジェクトは公開されない

オブジェクトACLによる公開状態の判定フロー

オブジェクトACL公開状態について、各ステータスの意図する内容は下記の通りです。

ステータス オブジェクトACL公開状態 備考
公開中 オブジェクトは公開中 PublicなオブジェクトACLの意図を確認要
非公開 オブジェクトは非公開 非公開オブジェクトの中には、PublicなオブジェクトACLを設定すれば公開可能なものも含む。

本プログラムの実行結果として、2つのファイルが出力されます。

  • バケット公開状態一覧ファイル(public_bucket_status_list_アカウントID.tsv)
    • 各バケットのバケットポリシーおよびバケットACLによる公開状態とそれぞれの設定内容を記載
  • オブジェクトACL公開による公開オブジェクト一覧ファイル(public_objects_by_object_acl_list_アカウントID.tsv)
    • オブジェクトACL公開状態が「公開中」であるオブジェクト一覧とオブジェクトACLの設定内容を記載

実行手順

AWS Cloudshellで実行する場合の手順を示します。

  • 対象アカウントのマネージメントコンソールにログイン
  • AWS CloudShellを起動
  • 「s3_public_status_check.py」をアップロード
    • AWS CloudShell右上の「Actions」→「Upload file」でアップロード可能
  • $ python3 s3_public_status_check.pyコマンドを実行
  • $ lsコマンドを実行し、下記2ファイルがカレントディレクトリに存在することを確認
    • 「public_bucket_status_list_アカウントID.tsv」
    • 「public_objects_by_object_acl_list_アカウントID.tsv」
  • 上記2ファイルをダウンロード
    • AWS CloudShell右上の「Actions」→「Download file」でダウンロード可能

ローカルで実行可能な環境があれば、そちらで実行いただいても問題ないです。
スクリプト実行は以上です。

バケット公開状態一覧ファイル確認

ダウンロードしたtsvファイル:「public_bucket_status_list_アカウントID.tsv」を確認します。
ここには各バケットのバケットポリシーおよびバケットACLによる公開状態とそれぞれの設定内容が記載されています。
下記にファイルのサンプルを示します。

public_bucket_status_list_123456789012.tsv

バケット名	バケットポリシー公開状態	バケットポリシー	バケットACL公開状態	バケットACL
test-public-123456789012-2	公開可能	None	非公開	[{'Grantee': {'DisplayName': 'dummy-12345', 'ID': 'dummy', 'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}]
test-public-123456789012	公開中	{"Version":"2012-10-17","Id":"Policydummy","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:getobject","Resource":"arn:aws:s3:::test-public-123456789012/test/*"}]}	公開可能	[{'Grantee': {'DisplayName': 'dummy-12345', 'ID': 'dummy', 'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}

ファイルには下記が記載されています。

  • バケット名
  • バケットポリシー公開状態
  • バケットポリシー
  • バケットACL公開状態
  • バケットACL

tsvファイルから下記を確認します。

  • バケットポリシー公開状態が「公開中」のものに関して、バケットポリシーを確認
  • バケットACL公開状態が「公開中」のものに関して、バケットACLを確認

バケット公開状態一覧ファイル確認は以上です。

オブジェクトACLによる公開オブジェクト一覧ファイル確認

ダウンロードしたtsvファイル:「public_objects_by_object_acl_list_アカウントID.tsv」を確認します。
ここにはオブジェクトACL公開状態が「公開中」であるオブジェクト一覧とオブジェクトACLの設定内容が記載されています。
下記にファイルのサンプルを示します。

public_bucket_status_list_123456789012.tsv

バケット名	公開オブジェクトキー	オブジェクトACL
test-public-123456789012	sample.txt	[{'Grantee': {'DisplayName': 'dummy-12345', 'ID': 'dummy', 'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}, {'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'READ'}]
test-public-123456789012	test/sample.json	[{'Grantee': {'DisplayName': 'dummy-12345', 'ID': 'dummy', 'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}, {'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'READ'}]

ファイルには下記が記載されています。

  • バケット名
  • 公開オブジェクトキー
  • オブジェクトACL

バケット公開状態一覧ファイルはステータスが公開可能または非公開であるバケットも含んでいましたが、
今回のオブジェクトACLによる公開オブジェクト一覧ファイルにはステータスが公開中のオブジェクトACLのみが記載されていますので、
ファイル内のすべての行が確認対象となります。

Publicなバケットポリシー、バケットACL、オブジェクトACLの意図を確認

ここまでで確認した内容で、Publicなバケット・オブジェクトに関する情報は揃っているはずです。
Publicなバケットポリシー、バケットACL、オブジェクトACLのそれぞれについて、意図を確認します。
場合によってはバケットの作成者や部門にヒアリングをしましょう。
調査の結果、バケットもしくはオブジェクトの公開が不要と判断された場合には、すみやかに公開を停止しましょう。

注意点

  • バケットポリシーに関してはワイルドカード指定が可能なため、実際にどのオブジェクトがバケットポリシーによって公開されているか判別が難しいときがあります
    • 例:ポリシーで"Resource":"arn:aws:s3:::test-public-123456789012/test/*"が設定されている場合、バケット内のtest/をプレフィックスに持つすべてのオブジェクトが対象
    • 必要であれば対象バケットでtest/をプレフィックスに持つオブジェクト一覧を出力するスクリプトなどを別途作成して調査してください
  • 本スクリプトでは明示的な拒否が設定されているバケットポリシーを考慮した公開設定の判定は対象外としています
    • バケットACLもしくはオブジェクトACLで公開状態となっていても、バケットポリシーで明示的な拒否が設定されている場合、拒否が優先されて実際には公開されていませんのでご注意ください

PublicなS3バケットとオブジェクトの棚卸しについての説明は以上です。

まとめ

今回のエントリではPublicなS3バケットとオブジェクトの棚卸しをするスクリプトをご紹介しました。
本ブログがどなたかのお役にたてば幸いです。