【AWS】CloudTrailのログファイルを検証して不正検知を通知する

# 目的
CloudTrailに設定した証跡により出力されたログファイルが、変更または削除されていないか検査する。

ログファイルが変更や削除された際、アラームを発報して任意のメールアドレスに通知するまでの設定を検証する。

## 構成概要
全体の流れをざっくりと記載すると以下のようになります。
1. CloudTrailで証跡を作成し、ログ整合性検査を有効にする
2. ログ整合性確認用Lambda作成しテスト実行する
3. CloudWatchでログメトリクスフィルターを作成しアラームを設定する
4. LambdaをEventBridgeのスケジュールで定期実行を設定する

## 前提
本記事に記載の検証および不正検知の実装を行うには、以下の環境が必要となります。

- 作業端末でAWS CLIのインストールが完了しており、コマンド実行できる環境があること
- 作業対象のAWSアカウント上で、以下の実行権限を持つIAMユーザーがあること
<dl>
    <dd>CloudTrailの操作</dd>
    <dd>CloudTrailに設定したS3に対する操作</dd>
    <dd>CloudWatchでメトリクスフィルターの作成やアラーム作成などの操作</dd>
    <dd>Lambdaの操作</dd>
    <dd>EventBridgeの操作</dd>
</dl>

:::note warn
本記事作成後にサービス内容や仕様が変更になる可能性があります。実際の運用業務で作業する際には、公式ドキュメントをご確認のうえ自己責任でお願いします。
:::

# CloudTrailについて
CloudTrailは、AWSアカウント上での全ての操作を記録・監視するサービスです。
セキュリティの向上、コンプライアンス順守、監査対応、トラブルシューティングなどの目的で使用されます。
そのため、ログファイルの整合性は非常に大切になってきます。

https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/cloudtrail-user-guide.html

# CloudTrailログの整合性検証を有効にする
CloudTrailログの整合性検証を有効にするにはまず、CloudTrailの「証跡」を作成する必要があります。
すでに証跡を作成済みの場合でも、後から変更が可能です。
証跡を作成した後も、ログの出力を停止・再開が可能です。

- AWSマネジメントコンソールから「CloudTrail」を検索します。

検索結果から「CloudTrail」をクリックし、サービスのコンソールを開きます。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/056af5fa-2ccd-07b2-b236-bbde502159e7.png)

- 左ペインの「証跡」を開き、「証跡の作成」をクリックします。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/4b798ffb-60c1-6315-d230-f68c1fdc571a.png)

証跡名やログファイルを送るS3バケットを設定していきます。
既存のS3を使用することもできますが、その際はS3側のバケットポリシーでCloudtrailからのアクセス権限を付与する必要がありますので注意してください。
ログ出力先は後からでも変更可能です。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/b2e06070-34b4-d37d-9959-dcfb427a4598.png)

「その他の設定」にあるログファイルの検証で有効にチェックされていることを確認します。
これはデフォルトで有効になっています。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/cbdfcdd8-5081-568c-fd98-d532b3df4e3b.png)

既に証跡が作成済みの場合は、証跡の「全般的な詳細」で「ログファイルの検証」が「有効」になっていることを確認してください。
なっていない場合は「編集」をクリックして、新規作成の時と同様に「その他の設定」にあるログファイルの検証で有効にチェックを入れてください。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/73a18f5f-a989-5f2c-e531-37c5b7de0b7c.png)

設定を有効にすると、1 時間ごとに過去 1 時間のログファイルを参照し、それぞれのハッシュを含むダイジェストファイルを作成して配信します。
そのハッシュを確認することで整合性を確認しますので、CloudTrailのログが出力されても最大1時間、整合性の検証が配信されないので注意が必要です。
ダイジェストファイルは、CloudTrailの証跡ログに関連付けられた 同一S3バケット内の別フォルダに配信されます。

設定したS3バケットを確認すると、フォルダが自動作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/9f6d1406-ea27-f60a-ad51-a7cd4c68ad85.png)

# 整合性を検査する
[公式](https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html "AWS公式サイト")には、
>CloudTrail ログファイルの整合性を検証するには、AWS CLI を使用するか、独自のソリューションを作成することができます。

とありますので、まずはAWS CLIで動作を検証していきます。
ただし、検査には以下の条件があります。

- ダイジェストファイルとログファイルを含む Amazon S3 バケットへの読み取りアクセスが必要です。
- ダイジェストファイルとログファイルは、CloudTrail が配信した元の Amazon S3 の場所から移動してはいけません。

