フォレンジック作業の証拠をS3に集めるAWSのフレームワークを試してみた
こんにちは、せーのです。
インシデントレスポンス(IR)やフォレンジックは関心が高い一方で、用語と現場の手順が一気に押し寄せて「とりあえず雰囲気はわかったが、腑に落ちない」まま終わりがちです。
そこで 2026-04-08 に公開された AWS Security Blog の A framework for securely collecting forensic artifacts into S3 buckets の公式の CDK サンプル sample-collect-forensic-artifacts-s3 で手元に落としてみました。
フォレンジックの「収集」って何(おさらい)
デジタルフォレンジックの流れは、NIST SP 800-86 ではざっくり次の 4 フェーズに分かれます。
- Collection(収集) … 今日の話はここ
- Examination(検証)
- Analysis(分析)
- Reporting(報告)
収集 とは、「あとで説明できるように、ディスクやメモリ、ログなどのアーティファクト を取り出して保全すること」です。AWS 上なら EBS スナップショットやメモリダンプ、各種ログが典型例になります。
ここが難所で、侵害の疑いがあるリソースに接続して何かを実行する プロセスそのものが、新たなリスクになり得ます。だからこそ、最小権限・期限付きの認証情報・手順の自動化 がセットで語られる、というのがこの手の設計記事の出発点です。
このフレームワークがやっていること
公式記事が強調している設計の考え方は、次のようなイメージです。
| 観点 | ざっくりした意味 |
|---|---|
| 最小権限 | ケース(チケット)ごとに S3 のプレフィックス などへアクセスを絞る |
| 時間制限 | STS で 期限付き の認証情報を扱う |
| 既存ツール互換 | S3 に載せられる フォレンジックツールなら流用しやすい |
| クレデンシャル自動発行 | 調査員が毎回コンソールでポチポチ…を減らす |
| プロセス自動化 | CDK(IaC)と Step Functions で 決まった手順 を機械に任せる |
証拠を置く S3 バケット側では、転送時の TLS 強制、保存時暗号化(CMK)、CloudTrail のデータイベント、バケットポリシーと IAM の組み合わせ、バージョニング、必要に応じた Object Lock など、「後から監査できる」「勝手に消されにくい」 方向のオプションが整理されています。細部は公式記事と CDK を正としてください。
全体の流れ(ざっくり)
ざっくりしたデータフローは次のとおりです。

- きっかけ: 検知元やインシデント対応者からの入力を SQS に載せる想定
- オーケストレーション: Lambda が Step Functions を起動
- 実行: SSM Run Command で対象 EC2 にツールを同期し、収集スクリプトを実行し、結果を S3 にアップロード
- 記録: 実行メタデータを DynamoDB に残す
- 監視: 証拠バケットへの不審なアクセスを CloudTrail データイベント → EventBridge → SNS などで通知
CDK はだいたい 3 スタックに分かれます。
- SecurityStack(セキュリティツール用アカウント想定)… Step Functions やキュー、証拠バケットなど
- AlertStack(同上)… 通知まわり
- CustomerStack-{アカウントID}(調査対象側)… SSM ドキュメントやクロスアカウント用ロールなど
本番では セキュリティ用アカウントと顧客(ワークロード)アカウントを分ける前提ですが、再現性を優先して 同一アカウントに載せる検証もよく行われます。この記事でもそのパターンを含めて触れます。
やってみた
前提とサンプルの入手
- AWS CLI、Node.js、CDK v2、Git が使えること
- 対象の EC2 が SSM の Managed Instance として Online であること(インスタンスプロファイルに
AmazonSSMManagedInstanceCore相当が付いている等)
git clone https://github.com/aws-samples/sample-collect-forensic-artifacts-s3
cd sample-collect-forensic-artifacts-s3
npm install
CDK CLI と aws-cdk-lib のバージョン合わせ
npm install した aws-cdk-lib が新しめだと、生成される CloudFormation の schema version が上がり、古いグローバルインストールの CDK CLI では cdk synth が通らないことがあります。手元では npm install aws-cdk --save-dev で プロジェクトローカルの CLI を揃えてから npx cdk synth する、という形に落ち着きました。
$ npx cdk synth
Successfully synthesized to cdk.out/
設定ファイル(constants/config.ts)
アカウント ID や通知先メール、許可するロール名などを書きます(値は架空の例です)。
export const SECURITY_ACCOUNT = "111122223333";
export const CUSTOMER_ACCOUNTS = ["111122223333"]; // 検証では同一アカウントに載せた
export const ALLOW_LISTED_ROLE_NAMES = ["SecurityAnalystRole", "ForensicsOperatorRole"];
export const ALERT_EMAIL_RECIPIENTS = ["security-alerts@example.com"];
CDK Bootstrap とデプロイ
$ npx cdk bootstrap aws://111122223333/us-east-1
✅ Environment aws://111122223333/us-east-1 bootstrapped.
$ npx cdk deploy SecurityStack AlertStack --require-approval never
✅ SecurityStack
✅ AlertStack
$ npx cdk deploy CustomerStack-111122223333 --require-approval never
✅ CustomerStack-111122223333
SNS のサブスクリプション確認メールが届いたら、必ず確認リンクを踏んで有効化します(AlertStack 側)。

