AWS Inspectorで定期セキュリティスキャンを実行して結果をSlackに通知する
Tweetデジカでは多数の AWS の機能を利用して自動化を進めていますが、今回はそのうちのひとつ、AWS Inspector の定期実行と実行結果の Slack への通知をどのようにして実現したかを説明します。
AWS Inspector
あまり広くは知られていなようですが、AWS には Inspector というセキュリティスキャンのサービスがあります。 EC2 インスタンスにエージェントをインストールしておくと、AWS の Web コンソールや API からインスタンスのセキュリティスキャンを実行することができます。 スキャンする内容はいくつかルールがあって、実行するルールを選択できます。例えば CVEルール (CVE に登録されている脆弱性が存在するパッケージを使用していないか) や、Security Best Practices ルール (システムの設定がベストプラクティスに従っているか、例えば sshd の設定で root のログインを許可していないかなど) があります。
全体像
以下で説明する設定を全部実行すると、AWS Inspector が定期的に対象インスタンスのスキャンを実行し、スキャンが完了すると Slack に結果を通知します。下図はその全体像です。
ソースコード
本記事で説明するソースコードは GitHub 上で公開しています。詳細に関してはソースコードを確認してください。 https://github.com/degica/scheduled-inspector-run
レポジトリ構成
説明する動作を実現するため以下の機能を利用していますが、各機能の概要説明は省略します。
- AWS CloudWatch Events (Scheduled Events)
- AWS Lambda
- AWS CloudFormation + SAM (Serverless Application Model)
- AWS SNS
- AWS Inspector
使い方
README に書いてあるとおりですが、改めて説明します。
Inspector agent のインストール
スキャン対象のインスタンスには Inspector のエージェントをインストールしておきます。 インストール方法は 公式ドキュメント を参照してください。
Inspector のセットアップ
まず、AWS Web コンソールから AWS Inspector Target と Template を作成します。 Targetはスキャン対象のインスタンスのタグを指定します。 Templateでは、お好みのルールパッケージを選択してください。
必要リソースの作成
awscli のインストール、レポジトリのcloneして以下のコマンドを実行することで必要なリソースのセットアップができます。
# Create a SAM package
$ aws cloudformation package \
--template-file ./template.yaml \
--s3-bucket=<S3 bucket name> \
--output-template-file packaged-template.yml
# Deploy the package
$ aws cloudformation deploy \
--region=ap-northeast-1 \
--template-file ./packaged-template.yml \
--stack-name scheduled-inspector-run \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
TemplateArns=<comma-separated AWS Inspector template ARNs> \
Schedule="cron(45 5 ? * * *)" \
SlackUrl=<Slack URL>
TemplateArns
には作成した Inspector Template の ARN を指定してください。Schedule
にはInspector実行のスケジュールを指定できます。(UTC)SlackUrl
には、結果通知先の Slack の Webhook URL を指定します。
Inspector Template のイベント通知先 SNS トピックの設定
作成したリソースには Inspector の結果通知先の SNS トピックが含まれています。Template の設定画面からこの SNS トピックを通知先として指定してください。イベントは “Run finished” だけでよいです。
動作説明
Inspector の定期実行
Inspector は AWS API から実行開始をすることができます。今回は CloudWatch Events の Scheduled Event を使用して、定期的に Lambda Function を実行し、Lambda Function が Inspector の API (inspector:StartAssessmentRun
) を呼び出す構成にしました。
const AWS = require('aws-sdk')
const inspector = new AWS.Inspector()
const templateArns = process.env.assessmentTemplateArns.split(',')
exports.handler = (event, context, callback) => {
const runs = templateArns.map((t) => {
new Promise((resolve, reject) => {
const params = {
assessmentTemplateArn: t
}
inspector.startAssessmentRun(params, (error, data) => {
if (error) {
console.log(error, error.stack);
reject(error)
}
console.log(data);
resolve(data);
});
})
})
Promise.all(runs).then((results) => {
callback(null, results)
}).catch((error) => {
console.log('Caught Error: ', error)
callback(error)
})
}
結果の Slack への通知
AWS Inspector Template に結果通知先のSNSトピックとして、CloudFormationにより作成したトピックを設定しておきます。説明したとおり、この部分はCloudFormationで自動化できないので、手動でSNSトピックの設定をする必要があります。SNSトピックは Publish
されると、Lambda Functionを実行し、Lambda Function はSlackチャンネルに結果をポストします。
const AWS = require('aws-sdk');
const inspector = new AWS.Inspector({region: 'ap-northeast-1'});
const url = require('url');
const https = require('https');
const util = require('util');
function postMessage(message, callback) {
const body = JSON.stringify(message);
const options = url.parse(process.env.SLACK_URL);
options.method = 'POST';
options.headers = {'Content-Type': 'application/json'};
const postReq = https.request(options, (res) => {
const chunks = [];
res.setEncoding('utf8');
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
if (callback) {
callback({
body: chunks.join(''),
statusCode: res.statusCode,
statusMessage: res.statusMessage
});
}
});
return res;
});
postReq.write(body);
postReq.end();
}
exports.handler = (event, context, callback) => {
const sns = event.Records[0].Sns
const message = JSON.parse(sns.Message)
console.log(util.inspect(event, {showHidden: false, depth: null}))
if(message.event != "ASSESSMENT_RUN_COMPLETED") {
return callback(null, null)
}
const templateNameP = new Promise((resolve, reject) => {
inspector.describeAssessmentTemplates({assessmentTemplateArns: [message.template]}, (error, data) => {
if (error) {
return reject(error)
}
return resolve(data)
})
})
const listFindingsP = new Promise((resolve, reject) => {
const params = {
assessmentRunArns: [message.run],
filter: {
severities: ["High"]
},
maxResults: 500
}
inspector.listFindings(params, (error, data) => {
if (error) {
return reject(error)
}
return resolve(data)
})
})
Promise.all([templateNameP, listFindingsP]).then((results) => {
const templateName = results[0].assessmentTemplates[0].name
const findings = results[1].findingArns
const highCount = findings.length
const slackMessage = {
username: "AWS Inspector Report",
attachments: [
{
pretext: "Assessment Run Completed",
text: `${highCount} high severities found\n\nTemplate Name: ${templateName}\nRun ARN: ${message.run}`,
color: (highCount == 0) ? 'good' : 'warning'
}
]
}
postMessage(slackMessage, (res) => {
callback(null, results);
})
}).catch((error) => {
console.log('Caught Error: ', error)
console.log(error.stack)
callback(error)
})
}
デジカでの活用
デジカではこのレポジトリをそのまま使用して、毎日朝10時に全インスタンスをスキャンし、結果を Slack に通知しています。もし、Severity “High” の結果があった場合は都度適切な対応を実施します。インスタンスの種別、アラートの種別にもよりますが通常、該当インスタンスを削除して新しいものに置き換えます。(新しいインスタンスは起動時にパッケージのセキュリティ更新を自動で実行します)
今後の応用
現在は単純に定期実行して結果を通知しているだけですが、いくつかの変更を加えることでさらに高度な自動化を実現できます。例えば
- インスタンス起動時に AWS Inspector のスキャンを実行して、スキャン結果に問題がなかった場合のみWebサービスを起動してインスタンスをELBにアタッチする
- Severity “High” のアラートが出た場合は該当のインスタンスを自動的にシャットダウンする
なにかおもしろいアイデアがあったらぜひ GitHub の Issue で教えてください。