また、検査できる内容は以下の通りです。

- CloudTrail ログファイルの変更または削除
- CloudTrail ダイジェストファイルの変更または削除
- 上記の両方の変更または削除

## 実行するコマンド
コマンドの構文は次のようになっています。
`aws cloudtrail validate-logs --trail-arn <trailARN> --start-time <start-time> [--end-time <end-time>] [--s3-bucket <bucket-name>] [--s3-prefix <prefix>] [--account-id <account-id>] [--verbose]`

[]で括られたオプションについては必須ではありません。

以下、オプションについての補足です。
- trail-arn
`arn:aws:cloudtrail:<リージョン名>: <AWSアカウント>:trail/<証跡名>`

## 正常な状態
ログファイルにもダイジェストファイルにも変更や削除が行われていない正常な状態でコマンドを実行した際の、検査結果サマリの出力例は以下のようになります。
```
11/11 digest files valid
494/494 log files valid
```
- 11/11 digest files valid:
ダイジェストファイルの整合性が検査された結果です。
そのうち11つが有効である（不正はない）ことを示しています。

- 494/494 log files valid
ログファイルの整合性が検査された結果です。
そのうち494つが有効である（不正はない）ことを示しています。

## ダイジェストファイルが変更/削除された状態
```
10/11 digest files valid, 1/11 digest files INVALID
494/494 log files valid
```
正常な状態とは異なり、「1/11 digest files INVALID」となります。
11件のダイジェストファイル中1件が、整合性が取れなかったという結果です。

今回は不正を検知したらアラームを飛ばしたいため、この「INVALID」に着目し、CloudWatchでログ監視を実装します。

## 補足
前述でログファイルの整合性検査は開始/停止を操作できると記載しました。
検査を停止した場合、停止されていた期間が検査結果にも出力されます。

（出力例）
```
No log files were delivered by CloudTrail between 2024-01-15T07:29:15Z and 2024-01-15T21:52:29Z
```
# 整合性確認用Lambda
今回はLambdaからAWS CLIコマンドを呼び出して実行する手法をとります。
以下のPythonコードをデプロイします。
Lambdaの設定でタイムアウトのデフォルトが3秒になっています。検査対象ログの量にも影響されますが、こちら処理に時間がかかりますので、長めに設定しておかないとエラーとなります。

```python
import subprocess
import os
from datetime import datetime, timedelta, timezone

def lambda_handler(event, context):
    # Lambda関数の環境変数PATHにPythonのバイナリパスを追加
    os.environ['PATH'] = '/opt/python/bin:' + os.environ['PATH']
    
    # 現在のUTC時刻を取得
    current_utc_time = datetime.utcnow()
    
    # 昨日の同じ時刻に変更
    yesterday_utc_time = current_utc_time - timedelta(days=1)
    
    # 昨日の0時0分のUTC時刻を設定
    yesterday_midnight_utc = datetime(
        year=yesterday_utc_time.year,
        month=yesterday_utc_time.month,
        day=yesterday_utc_time.day,
        hour=0,
        minute=0,
        second=0,
        microsecond=0,
        tzinfo=timezone.utc
    )
    
    # 出力する形式を指定して文字列に変換
    formatted_time = yesterday_midnight_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
    
    try:
        # AWS CLIコマンドの例
        aws_command = '/opt/aws cloudtrail validate-logs --trail-arn arn:aws:cloudtrail:ap-northeast-1:<AWSアカウント>:trail/Test_Cloudtrail --start-time ' + formatted_time

        # AWS CLIを呼び出す
        result = subprocess.run(aws_command, shell=True, capture_output=True, text=True)

        # 結果をログに出力する
        print(result)

        return {
            'statusCode': 200,
            'body': 'AWS CLI command executed successfully.'
        }
    except Exception as error:
        print('Error executing AWS CLI command:', error)
        return {
            'statusCode': 500,
            'body': 'Error executing AWS CLI command.'
        }

```
:::note info
2024年1月時点でLambdaではAWS CLIをデフォルトで実装していません。
コマンドの実行にはLambdaLayerを使用するなどしてAWS CLIコマンドを環境に用意する必要があります。

またAWS CLI以外の方法で実装できないかと思いSDKを用いて調査・検証しましたが、私個人の調査レベルにおいて、今回実装したい内容と同様の事は実現できませんでした。
:::

