Amazon Macieを本番導入したら「S3の闇」が見えた話|SOC2審査がきっかけの6ヶ月実録

「どこに個人情報があるか分からない」——SOC2審査で詰まった経験、ありませんか?Macieを150超のS3バケットに本格導入して見えた誤検知・コスト・自動修復の実態をまとめました。

きっかけは「どこに個人情報があるかわからない」という一言

去年の秋、うちのチームでSOC2の審査対応をしていたとき、監査法人の担当者に「S3バケット内の個人情報の所在を証明できますか?」と聞かれて完全に詰まったんですよね。当時はバケット数が150を超えていて、誰がいつ何をアップロードしたか把握しきれていなかった。SOC2対応の記事でも書いたけど、証明責任が問われる場面ではドキュメントの有無より「実態として管理できているか」が問われる。

そこでAmazon Macieを本格導入することにした。以前から「名前は知ってるけど使ってない」ツールの代表格だったんですが、実際に6ヶ月ほど運用してみたら、導入前には想像もしなかった問題が次々と出てきた。誤検知との格闘は以前の記事で触れているので、今回は導入設計からスキャン戦略、自動修復フロー構築までをまとめて書く。


Macieが2026年時点でできること・できないこと

まず現状整理から入る。2025年後半のアップデートでMacieはいくつか機能強化されていて、2026年4月時点ではこんな状態だ。

機能2024年時点2026年時点
管理対象ストレージS3のみS3 + EFS(プレビュー)
カスタム機密データ識別子最大10,000パターン最大50,000パターン
検出結果の自動抑制ルールベースのみMLベースの信頼スコア付き
Organizations統合委任管理者1アカウントマルチリージョン委任管理
EventBridge連携手動設定自動検出フロー統合
価格モデルスキャンデータ量従量Automated Discovery込みの月額固定モデル選択可

EFSへの対応がプレビューで来たのは地味に嬉しい。うちのチームはS3だけじゃなくEFSにも帳票データを置いてたりするので。ただ正直まだ検証中なのがEFS対応の精度で、S3に比べてFalse Positiveが多い印象がある。本番でEFSスキャンを使う場合は閾値を慎重に設定したほうがいい。


実際に組んだAWS構成

Macieを単体で動かすだけなら簡単だけど、検出結果を活用して自動修復まで持っていくためにはそれなりの構成が必要になる。うちが実装したのはこういうアーキテクチャだ。

graph TB
    subgraph "Security Account(委任管理者)"
        Macie[Amazon Macie\n委任管理者]
        SM_Dashboard[Security Hub\n集約ダッシュボード]
        SNS_Alert[SNS Topic\n緊急通知]
    end

    subgraph "Production Account"
        subgraph "VPC(ap-northeast-1)"
            subgraph "Private Subnet"
                Lambda_Remediation[Lambda\n自動修復関数]
            end
        end
        subgraph "S3バケット群"
            S3_App[app-data-bucket\n決済ログ等]
            S3_Logs[access-logs-bucket]
            S3_Backup[backup-bucket]
            S3_Reports[reports-bucket\n帳票PDF]
        end
        EB_Rule[EventBridge Rule\nMacie Finding検知]
        SQS_Queue[SQS Queue\nリトライ付き]
        StepFn[Step Functions\n修復ワークフロー]
    end

    subgraph "Audit Account"
        S3_Findings[findings-archive-bucket\n検出結果保管]
        Athena[Athena\nクエリ分析]
        QuickSight[QuickSight\nレポーティング]
    end

    Macie -->|委任管理| Production Account
    Macie -->|検出結果送信| EB_Rule
    Macie -->|Finding集約| SM_Dashboard
    EB_Rule -->|HIGH/CRITICAL| SNS_Alert
    EB_Rule -->|修復トリガー| SQS_Queue
    SQS_Queue --> StepFn
    StepFn --> Lambda_Remediation
    Lambda_Remediation -->|パブリックACL削除\n暗号化強制| S3_App
    Lambda_Remediation -->|暗号化強制| S3_Reports
    Macie -->|アーカイブ| S3_Findings
    S3_Findings --> Athena
    Athena --> QuickSight

    S3_App -.->|スキャン対象| Macie
    S3_Logs -.->|スキャン対象| Macie
    S3_Backup -.->|スキャン対象| Macie
    S3_Reports -.->|スキャン対象| Macie

構成のポイントを2つ。

Security Accountを委任管理者にするのはOrganizations運用のベストプラクティスで、AWS Organizationsの記事でも触れているパターン。MacieをSecurity Accountから一元管理することで、各プロダクトアカウントのスキャン結果を横断的に見られるようになる。

