1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Control Towerにおける非準拠リソースの通知

Last updated at Posted at 2023-12-11

この記事はカバー株式会社 Advent Calendar 2023 11日目の記事になります。
カバー株式会社でSREをしているSです。よろしくお願いします。

前回の記事は @kura_cvr によるホロライブアプリのアーキテクチャ その2でした。こちらの記事もぜひご覧ください。

この記事について

AWS Control Towerにおいて適用したコントロールに対する非準拠リソースを通知する仕組みについて書きます。

AWS Control Tower

AWS Control Towerは複数アカウント環境を設定して管理するためのサービスです。コントロールによってAWS環境全体にベストプラクティス、標準、規制要件を適用します。その他にもAccount Factoryによるアカウント作成の自動化や、ダッシュボードによってプロビジョニングされているアカウントを監視でき、コントロールに非準拠のリソースを確認できます。

コントロール

AWS Control Towerのコントロールは複数のアカウントを束ねる組織単位(OU)に対して、例えば「SSH を介した無制限のインターネット接続が許可されているかどうかを検出する」といったルールを適用することができます。

検出以外にも、予防やプロアクティブによってリソース作成、更新時にコントロールに非準拠な場合はアクションを禁止することができます。

これによりAWS環境全体に継続的なガバナンスを提供することができます。

非準拠リソースの通知

AWS Control Towerを既存のOrganizationに適用する場合、最初から予防コントロールを適用すると非準拠のリソースを更新できないため生産性が下がります。よって、AWS Control Tower導入時は検出コントロールで非準拠リソースを検出することから始めます。この時に検出された非準拠リソースの情報を通知し素早く修正することが求められます。

検出コントロールはAWS Configルールによって実装されます。

よって、検出コントロールの通知はAWS Configのイベントを捕捉することで実装することができます。
このイベントを捕捉するのは容易で、AWS Control Towerが作成する監査アカウントのSNSにイベントを収集する aws-controltower-AggregateSecurityNotifications トピックが作成されています。このトピックをサブスクライブしてSlackに通知することができます。

しかし、このトピックをAWS Chatbotでサブスクライブするだけでは、非準拠に関するイベント以外のイベントも通知されてしまうためフィルタリングが必要となります。
なので、AWS Lambdaでこのトピックをサブスクライブしつつフィルタリングし、Slackに通知する実装をします。

AWS Lambda

AWS LambdaでAWS Configのイベントにおける NON_COMPLIANT のみをSlackに通知をします。Javascriptの場合は以下の実装になります。

import https from 'https';
import url from 'node:url';
import {
  GetSecretValueCommand,
  SecretsManagerClient,
} from '@aws-sdk/client-secrets-manager';

export const handler = async (event) => {
  // NOTE: ドリフトのイベントが飛んでくる場合がある
  if (event.Records[0].Sns == null) {
    return;
  }

  const message = JSON.parse(event.Records[0].Sns.Message);
  const newComplianceType = message.detail.newEvaluationResult.complianceType;
  if (newComplianceType !== 'NON_COMPLIANT') {
    return;
  }

  const secret = await getSecretValue('config_alert');
  const secrets = JSON.parse(secret);

  const configRuleName = message.detail.configRuleName;
  const region = message.region;

  const payload = {
    icon_emoji: ':exclamation:',
    text: 'AWS Config Rule Compliance State Alert',
    attachments: [
      {
        color: '#ff0000',
        blocks: [
          {
            type: 'section',
            block_id: 'sectionConfigRuleName',
            text: {
              type: 'mrkdwn',
              text: '*Config Rule Name*: ' + configRuleName,
            }
          },
          {
            type: 'section',
            block_id: 'sectionComplianceState',
            text: {
              type: 'mrkdwn',
              text: '*Compliance State*: ' + message.detail.newEvaluationResult.complianceType,
            }
          },
          {
            type: 'section',
            block_id: 'sectionAWSAccountID',
            text: {
              type: 'mrkdwn',
              text: '*AWS Account ID*: ' + message.account,
            }
          },
          {
            type: 'section',
            block_id: 'sectionAWSRegion',
            text: {
              type: 'mrkdwn',
              text: '*AWS Region*: ' + region,
            }
          },
          {
            type: 'section',
            block_id: 'sectionResourceType',
            text: {
              type: 'mrkdwn',
              text: '*Resource Type*: ' + message.detail.resourceType,
            }
          },
          {
            type: 'section',
            block_id: 'sectionResource',
            text: {
              type: 'mrkdwn',
              text: '*Resource*: ' + message.detail.resourceId,
            }
          },
          {
            type: 'section',
            block_id: 'sectionTimestamp',
            text: {
              type: 'mrkdwn',
              text: '*Timestamp*: ' + message.time
            }
          },
          {
            type: 'section',
            block_id: 'sectionLinkToRule',
            text: {
              type: 'mrkdwn',
              text: `*Link to Rule*: https://console.aws.amazon.com/config/home?region=${region}#rules/rules/rule-details/${encodeURIComponent(configRuleName)}`
            }
          },
        ],
      }
    ]
  };

  const body = JSON.stringify(payload);
  await sendMessage(secrets.SLACK_URL, body)
};

const getSecretValue = async (secretName) => {
  const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
  const response = await client.send(new GetSecretValueCommand({ SecretId: secretName }));
  return response.SecretString;
};

const sendMessage = async (uri, body) => {
  let options = url.parse(uri);
  options.method = 'POST';
  options.headers = {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  };

  return new Promise((resolve, reject) => {
    let req = https
      .request(options, (res) => {
        res.on('end', () => {
          resolve(res.statusCode);
        });
      })
      .on('error', (e) => {
        console.log('error postRequest:' + e.message);
        reject(e);
      });
    req.write(body);
    req.end();
  });
};

内容はシンプルで NON_COMPLIANT 以外のイベントはreturnし、 NON_COMPLIANT SlackのWebhookで通知しています。
この関数をTerraformの aws_lambda_function で管理する場合、シークレットがtfstateに乗らないようにAWS Secret Managerからシークレットを取得しています。実行環境がコンテナでない場合はAWS Parameters and Secrets Lambda ExtensionによってAWS Secret Managerからの取得をキャッシュすることもできます。

通知

Slackに通知した結果は以下のようになります。 AWS Account ID などはマスクしています。

image.png

まとめ

AWS Control Towerにおける非準拠リソースの通知について書きました。AWS Lambdaによるフィルタリングの例がなかったためまとめました。皆さんの時間の節約になれば幸いです。

次回は@tk-coverによる「Mermaidでドキュメントを作成する」です。こちらも是非ご覧ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?