デプロイでハマった点(IAM の依存関係)
初回デプロイで、次のようなエラーに当たりました(リソース名は読みやすくしています)。
CREATE_FAILED | AWS::IAM::Role | SecurityTriageRole
Resource handler returned message: "Invalid principal in policy: ...:role/LambdaRole"
原因の推測はこうです。SecurityTriageRole の信頼ポリシーに LambdaRole の ARN を文字列で直書きしている一方、CloudFormation 上では 両方のロールが並列に作られようとするため、プリンシパル検証の時点で まだ片方が存在しないと怒られる、というパターンです。
対処はシンプルで、LambdaRole を先に定義し、SecurityTriageRole の信頼ポリシーは lambdaRole.roleArn を参照させて CDK に依存関係を認識させます(文字列 ARN 依存をやめる)。
// 例: 概念だけ(実ファイルの構造に合わせて読み替えてください)
const lambdaRole = new iam.Role(this, "LambdaRole", { /* ... */ });
const securityTriageRole = new iam.Role(this, "SecurityTriageRole", {
assumedBy: new iam.ArnPrincipal(lambdaRole.roleArn),
});
つまり、「IAM は依存関係の見えない文字列」でつなぐと壊れやすい、という学びでした。
シングルアカウント検証の注意(SSM 権限)
同一アカウントに Security と Customer を載せると、サンプル内の分岐により クロスアカウントロール経由のパスがスキップされることがあります。その結果、Lambda 実行ロールに SSM 向け権限が無いと Run Command が進まない、という症状が出ます。
検証では一時的に Lambda 用ロールへ SSM 実行ポリシーをアタッチして切り抜けましたが、本番でアカウント分離しているなら、設計上は不要なワークアラウンドだと考えてください(公式サンプルの想定はマルチアカウント側に寄っています)。
SSM Managed Instance の確認
$ aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=i-0a1b2c3d4e5f67890" \
--query 'InstanceInformationList[0].[InstanceId,PingStatus,AgentVersion]' \
--output text
i-0a1b2c3d4e5f67890 Online 3.3.xxxx
STS とセッションポリシー(ケース別プレフィックス)
実効権限はざっくり「ロールに付いたポリシー」「リソース側ポリシー」「STS のセッションポリシー」の 共通部分(交差) になります。セッションポリシーで CASE-0001/* に絞ると、同じバケットの CASE-0004/* へは s3:PutObject 自体がセッション上は許可されない、という動きになります。
一時認証情報の取得(例)
$ aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/SecurityTriageRole \
--role-session-name CASE-0001 \
--duration-seconds 3600 \
--policy '{"Version":"2012-10-17","Statement":[
{"Effect":"Allow","Action":["s3:PutObject","s3:AbortMultipartUpload"],
"Resource":"arn:aws:s3:::111122223333-evidence-bucket-us-east-1/CASE-0001/*"},
{"Effect":"Allow","Action":["kms:GenerateDataKey","kms:Encrypt","kms:Decrypt"],
"Resource":"arn:aws:kms:us-east-1:111122223333:key/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}
]}'
環境変数に載せ替えてから aws s3 cp します。
許可されたプレフィックスへのアップロード(成功)
$ aws s3 cp evidence.txt s3://111122223333-evidence-bucket-us-east-1/CASE-0001/evidence.txt
upload: ./evidence.txt to s3://111122223333-evidence-bucket-us-east-1/CASE-0001/evidence.txt
別プレフィックスへのアップロード(拒否)
$ aws s3 cp evidence.txt s3://111122223333-evidence-bucket-us-east-1/CASE-0004/evidence.txt
An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::111122223333:assumed-role/SecurityTriageRole/CASE-0001 is not authorized
to perform: s3:PutObject on resource: "arn:aws:s3:::111122223333-evidence-bucket-us-east-1/CASE-0004/evidence.txt"
because no session policy allows the s3:PutObject action
ポイントは、AssumeRole で セッションポリシーを付けたとき、その一時クレデンシャルの実効権限は「ロールに付いたポリシー」と「セッションポリシー」の積集合(両方を満たす部分だけ) になることです。セッションポリシーは ロールより権限を広げることはできず、さらに狭めるためだけ に効きます。
ですから、ロール側が広くても 実行時にセッションポリシーでプレフィックスなどを閉じれば、そのセッションでは結果として狭い権限だけ が残ります。逆に、ロールがもともと狭いなら、セッションポリシーで「広い許可」を足すことはできません。違いを整理すると、「ロールが許す範囲」∩「このセッションが許す範囲」 がその STS セッションの実効権限、というイメージです。
期限切れ(ExpiredToken)
今回は 1 時間待ちの実測までは省略しますが、期限後に同じ操作をすると ExpiredToken で落ちます。設計としては 「漏れても長くは生きない認証情報」 を作れる、という意味合いです。
自動ワークフロー(SQS → Step Functions → SSM → S3)
SQS に、次のような JSON を投げて起動します(値は架空例です)。
$ aws sqs send-message \
--queue-url https://sqs.us-east-1.amazonaws.com/111122223333/AlertQueue \
--message-body '{
"account": "111122223333",
"ticket_id": "CASE-0002",
"region": "us-east-1",
"instance_id": "i-0a1b2c3d4e5f67890"
}'
{
"MD5OfMessageBody": "...",
"MessageId": "11111111-2222-3333-4444-555555555555"
}
Step Functions はコンソールでも追えますが、ここでは CLI 中心なので、実行が安定したあとに describe-execution で状態だけ拾うイメージです。
$ aws stepfunctions describe-execution \
--execution-arn "arn:aws:states:us-east-1:111122223333:execution:SecurityWorkflow-demo:exec-demo-001"
{
"status": "SUCCEEDED",
"startDate": "...",
"stopDate": "..."
}
実行ステップの流れは、ざっくり次のような順番でした(名前はサンプル実装に倣っています)。
Evaluate Instance
→ Check Evaluation Results
→ ToolSync(run_collection.sh を EC2 に同期)
→ Check ToolSync Execution Status
→ Execute Tooling(run_collection.sh 実行)
→ Check Tool Execution Status
→ Upload Results(出力を S3 にアップロード)
→ Check Upload Results Status
→ Update DynamoDB Table
→ Finish
DynamoDB 側には、チケット ID やインスタンス ID、各ステップの成否、アップロード先の S3 パスなどが載ります(scan の出力は長いので 必要な属性だけ見ます)。
$ aws dynamodb scan --table-name AlertMetricsTable --max-items 1
{
"Items": [
{
"ID": { "S": "CASE-0002" },
"AccountID": { "S": "111122223333" },
"InstanceId": { "S": "i-0a1b2c3d4e5f67890" },
"Region": { "S": "us-east-1" },
"S3ArtifactLocation": {
"S": "s3://111122223333-evidence-bucket-us-east-1/CASE-0002/111122223333/i-0a1b2c3d4e5f67890/..."
},
"ToolSyncStatus": { "BOOL": true },
"CollectionScriptStatus": { "BOOL": true },
"UploadResultsStatus": { "BOOL": true }
}
],
...
}
S3 上のキーは、だいたい {ticket_id}/{account_id}/{instance_id}/... のような階層になります。
$ aws s3 ls s3://111122223333-evidence-bucket-us-east-1/ --recursive
CASE-0001/evidence.txt
CASE-0002/111122223333/i-0a1b2c3d4e5f67890/CASE-0002_i-0a1b2c3d4e5f67890_output.txt
アラート(許可リスト外プリンシパル)
証拠バケットには、許可されたロール以外のアクセスを検知して通知する仕組みが載っています。検証では 許可リストに入っていないロールで GetObject を試し、明示的 deny で落ちることと、しばらくしてから SNS メールが来るところまで確認しました(CloudTrail のデータイベント配信には 数分単位の遅延がありえます)。
$ aws s3api get-object \
--bucket 111122223333-evidence-bucket-us-east-1 \
--key "CASE-0002/111122223333/i-0a1b2c3d4e5f67890/CASE-0002_i-0a1b2c3d4e5f67890_output.txt" \
/tmp/out.txt
An error occurred (AccessDenied) when calling the GetObject operation:
User: arn:aws:sts::111122223333:assumed-role/SecurityTriageRole/alert-test-001
is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::111122223333-evidence-bucket-us-east-1/.../CASE-0002_..._output.txt"
with an explicit deny in a resource-based policy

メール本文は長いので要約すると、**「どのプリンシパルが、どのオブジェクトに、どの API を試みたか」**が載り、失敗理由が AccessDenied であることも分かる、というタイプでした(送信元・件名・文面は環境依存なので、実運用ではテンプレを整えるのがよさそうです)。
※クリーンアップの注意
バージョニング有効のバケットは、スタック削除だけでは消えきらず オブジェクト(と削除マーカー)を先に空にする必要が出がちです。また、検証で手動アタッチした IAM ポリシーが残っていると、スタック削除順でもつまずきます。「実験で足したものは実験で外す」 を徹底すると安全です。
まとめ
フォレンジックの 収集は、技術だけでなく 権限設計と運用の設計が一体になってきます。今回のサンプルは、その中心に S3 と STS と SSM と Step Functions を置いていて、「ケース単位で狭く」「期限付きで」「手順は機械に」 をかなりストレートに体験できます。
やってみて感じたのは次のような点です。
- セッションポリシーでプレフィックスを絞ると、エラーメッセージの
because no session policy allowsが説得力を持って返ってくる - CDK で IAM を組むとき、文字列 ARN の直書きは依存関係の罠になりやすい(参照でつなぐ)
- シングルアカウント検証は楽だが、権限経路が本番とズレるので「なぜそのワークアラウンドが要ったのか」を言語化しておくのが大事
- 証拠バケットは 監査ログ・通知・バージョニングとセットで初めて「証拠っぽさ」が立ち上がる
みなさんのシステムのセキュリティ対応の参考になさってください。
参考リンク
- A framework for securely collecting forensic artifacts into S3 buckets(AWS Security Blog)
- sample-collect-forensic-artifacts-s3(GitHub)
- How to automate forensic disk collection in AWS
- Automated Forensics Orchestrator for Amazon EC2(Solutions)
- Cyber forensics(AWS Prescriptive Guidance)
- awslabs/aws-automated-incident-response-and-forensics(GitHub)