Step Functionsで修復ワークフローを組むのはLambda単体にするより可観測性が上がるのでオススメ。何の処理でエラーになったか、再試行はいつ走ったか、全部ビジュアルで確認できる。個人的にはこの差がじわじわ効いてくる。


スキャン戦略とコスト試算

MacieにはAutomated DiscoverySensitive Data Discovery Jobsの2つのモードがある。前者は継続的にサンプリングスキャンするやつで、後者は任意のタイミングでフルスキャンするやつだ。

最初は「Automated Discoveryだけでいいじゃん」と思ってたんですが、コンプライアンス証明には「特定時点のスキャン結果」が必要なケースがある。SOC2の監査で「〇月〇日時点でこのバケットに機密データがなかった」という証拠が必要な場面では、Jobsを手動トリガーして結果をアーカイブする運用が必要になった。

# boto3でスキャンジョブを定期実行するスクリプト
import boto3
import json
from datetime import datetime, timezone

def create_macie_job(bucket_names: list[str], job_name_prefix: str) -> dict:
    client = boto3.client('macie2', region_name='ap-northeast-1')
    
    timestamp = datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')
    
    response = client.create_classification_job(
        jobType='ONE_TIME',
        name=f"{job_name_prefix}-{timestamp}",
        s3JobDefinition={
            'bucketDefinitions': [
                {
                    'accountId': '123456789012',
                    'buckets': bucket_names
                }
            ],
            'scoping': {
                'includes': {
                    'and': [
                        {
                            'simpleScopeTerm': {
                                'comparator': 'EQ',
                                'key': 'OBJECT_EXTENSION',
                                'values': ['pdf', 'csv', 'json', 'xlsx', 'txt']
                            }
                        }
                    ]
                }
            }
        },
        # カスタム識別子:日本の個人情報パターン
        customDataIdentifierIds=[
            'custom-id-jp-mynumber',     # マイナンバー
            'custom-id-jp-bankaccount',  # 銀行口座番号
            'custom-id-jp-driverlicense' # 運転免許証番号
        ],
        samplingPercentage=100,  # フルスキャン
        description=f'Quarterly compliance scan - {timestamp}',
        tags={
            'Purpose': 'compliance-audit',
            'Quarter': f"Q{(datetime.now().month - 1) // 3 + 1}-{datetime.now().year}"
        }
    )
    
    return {
        'jobId': response['jobId'],
        'jobArn': response['jobArn'],
        'timestamp': timestamp
    }

if __name__ == '__main__':
    target_buckets = [
        'prod-app-data-bucket',
        'prod-reports-bucket',
        'prod-backup-bucket'
    ]
    result = create_macie_job(target_buckets, 'quarterly-compliance')
    print(json.dumps(result, indent=2))

コスト面は正直、最初かなり衝撃を受けた。うちの場合S3の総データ量が約40TBあって、最初の全量スキャンで試算すると「40,000 GB × $1.00 = $40,000」という数字が出てくるんですよね。「え、4万ドル?」と震えたんですが、実際にはPDFや画像から抽出されたテキスト部分だけが課金対象なので実費は約$3,200だった。それでも事前試算は絶対にやったほうがいい。

xychart-beta
    title "Macieスキャンコスト推移(月次・USD)"
    x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
    y-axis "コスト(USD)" 0 --> 5000
    bar [3200, 420, 380, 410, 390, 400]
    line [3200, 420, 380, 410, 390, 400]

初月は全量スキャンで$3,200かかったけど、2月以降はAutomated Discoveryのみ+差分スキャンに切り替えたので月$400前後に落ち着いた。「初回全量スキャンで基準を作る→以降は差分追跡」という運用が今のところ一番コスパがいいと思っている。


カスタム識別子で日本固有のデータパターンを検出する

Macieのデフォルト検出パターンは米国中心なので、日本の個人情報を検出するにはカスタムデータ識別子を作り込む必要がある。これが地味に大変だった。

マイナンバーのパターン自体は意外とシンプルで、12桁の数字にチェックデジットがある。厄介なのは「123456789012」みたいな連番テストデータとの誤検知をどう除外するかで、ここに一番時間を使った。

# カスタム識別子の作成スクリプト
import boto3

client = boto3.client('macie2', region_name='ap-northeast-1')

# マイナンバー検出パターン
# 区切り文字なし12桁、またはハイフン区切り
my_number_patterns = [
    r'\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b',
]

# 誤検知を減らすキーワード(周辺コンテキスト)
my_number_keywords = [
    'マイナンバー',
    '個人番号',
    'my number',
    'mynumber',
    '個人識別番号',
]

