背景
あるプロジェクトでお客様から下記2つの要件がありました。
- セキュリティ要件として、ログの出力時にパスワードといった個人情報を出力してはダメ
- 監査目的で7年間ログを保持する必要があるが、出来るだけログ保持のランニングコストを安くしていきたい
解決策
背景で述べた課題について下記のような解決策で対応。
No | 課題 | 解決策 |
---|---|---|
1 | ログの出力時、個人情報の出力を抑制 | Cloudwatchにマスキングの設定を行う ※1 |
2 | 出来るだけログの保持のランニングコストを安くする | CloudWatchのログを毎日S3へ出力し、Cloudwatchでは3か月間のみ保持する ※2 |
※1:https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html
※2:Cloudwatchの保存料金はUSD 0.033/GB。S3(標準)の保存料金は0.025USD/GB。 (2024/6時点の東京リージョンでの料金)
疑問
お客様から出されたそれぞれの要件に対して
解決策に記載した通りの内容で対処を進めていましたが
No1にてCloudwatch上でマスキングしていたものを、No2でS3へ出力した際
マスキングしていたものはどうなるのか気になったので検証してみました。
検証準備
Cloudwatchでマスキング設定
マスキング対象のログ
今回マスキング対象のログとして下記のサンプルを使用します。
本サンプルでマスキング対象となるのは、「password」と「mailAddress」になります。
{
"id": "199",
"password": "cX3T*FMZ",
"mailAddress": "hogehoge@gmail.com",
"createdAt": "2020-02-20T11:00:28.107Z"
}
マスキングの設定
マスキングの設定は「Cloudwatch->設定->ログ」から行い
「メールアドレス」、「パスワード」のマスキングはそれぞれ下記の機能を利用します。
- メールアドレスのマスキング:データ保護アカウントポリシー
- パスワードのマスキング :カスタムデータ識別子
メールアドレスのマスキング
メールアドレスは、[1]にあるようにAWS側でマスキングの機能が用意されているためこちらの機能を使用します。
[1]保護できるデータの種類
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types.html
【設定手順】
①:データ保護アカウントポリシー設定を選択
②:アカウントポリシーからマスキングの対象としてEmailAddressを選択し、データ保護をアクティブ化する
パスワードのマスキング
メールアドレスとは違い、パスワードはマスキングできる対象に含まれていないようなので
カスタムデータ識別子を利用して、マスキングを行います。
【設定手順】
①:データ保護アカウントポリシーの設定を選択 (メールアドレスのマスキングの手順①と同様)
②:アカウントポリシーからカスタムデータ識別子を選択し、パスワードを検出する正規表現を設定し、データ保護をアクティブ化する
今回使用した正規表現("password"[^,]*")は
「"password"から始まり、シングルクオーテーションまで」をマスキング対象としています。
マスキングの結果を確認
マスキング対象のログ を実際にCloudWatchに出力した結果が下記になります。
→ 「メールアドレス」と「パスワード」がマスキングされています。
CloudwatchのログをS3に出力する
Lambda作成
CloudwatchからS3へ出力するLambdaを作成します。(Node.js 20.x)
CloudwatchからS3への出力は、「CreateExportTaskCommand」を使用します。
import { CloudWatchLogsClient, CreateExportTaskCommand, DescribeExportTasksCommand } from "@aws-sdk/client-cloudwatch-logs";
const client = new CloudWatchLogsClient({ region: "your-region" });
export const handler = async (event) => {
// パラメータの設定
const logGroupName = 'your-log-group-name';
const bucketName = 'your-bucket-name';
// 現在の日時を取得
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
// プレフィックスの設定 (ロググループ名/yyyy/mm/dd)
const destinationPrefix = `${logGroupName}/${year}/${month}/${day}`;
// エクスポートタスクの時間範囲を設定
const startTime = now.setHours(now.getHours() - 1); // 1時間前
const endTime = Date.now(); // 現在
// エクスポートタスクの作成
try {
const createCommand = new CreateExportTaskCommand({
taskName: 'ExportLogsToS3',
logGroupName: logGroupName,
from: startTime,
to: endTime,
destination: bucketName,
destinationPrefix: destinationPrefix
});
const createResponse = await client.send(createCommand);
const taskId = createResponse.taskId;
console.log(`Export task created with ID: ${taskId}`);
// エクスポートタスクのステータスを確認
const describeCommand = new DescribeExportTasksCommand({
taskId: taskId
});
let taskStatus = 'PENDING';
let taskMessage = '';
while (taskStatus === 'PENDING' || taskStatus === 'RUNNING') {
const describeResponse = await client.send(describeCommand);
const exportTasks = describeResponse.exportTasks;
if (exportTasks && exportTasks.length > 0) {
const task = exportTasks[0];
taskStatus = task.status.code;
taskMessage = task.status.message || '';
console.log(`Task Status: ${taskStatus}, Message: ${taskMessage}`);
}
if (taskStatus === 'PENDING' || taskStatus === 'RUNNING') {
await new Promise(resolve => setTimeout(resolve, 10000)); // 10秒待機
}
}
if (taskStatus === 'COMPLETED') {
console.log('Export task completed successfully.');
} else {
console.log(`Export task failed with status: ${taskStatus}, message: ${taskMessage}`);
}
return {
statusCode: 200,
body: `Export task completed with status: ${taskStatus}, message: ${taskMessage}`
};
} catch (error) {
console.error(`Error creating or describing export task: ${error}`);
return {
statusCode: 500,
body: `Error creating or describing export task: ${error}`
};
}
};
Lambdaが使用するロールに適切な権限を付与
S3へ出力する権限をLambdaに持たせるため、下記の内容をLambdaにアタッチするIAMロールに含めます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateExportTask",
"logs:DescribeExportTasks",
"logs:CancelExportTask"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
S3バケットポリシーの修正
Cloudwatchからのログ出力を受け止められるようにS3のバケットポリシーに下記を設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::your-bucket-name",
"Condition": {
"StringLike": {
"aws:SourceArn": "your-log-group-arn"
}
}
},
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::from-cloudwatch-to-s3-yy/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
},
"StringLike": {
"aws:SourceArn": "your-log-group-arn"
}
}
}
]
}
検証結果
マスキング対象のログ をCloudwatchに出力後、Lambda作成 で用意したLambdaを実行し、S3に出力されたログファイルを確認すると、マスキングされたまま出力されました。
◆S3に出力されたログ
2024-06-15T04:39:21.385Z test
2024-06-15T04:35:41.269Z { "id": "199", **********************, "mailAddress": "******************", "createdAt": "2020-02-20T11:00:28.107Z" }
注意事項
Cloudwatchにログを作成してから、すぐに「CreateExportTaskCommand」を実行してもログが出力されないことがありました。
→ ログの出力対象と認識されるまでに多少のラグがある・・・?