## 実行結果の確認
手動でテスト実行を行い、CloudWatchLogsに出力されたログを確認します。
実際は1行につながって出力されますが、見やすいように改行すると以下になります。

```
CompletedProcess(
args=
'/opt/aws cloudtrail validate-logs --trail-arn arn:aws:cloudtrail:ap-northeast-1:\<AWSアカウント>:trail/Test_Cloudtrail --start-time 2024-01-14T00:00:00Z', returncode=1, 

stdout=
'Validating log files for trail arn:aws:cloudtrail:ap-northeast-1:\<AWSアカウント>:trail/Test_Cloudtrail between 2024-01-14T00:00:00Z and 2024-01-17T08:11:05Z\n\n

Results requested for 2024-01-14T00:00:00Z to 2024-01-17T08:11:05Z\nResults found for 2024-01-14T23:29:15Z to 2024-01-15T23:52:29Z:\n\n

10/11 digest files valid, 1/11 digest files INVALID\n
494/494 log files valid\n', 

stderr=
'No log files were delivered by CloudTrail between 2024-01-15T07:29:15Z and 2024-01-15T21:52:29Z\n\n
Digest file\ts3://test-cloudtrail-training/AWSLogs/\<AWSアカウント>/CloudTrail-Digest/ap-northeast-1/2024/01/14/\<AWSアカウント>_CloudTrail-Digest_ap-northeast-1_Test_Cloudtrail_ap-northeast-1_20240114T232915Z.json.gz\t
INVALID: signature verification failed\n\n')
```
今回検証で変更を加えたファイル名がログにも出力されました。
> \<AWSアカウント>_CloudTrail-Digest_ap-northeast-1_Test_Cloudtrail_ap-northeast-1_20240114T232915Z.json.gz

また、「INVALID」の文字列が出力されたことも確認できました。

整合性検査を停止していた期間については、「stderr」として出力されています。

# ログメトリクスフィルターの設定
CloudWatchLogsで「INVALID」の文字を検出する設定を行います。

AWSコンソールからCloudWatchサービスを開き、左ペインの ログ > ロググループ を開きます。
ロググループ一覧から、今回作成したLambdaのロググループを開き、アクションから「メトリクスフィルターを作成」を選択します。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/bf029f02-9f58-a405-4b5e-b6136fa3c87b.png)

フィルターパターンに「INVALID」を入力し、先ほどテスト実行したLambdaのログストリームを選択します。

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/1e26777a-9366-4852-7477-d82c52b22907.png)

「パターンをテスト」からフィルターが機能しているか確認し、結果が出力されたらNextで次の設定に進みます。

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/f82b474e-cd16-5bf9-880e-64864be4a18f.png)

メトリクスを設定してNextで次に進みます。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/41a60e48-e825-b3b9-8509-b94c7a091154.png)
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/fc5c830d-2452-603b-6e81-cd6aa01b8db6.png)

メトリクスフィルターを作成し、結果を確認します。

![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/eeee730a-fd51-819e-1592-8e98cbbac255.png)

検証で用意した環境を使って、わざと「INVALID」を出力させフィルターが機能しているか確認します。
メトリクスをクエリし、情報が拾えていることが確認できます。

![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/c56c83ab-b974-1504-625b-6a67b7839617.png)

# アラームの設定

アラームの作成から、先ほど作成したメトリクスを条件に設定します。
1件でも検出されればアラーム状態にしたいため、統計は「最大」にしておきます。

![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/15497030-7fe4-7b98-cc75-c5a880d5e6d8.png)

その他、今回は以下の様な設定にしました。

![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/563b0367-eb1b-b937-35c7-cc388886661d.png)

アクションの設定で、通知したいメールを設定したSNSトピックを作成します。
既にSNSトピック作成済みの場合は、既存のSNSトピックの設定も可能です。

![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/5de77ea3-8eff-d239-4ae6-952cf173e441.png)

# EventBridgeのスケジュール

作成した整合性確認用Lambdaを定期実行するようEventBridgeのスケジューラーで設定します。
詳細な設定方法はここでは割愛します。

# アラームメールの確認

再度わざと「INVALID」を出力させ、設定したメールにアラームが発報されるか確認します。

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348322/9c1b4073-ca93-2f0c-4d18-de1483bfc9f6.png)

# 最後に
以上で検証及び設定の実装を終了します。

あとはEventBridgeのスケジューラーがちゃんと動くのを確認したりとあると思いますが、おおまかな設定はここまでとなります。