response = client.create_custom_data_identifier(
    name='jp-mynumber-v2',
    description='日本のマイナンバー(個人番号)検出パターン v2',
    regex=my_number_patterns[0],
    keywords=my_number_keywords,
    maximumMatchDistance=200,  # キーワードとの最大距離(文字数)
    ignoreWords=[
        '0000-0000-0000',  # テストデータ除外
        '1111-1111-1111',
        '9999-9999-9999',
    ],
    tags={'Type': 'PII', 'Region': 'JP', 'Standard': 'Act-on-Protection-of-Personal-Information'}
)

print(f"Created identifier ID: {response['customDataIdentifierId']}")

# 銀行口座番号(日本)
# 銀行コード4桁 + 支店コード3桁 + 口座番号7桁
bank_response = client.create_custom_data_identifier(
    name='jp-bank-account-v2',
    description='日本の銀行口座番号パターン',
    regex=r'\b\d{4}[\s\-]?\d{3}[\s\-]?\d{7}\b',
    keywords=['口座番号', '普通口座', '当座口座', '銀行口座', 'bank account'],
    maximumMatchDistance=150,
    tags={'Type': 'Financial', 'Region': 'JP'}
)

print(f"Created bank identifier ID: {bank_response['customDataIdentifierId']}")

ここで地味に効いたのが maximumMatchDistance パラメータだ。これはキーワードと正規表現のマッチがどれだけ離れていてもOKかを設定するもので、小さすぎると「マイナンバーと書いてある近くの12桁だけ」を検出、大きすぎると文脈関係なく12桁数字を全部拾う。200文字くらいが経験的にちょうどよかった。

導入直後の2週間は、注文番号や管理番号といったPIIじゃない12桁数字が大量に引っかかってきてその対処に追われた。コンテナセキュリティの記事でも書いたけど、セキュリティツールの運用コストは「誤検知の対処」が大半を占める。Macieも例外じゃなかった。


EventBridgeと連携した自動修復フロー

検出した問題を人間が毎回手動で直すのは続かない。うちはHIGH以上の検出結果は自動修復、MEDIUM以下は通知のみという仕切りにした。

# EventBridge Rule(CloudFormationテンプレート抜粋)
MacieFindingRule:
  Type: AWS::Events::Rule
  Properties:
    Name: macie-finding-auto-remediation
    Description: Macie HIGH/CRITICAL findings trigger auto-remediation
    EventPattern:
      source:
        - aws.macie
      detail-type:
        - Macie Finding
      detail:
        severity:
          description:
            - High
            - Critical
        type:
          - SensitiveData:S3Object/Financial
          - SensitiveData:S3Object/Personal
          - Policy:IAMUser/S3BucketPubliclyReadable
          - Policy:IAMUser/S3BucketPubliclyWritable
    State: ENABLED
    Targets:
      - Id: RemediationQueue
        Arn: !GetAtt RemediationQueue.Arn
        InputTransformer:
          InputPathsMap:
            bucketName: "$.detail.resourcesAffected.s3Bucket.name"
            objectKey: "$.detail.resourcesAffected.s3Object.key"
            findingType: "$.detail.type"
            severity: "$.detail.severity.description"
            findingId: "$.detail.id"
          InputTemplate: |
            {
              "bucketName": "<bucketName>",
              "objectKey": "<objectKey>",
              "findingType": "<findingType>",
              "severity": "<severity>",
              "findingId": "<findingId>",
              "action": "REMEDIATE"
            }
# Lambda修復関数の核心部分
import boto3
import json
import logging
from enum import Enum

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class RemediationAction(Enum):
    BLOCK_PUBLIC_ACCESS = 'block_public_access'
    QUARANTINE_OBJECT = 'quarantine_object'
    ENCRYPT_OBJECT = 'encrypt_object'
    NOTIFY_OWNER = 'notify_owner'

