GuardDuty S3 Malware Protection 導入に向けた S3 バケット棚卸し調査
関連 Issue: https://github.com/fastdoctor-jp/mental-online-karte/issues/17287Notion: S3 バケット棚卸し・データ分類(本番環境)関連設計書: GuardDuty 組織統合設計書調査期間: 2026-08-12 〜 2026-08-14
1. 調査背景
GuardDuty 組織統合設計書にて「Malware Protection for S3 は別途設計後に有効化。対象バケットの精査が必要」と記載されていた(タスク #6)。 本調査は、対象バケットの選定に必要な以下を実施した。
- 本番環境の全 S3 バケット(367件)の棚卸し・用途分類
- 外部ユーザー(患者)が直接アップロードするバケットの特定
- アップロード頻度の実測とコスト試算
- アクセスログ・ライフサイクルポリシーの検討
- Macie との比較・導入優先度の判断
2. 調査方法
2.1 バケット一覧の取得
aws s3api list-buckets --profile fd-prod-read
aws s3api list-buckets --profile fd-sys-read- production アカウント: 162 バケット
- fd-sys アカウント: 205 バケット
- 合計: 367 バケット
2.2 バケット用途の分類
以下の方法で全367バケットを分類した。
- 命名パターンによる自動分類:
*-alb-logs-*(ALBログ)、*-codepipeline(CI/CD)、*-serverlessdeploymentbuck-*(Serverless Framework)等 - Terraform コード調査:
aws_s3_bucketリソース定義、IAM ポリシー、CORS 設定を確認 - AWS CLI による実態確認:
aws s3 ls、aws s3api get-bucket-taggingでバケット内容・タグを確認 - ソースコード調査: GitHub API でアプリケーションリポジトリのアップロード/参照コードを確認
2.3 外部アップロードの特定
以下の2つのアプローチで網羅的に確認した。
- Terraform CORS PUT 検索: リポジトリ全体で
allowed_methods = ["PUT"]を検索 → 2件のみヒット - アプリケーションコード検索: presigned URL 生成、STS AssumeRole、Shrine アップロード等のパターンを GitHub コード検索で確認
2.4 アップロード頻度の計測
CloudWatch NumberOfObjects メトリクスの日次差分で算出した。
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name NumberOfObjects \
--dimensions Name=BucketName,Value={バケット名} Name=StorageType,Value=AllStorageTypes \
--start-time 2026-08-07T00:00:00Z \
--end-time 2026-08-14T00:00:00Z \
--period 86400 \
--statistics Average \
--region {us-east-1 or ap-northeast-1}- production バケット: us-east-1 から取得(S3 ストレージメトリクスの仕様)
- fd-sys バケット: ap-northeast-1 から取得
3. 調査結果
3.1 全バケット分類
| 分類 | 件数 | GuardDuty 対象 |
|---|---|---|
| 外部ユーザー直接アップロード | 4 | ○(確定) |
| システム録画・自動記録 | 5 | × |
| 内部バッチ・運用生成 | 8 | × |
| CI/CD・デプロイ | 約170 | × |
| ML/データパイプライン | 6 | × |
| ログ(ALB/WAF/CDN/VPC/セキュリティ) | 約120 | × |
| 静的コンテンツ配信 | 8 | × |
| Amazon Connect 関連 | 9 | × |
| Terraform/インフラ管理 | 約10 | × |
| テスト・デモ・空・廃止 | 約10 | × |
| 要オーナー確認 | 1 | 可能性低 |
全367バケットの詳細は以下を参照。
- S3 バケット棚卸しスプレッドシート(全367バケットの詳細)
3.2 GuardDuty S3 Malware Protection 対象バケット(確定)
| バケット | アカウント | アップロード契機 | アップロード方式 | リージョン | 日次アップロード | 月次スキャン量 | 月額コスト |
|---|---|---|---|---|---|---|---|
| fastdoctor-images-production | fd-sys | 患者が保険証・医療証、医師がカルテ画像追加 | Shrine presigned POST | ap-northeast-1 | ~2,490件/日 | ~54.2 GB | ~$27.2 |
| mental-reserve-patient-medical-image-production | production | メンタルヘルス患者が保険証等をアップロード(初診時のみ) | presigned URL PUT(ブラウザ直接) | us-east-1 | ~1,203件/日 | ~87.4 GB | ~$15.4 |
| ai-triage-prd-file-upload | production | 事前問診で患者が症状の写真アップロード | presigned URL PUT(STS AssumeRole → 5分期限) | us-east-1 | ~1,877件/日 | ~41.9 GB | ~$15.7 |
| online-karte-static-front-production | production | 医師が検査結果・お薬手帳・保険証画像を添付、処方箋PDF生成 | BFF server-side PutObject + presigned URL PUT(KB用) | ap-northeast-1 | ~1,123件/日 | ~15.8 GB | ~$11.2 |
| 合計 | ~6,693件/日 | ~199.3 GB/月 | ~$69.5/月(~$834/年) |
コスト算出根拠:
- 計測期間: 2026-07-14〜08-13(30日間)
- 計測方法: CloudWatch
NumberOfObjectsの日次スナップショットから前日との差分を日次アップロード数として算出 - 平均ファイルサイズ: CloudWatch
BucketSizeBytes÷NumberOfObjectsで算出(全オブジェクトの平均) - 月次スキャン量: 日次アップロード数 × 平均ファイルサイズ × 30日
- 月額コスト: データスキャン料金 + リクエスト料金(リージョンにより異なる)
- リージョン別単価(AWS Pricing API から取得):
| 課金項目 | us-east-1(バージニア) | ap-northeast-1(東京) |
|---|---|---|
| データスキャン | $0.09/GB | $0.1185/GB |
| リクエスト | $0.000215/件 | $0.000282/件 |
- 例: fastdoctor-images-production(東京)= 54.2 GB × $0.1185 + 74,700件 × $0.000282 = $6.42 + $21.07 = ~$27.5/月
mental-reserve-patient-medical-image-productionとai-triage-prd-file-uploadは Terraform のvar.regionがus-east-1であるため、バージニアリージョンの単価を適用- 30日計測により平日/休日・月内変動を反映済み
- 年末年始等の繁忙期は診察件数増加に伴いアップロード数が上振れる可能性があるが、大幅な増加は想定しにくい
3.3 対象外と判断したバケット(当初候補)
| バケット | 当初の想定 | 調査結果 |
|---|---|---|
| ai-triage-assets-prd | ユーザーアップロード先の可能性 | ⛔ 問診定義 JSON の配信専用(ソースコード確認済み) |
| retool-storage-prod | Retool 添付ファイルの可能性 | ⛔ 6オブジェクト・変化なし・orca/ のみ |
| online-examination-images-production | オンライン健診画像 | ⛔ サービス廃止済み(リポジトリ削除済み・30日間ゼロ) |
| health-center-automation | 外部からファイル入力の可能性 | ⛔ 11オブジェクト・サイズほぼゼロ(影響極小) |
3.4 アップロード・参照パターン
全4バケットについて、ソースコード・設計書レベルで参照パターンを確認した。
| バケット | 毎回アップロードか | 過去分の参照 | 根拠 |
|---|---|---|---|
| fastdoctor-images-production | 診察ごとに新規追加 | あり(確定) | @karte.images.order(id: :desc) で全件返す |
| mental-reserve-patient-medical-image | 初診時のみ(再診では不要) | あり(確定) | 設計書「原則ファイル削除しない」。DB に objectKey 永続保存 |
| ai-triage-prd-file-upload | 毎回新規 | あり(確定) | attachments テーブルにキー永続保存 |
| online-karte-static-front-production | 診察時に新規追加 | あり(確定) | 設計書「カルテ改ざん防止のため削除しない」。論理削除のみ |
全バケット共通: 設計原則として「医療記録の改ざん防止のためファイルは原則削除しない」。S3 オブジェクトの物理削除は行わない。
3.5 GuardDuty S3 Malware Protection の課金形態
| 課金項目 | 単価 | FastDoctor 推定コスト |
|---|---|---|
| データスキャン | us-east-1: $0.09/GB / ap-northeast-1: $0.1185/GB | ~$18.1/月 |
| リクエスト | us-east-1: $0.000215/件 / ap-northeast-1: $0.000282/件 | ~$49.7/月 |
| EventBridge Managed Rule | アップロード検知用イベント取り込み | ~$0.21/月 |
| S3 API (GET/PUT) | GuardDuty が IAM ロール経由で S3 API 実行 | 数円/月 |
| S3 Object Tagging(任意) | スキャン結果を S3 タグで付与 | ~$2.1/月 |
| 合計 | ~$69.5/月 |
重要: スキャン対象は新規アップロードのみ。既存オブジェクト(合計 7TB+)はスキャンされない。
根拠: AWS 公式ドキュメント — "Malware Protection for S3 listens to the Amazon EventBridge notifications. When an object gets uploaded to the selected bucket or one of the prefixes, GuardDuty downloads that object..."
Free Tier はアカウントあたり月 1,000 リクエスト + 1GB。FastDoctor は日次 ~7,000 件のため初日で超過。30日間無料トライアルはなし。
4. Macie との比較・方針
Macie と GuardDuty S3 Malware の違い
| 項目 | GuardDuty S3 Malware | Macie |
|---|---|---|
| 目的 | マルウェア検出 | 機密データ発見 |
| タイミング | アップロード時リアルタイム | 日次サンプリング |
| 対象選定 | バケット単位で明示的に有効化 | デフォルト全バケット → 除外リスト |
Macie 見送りの理由
- 費用対効果が薄い: 最大容量の3バケット(合計 7TB+)は JPEG 画像で Macie は画像の中身を読めない
- 漏洩リスクが低い: 全バケットでパブリックアクセスブロック済み + アカウントレベルでも無効化
- ガバナンス未整備: 機密データの分類・管理ポリシーが未整備。Macie の検出結果を受けて是正するプロセスがない
- コスト: $130〜200+/月の追加費用に対して ROI が低い
ロードマップ
| Phase | 施策 | 状態 |
|---|---|---|
| Phase 1 | GuardDuty S3 Malware Protection 導入(4バケット) | 対象確定・コスト試算済み → 実装へ |
| Phase 2 | S3 Server Access Logs + Storage Lens 有効化 | 調査完了 → 実装へ |
| Phase 3 | データ分類ポリシー・ガバナンス整備 | 未着手 |
| Phase 4 | Macie 導入(ガバナンス整備後) | 見送り中 |
5. S3 Server Access Logs
導入方針
監査の観点から、医療・個人情報を含むバケット全てにアクセスログを有効化する。
- 得られる情報: オブジェクト単位のアクセス日時・誰が・どこから・何をしたか
- 用途: 監査対応 + ライフサイクルポリシー検討の実測データ
コスト試算
S3 Server Access Logs の課金はログファイルの S3 保存料金のみ。ロギング機能自体は無料。
試算根拠:
- 1ログ行 ≈ 300 bytes(AWS公式: ログレコードフォーマット)
- S3 Standard ストレージ: $0.025/GB/月(東京リージョン)
バケット別試算(GET リクエストを PUT の10倍と仮定):
| バケット | PUT/月 | GET/月(推定) | 合計リクエスト/月 | ログサイズ/月 | 保存コスト/月 |
|---|---|---|---|---|---|
| fastdoctor-images-production | ~84K | ~840K | ~924K | ~277 MB | ~$0.007 |
| mental-reserve-patient-medical-image | ~34K | ~340K | ~374K | ~112 MB | ~$0.003 |
| ai-triage-prd-file-upload | ~57K | ~570K | ~627K | ~188 MB | ~$0.005 |
| online-karte-static-front-production | ~39K | ~390K | ~429K | ~129 MB | ~$0.003 |
| 合計 | ~2.35M | ~706 MB | ~$0.018/月 |
悲観ケース(GET が PUT の100倍):
| バケット | 合計リクエスト/月 | ログサイズ/月 | 保存コスト/月 |
|---|---|---|---|
| fastdoctor-images-production | ~8.5M | ~2.5 GB | ~$0.06 |
| 4バケット合計 | ~23.5M | ~7 GB | ~$0.18/月 |
結論: 悲観ケースでも月額 $0.18(約27円)。コストは事実上ゼロ。
注: S3 Server Access Logs は PUT リクエストに対する課金もない(ログ配信自体が無料)。課金されるのは保存先バケットのストレージ料金のみ。 参考: AWS公式 - S3 Server Access Logging
現状
組織 CloudTrail(fd-organization-trail)は S3 オブジェクトレベルのログを記録していない(data_resource ブロックなし)。 CloudTrail Data Events は高コスト($0.10/10万イベント)のため、S3 Server Access Logs を採用する。
S3 Server Access Logs の設定方針
対象バケット: 医療・個人情報を含む全バケット
| バケット | アカウント | TF管理 | 設定方法 | ログ出力先 |
|---|---|---|---|---|
| mental-reserve-patient-medical-image-production | production | ○ | TF コードに追加 | fd-s3-logs-prd(既存) or 新規作成 |
| ai-triage-prd-file-upload | production | ○ | TF コードに追加 | 同上 |
| online-karte-static-front-production | production | ○ | TF コードに追加 | 同上 |
| fastdoctor-images-production | fd-sys | × | common/production/globals/s3/ に import → TF コードに追加。または手動(AWS CLI) | fd-s3-logs-prd(fd-sys 側に既存) |
| call-voice-record | fd-sys | × | 手動 | 同上 |
| call-voice-record-transcribed | fd-sys | × | 手動 | 同上 |
| fastdoctor-pgbackup / -virginia | fd-sys | × | 手動 | 同上 |
| online-examination-images-production | fd-sys | × | 手動(ただし廃止済みのため優先度低) | 同上 |
fd-s3-logs-prdは fd-sys アカウントに既存の S3 アクセスログ保存先バケット。production アカウント側にも同様のログ保存先バケットがあるか確認が必要。
S3 Storage Lens
Terraform での追加設定は不要。 デフォルトダッシュボード(default-account-dashboard)が AWS により全アカウントに自動作成済みであることを確認した(production / fd-sys 両方)。
- S3 コンソール → Storage Lens →
default-account-dashboardでバケット単位のメトリクスを確認可能 - バケットごとの容量・オブジェクト数・暗号化状態・ライフサイクルルール有無が確認できる
- Free tier のメトリクスは14日間保持
- リクエスト数(Activity metrics)は Advanced tier(有料)が必要だが、Access Logs で代替するため不要
TF 管理外バケットの import 方針
以下の TF 管理外バケットは common/amazon-connect/globals/s3/ に import し、Access Logs を Terraform で設定する。
| バケット | アカウント | import 先 |
|---|---|---|
| fastdoctor-images-production | fd-sys | common/amazon-connect/globals/s3/ |
| call-voice-record | fd-sys | 同上 |
| call-voice-record-transcribed | fd-sys | 同上 |
| fastdoctor-pgbackup / -virginia | fd-sys | 同上 |
| online-examination-images-production | fd-sys | 同上(優先度低:サービス廃止済み) |
6. 新規バケット追加時の漏れ防止策
今後新しいサービスが S3 にユーザーアップロード機能を追加した際に GuardDuty を有効化し忘れるリスクへの対策。
- 開発プロセスで防ぐ: 新規バケット作成時の PR レビューチェックリストに「ユーザーアップロードを受けるか?→ GuardDuty 必須」を追加
- Terraform モジュールで強制する: ファイルアップロード用バケットの共通モジュールを作成し、GuardDuty + Access Logs + 暗号化 + パブリックアクセスブロックをバケット作成と同時に自動適用
7. 今後の実装タスク
| # | タスク | 備考 |
|---|---|---|
| 1 | ✅ 完了(PR #2639)。4バケットに aws_guardduty_malware_protection_plan を適用。infra-dev で検証済み | |
| 2 | ✅ 完了(PR #2642)。production に fd-s3-access-logs-production を新規作成。fastdoctor-images-production を import | |
| 3 | ✅ 対応不要。デフォルトダッシュボード(default-account-dashboard)が全アカウントに自動作成済み | |
| 4 | ファイルアップロード用共通モジュール設計 | 将来の漏れ防止策 |
別タスク(今回のスコープ外)
| タスク | 備考 |
|---|---|
fd-private-key の Secrets Manager 移行 | SSH 秘密鍵を S3 に保管しているセキュリティリスク |
fastdoctor-pgbackup のバックアップ運用確認 | 世代管理されているか等 |
| ストレージクラス移行(Glacier 等) | Access Logs 30日実測後に検討 |
| 非本番環境(staging, develop)の調査 | 本番の結果をテンプレートに展開 |