動機
AWSの操作についてはCloudTrailで記録できる訳ですが、セキュリティの観点から「ヤバそうな操作」は検知・通報されたほうが何かとエンタープライズな組織では安心かと思います。
AWS Configルールで打ち取る、GuardDutyを使う、evident.ioなどの3rdパーティーソリューション利用も考えられますが、お財布のご都合もあるので「まずはざっくりと拾おうぜ」という考え方もあろうかと思いやってみました。
(お財布のご都合がなければ全体的にAWS Configでいい(1項目につき\$0.003、1ルールにつき\$2.00)と思われます)
ヤバそうな操作
IAM関連の操作(ユーザー作成、ロール作成、ポリシーアタッチなど)やS3関連操作(バケット作成、バケットポリシー変更、Website Hostingの設定)あたり。
CloudTrail
- まず「証跡情報」(Trail)を作成
- S3バケットにLogを送るようにすると、そのイベントをSNSトピックに送ることができる
- 届くのは、s3bucket名、ObjectKey。これをトリガーにobjectを取りに行ってgz解凍して、という流れになりそう
- ただ、全部通知が来るので、全量欲しい訳ではない場合は過剰になる
- 「IAMとS3関連のアクションだけ通知したい」には不向きかも
- 今回はCloudWatch Logsに送って、そこからフィルタしたい
CloudTrail Logs
- メトリクスフィルタからのアラームでやると、詳細が分からない
- 検知はできるので、詳細は別途見ればいいという考えならばこれでもOKかも
- そこでログストリーミングをサブスクリプション
- サブスクリプションは1つのロググループに1つしか指定できない
- Lambdaか、Elastic SearchServiceか
- 変更もできない。。(削除して作成)
- サブスクリプションフィルタ
- フィルタパターンの書き方:https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
- ここで大枠を絞っておき、Lambda側で詳細を絞るのが良さそう
- 例:
{ (($.eventSource = "s3.amazonaws.com") || ($.eventSource = "iam.amazonaws.com")) && ($.eventName != "Get*") && ($.eventName != "List*") && ($.eventName != "Describe*")}
- ただしログが大量になる場合にはLambdaだとつらそうなのでここで絞る案もありえる
- 参考:セキュリティーグループが変更されたらSlackに通知する | ハンズラボエンジニアブログ
LambdaでSNSにPublish
- 事前にPublish先のSNSトピックは作っておく
- 参考:CloudWatch LogsのLambdaによるログ監視 | Developers.IO
log2sns/index.js
var zlib = require('zlib');
var aws = require('aws-sdk');
var sns = new aws.SNS({ region: 'ap-northeast-1' });
exports.handler = function(input, context, callback) {
var data = new Buffer(input.awslogs.data, 'base64');
zlib.gunzip(data, function(e, result) {
if (e) {
callback(e);
} else {
result = JSON.parse(result.toString('utf-8'));
var events = result['logEvents'].filter(
function(evt) { return evt['message'].match(process.env.FILTER_PATTERN);}
).filter(
function(evt) { return !evt['message'].match(process.env.FILTER_PATTERN_EXCLUDED) ;}
).map(
function(evt) { return evt['message'] }
);
console.log('processing ' + events.length + '/' + result['logEvents'].length + ' events.');
if (events.length === 0) {
callback();
return;
}
if (0 < events.length) {
// has error log
var subject = 'CloudWatch logs by lambda';
var payload = { default: '' };
payload['default'] += 'NotifyAt: ' + new Date() + '\n';
payload['default'] += 'Log: ' + result['logGroup'] + ' - ' + result['logStream'] + '\n';
payload['default'] += 'Filter: ' + result['subscriptionFilters'] + '\n';
payload['default'] += 'Messages:\n';
payload['default'] += events.join('\n---\n');
sns.publish({
Subject: subject,
Message: JSON.stringify(payload),
MessageStructure: 'json',
TargetArn: process.env.SNS_ARN
}, function(err, data) {
if (err) {
callback(err);
} else {
callback(null, data);
}
});
}
}
});
};
- あとはこのSNSにサブスクライブすれば各種通知が可能に。
つまづいたポイント
- IAMのTrailがストリーミングされてこない・・・
- リージョンがus-east-1のtrailだからではないか?Trailにはあるが、Logsに出ていない。
- 東京のCloudTrail設定が「証跡情報を全てのリージョンに適用」が「いいえ」であるためと思われる。
- リージョン間データ転送が発生するので個人で使う範囲だと落としたい設定
- ドキュメント:https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/how-cloudtrail-works.html
- us-east-1のTrailを有効化、CloudWatch Logsのロググループは共通とするように設定してみる。
- 名前は同じでもLoggroupは別になった・・・(リージョンごとにある)
- ストリーミング先のLambdaも別になる
- Publish先のSNSは同じものを指定できる、とはいえLambdaのロジックが二重管理となってしまう
- そのまま内容を別のLambdaに転送するようにする、など解決策はあろうかと思われるが、シンプルさに欠ける。
雑感
- ”エンタープライズな組織”ならカスタムルールは入れないまでも、AWS Configの費用は誤差範囲のような気もしてきました。
- SNSを挟んでいるので、これをトリガーにSlackへ通知やConnectで電話連絡なんかもできそう。
- さらに「本当にヤバいやつ」はLambdaを呼んで直接Killしにいくような事もできそう。
- でもそこまでやるならCloud Custodianのようなものを検討したほうがいいような気もする。。