def handler(event, context):
    s3 = boto3.client('s3')
    message = json.loads(event['Records'][0]['body'])
    
    bucket_name = message['bucketName']
    object_key = message['objectKey']
    finding_type = message['findingType']
    finding_id = message['findingId']
    
    logger.info(f"Processing finding: {finding_id}, type: {finding_type}")
    
    actions_taken = []
    
    # パブリックアクセス系の問題は即ブロック
    if 'PubliclyReadable' in finding_type or 'PubliclyWritable' in finding_type:
        s3.put_public_access_block(
            Bucket=bucket_name,
            PublicAccessBlockConfiguration={
                'BlockPublicAcls': True,
                'IgnorePublicAcls': True,
                'BlockPublicPolicy': True,
                'RestrictPublicBuckets': True
            }
        )
        actions_taken.append(RemediationAction.BLOCK_PUBLIC_ACCESS.value)
        logger.info(f"Blocked public access for bucket: {bucket_name}")
    
    # 機密データ検出は対象オブジェクトを隔離バケットへ移動
    if 'SensitiveData' in finding_type and object_key:
        quarantine_bucket = 'security-quarantine-bucket'
        quarantine_key = f"findings/{finding_id}/{object_key}"
        
        # オブジェクトをコピーして元を削除(Move相当)
        s3.copy_object(
            CopySource={'Bucket': bucket_name, 'Key': object_key},
            Bucket=quarantine_bucket,
            Key=quarantine_key,
            ServerSideEncryption='aws:kms',
            SSEKMSKeyId='arn:aws:kms:ap-northeast-1:XXXX:key/YYYY',
            MetadataDirective='COPY',
            TaggingDirective='COPY'
        )
        
        # 元のオブジェクトに隔離タグを付ける(削除前の確認期間として24h保持)
        s3.put_object_tagging(
            Bucket=bucket_name,
            Key=object_key,
            Tagging={
                'TagSet': [
                    {'Key': 'SecurityStatus', 'Value': 'QUARANTINED'},
                    {'Key': 'FindingId', 'Value': finding_id},
                    {'Key': 'QuarantinedAt', 'Value': context.aws_request_id}
                ]
            }
        )
        actions_taken.append(RemediationAction.QUARANTINE_OBJECT.value)
    
    return {
        'statusCode': 200,
        'findingId': finding_id,
        'actionsTaken': actions_taken
    }

「機密データが入ったオブジェクトを即削除」という設計も最初は検討したんだけど、これは危ないとすぐわかった。誤検知で必要なファイルが消えてしまうリスクがあるので、隔離バケットへ移動→タグ付き保留→24時間後に人間がレビューして削除判断というフローにした。実際にこれで助かったのが、誤検知で隔離されたファイルを2件ほど復元できたことだ。即削除にしてたらアウトだった。


6ヶ月運用してわかった、本当の効果と課題

運用してみてわかった「検出されたもの」のリアルをざっくり共有する。

pie title 検出されたFindingの内訳(6ヶ月累計)
    "テストデータ内の個人情報" : 42
    "不要になった旧バックアップ" : 28
    "ステージングデータの本番混入" : 15
    "本当の機密データ漏洩リスク" : 8
    "誤検知" : 7

一番多かったのが「テストデータ内の個人情報」で全体の42%。開発チームが動作確認で使った実データがS3に残り続けていたケースで、「テスト用だから大丈夫」という意識が現場にあったわけだけど、S3に置かれた時点でリスクは変わらない。これは導入前は誰も把握できていなかった。

2番目の「不要になった旧バックアップ」も28%と多くて、2〜3年前のバックアップが暗号化なしで残っていたりした。ライフサイクルポリシーを整備するきっかけになったのは、正直Macieの副産物だった。

「本当の機密データ漏洩リスク」は8%(具体的には4件)で、そのうち1件は外部ベンダーから受け取ったCSVに個人情報が含まれていて、処理後の削除が漏れていたケース。これを自動で発見できたのはMacie導入の明確な成果だったと思う。

一方でチームで困ったのが「誰が修正責任者か」の問題だ。検出されても担当者へのエスカレーションルールが曖昧だとFindingが宙に浮く。インシデント対応のベストプラクティスと同じで、「誰が何を担当するか」を事前に決めておかないとツールだけ入れても機能しない。ここは今も正直改善途中だ。

皆さんのチームはS3の機密データ所在ってちゃんと把握できてますか?「たぶん大丈夫」は実は「把握できていない」と同じなんですよね、これが。


まとめ

6ヶ月Macieを運用して得た知見を整理する。

  1. 初回は全量スキャン必須:Automated Discoveryはサンプリングなので、基準線を引くために全量スキャンを1回やる。コストは思ったより安い(テキスト抽出後の量ベース課金)
  2. カスタム識別子は日本固有パターンを丁寧に作るmaximumMatchDistanceignoreWordsの調整で誤検知を大幅に減らせる
  3. 自動修復は「隔離→レビュー→削除」のフローで:即削除は危険。誤検知が必ず発生するので復元経路を確保する
  4. 検出より「誰が直すか」の整備が重要:OwnerタグとFinding通知の紐付けを事前に設計しないと機能しない
  5. 本当の効果はコンプライアンス証明だけじゃない:テストデータ放置やライフサイクル未設定バケットの発見など、セキュリティ衛生観点でも使える

次のアクションとしては、EFSスキャンのGA版が出次第本番適用するのと、検出結果をDatadogのダッシュボードに統合してメトリクスを一元化する予定。あとカスタム識別子のパターン精度を上げるために、機械学習ベースのアノテーションツールも試してみたいと思っている。まだ検証中なので、結果が出たらまた書く。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事