S3のログにCloudWatchLogsのAlarmを設定する（CloudFormation編）

# はじめに
[前回](https://qiita.com/a_b_/items/6bc63af99c305c906b53)コンソールから作成した、S3の不正アクセスを検出する環境を、CloudFormationで記述しました。

# 構成
複数の環境を想定して、「共通で使うリソース」と「個別の環境で使うリソース」で分けて作成しました。
「共通で使うリソース」のCloudTrailは、監視対象バケットが増えるたびに設定（監視するバケット）を変更する想定です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/29ff83c2-5284-3500-c3da-6398018be1d9.png)

- 共通で使うリソース
    - CloudTrail
        - 監視対象が増えるたびに設定変更します。
            - 初回作成時は監視対象バケットが無いので、作成しないようにします。
    - 証跡ログバケット
    - S3アクセスログ用Log Group
    - SNS
- 個別の環境で使うリソース
    - 監視対象バケット
    - メトリクスとアラート
        - ロググループは一つなので、監視対象バケットの名前などをフィルターに入れて分別しています。

# ClooudFormation
コードは以下になります、説明等に使われている英語の拙さはご容赦ください。
## 共通で使うリソース
01_createBaseEnvForTrail.yml

```
AWSTemplateFormatVersion: 2010-09-09
Description: Create Base Env for Trail on S3. And Add target Bucket for trail.
Parameters:
  TrailBuckets:
    Description: Set S3Bucket Arn(arn:aws:s3:::xxxxxxxxxxx/) for trail. delimiter is connma(,). If bukcet don't exists, set empty (Don't Create CloudTrail) .
    Type: CommaDelimitedList
    
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: User Setting.
        Parameters:
          - TrailBuckets

# パラメータのリストを連結させて空文字なら、CloudTrailを作らない
Conditions:
  CreateTrail:
    !Not [!Equals [ !Join [ '', !Ref TrailBuckets ] , '']]

Resources: 
##################################################
# SNS
##################################################
  IllegalTraficSnsTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: illegal-trafic-sns-topic
      Tags:
          - Key: "usefor"
            Value: "send e-mail detection of illegal trafic"
##################################################
# Cloudwatch log Group
##################################################
  S3AccesslogLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /custom/s3accesslog
      RetentionInDays: 3653   # 未指定時は「失効しない」
##################################################
# Bucket for CloudTrail
##################################################
  OutCloudTrailBucket:
    Type: AWS::S3::Bucket
    Properties:
        BucketName: !Sub "out-cloudtrail-s3accesslogs-${AWS::AccountId}"
        BucketEncryption: 
            ServerSideEncryptionConfiguration: 
              - 
                ServerSideEncryptionByDefault: 
                    SSEAlgorithm: "AES256"
                BucketKeyEnabled: false
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true
  # SSLがないのはNG
  OutCloudTrailBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref OutCloudTrailBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DenyNotSecureTransport
            Effect: Deny
            Principal: "*"
            Resource:
              - !Sub "arn:aws:s3:::${OutCloudTrailBucket}"
              - !Sub "arn:aws:s3:::${OutCloudTrailBucket}/*"
            Action: "*"
            Condition:
                Bool:
                    aws:SecureTransport: false
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:GetBucketAcl
            Resource: !Sub "arn:aws:s3:::${OutCloudTrailBucket}"
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:PutObject
            Resource: !Sub "arn:aws:s3:::${OutCloudTrailBucket}/s3AccessLogTrail/AWSLogs/${AWS::AccountId}/*"
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control
##################################################
# Role
##################################################
  ## CloudTrailにアタッチする、S3Accesslogをロググループに出力可能なロール作成
  S3AccesslogOutRole:
    Type: AWS::IAM::Role
    Condition: CreateTrail
    Properties:
      RoleName: for-s3accesslog-trail
      Path: /
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "cloudtrail.amazonaws.com"
            Action: 'sts:AssumeRole'
      Policies:
        # IAMの参照などを追加
        - PolicyName: out-s3accesslog
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: !GetAtt S3AccesslogLogGroup.Arn
      Tags:
          - Key: "usefor"
            Value: "S3 Accesslog"
##################################################
# CloudTrail
##################################################
  Trail:
    Type: AWS::CloudTrail::Trail
    Condition: CreateTrail
    Properties:
      S3BucketName: !Ref OutCloudTrailBucket
      S3KeyPrefix: "s3AccessLogTrail"
      CloudWatchLogsLogGroupArn: !GetAtt S3AccesslogLogGroup.Arn
      CloudWatchLogsRoleArn: !GetAtt S3AccesslogOutRole.Arn
      EnableLogFileValidation: true
      IsLogging: true
      TrailName: S3AccessLogTrail
      IncludeGlobalServiceEvents: true
      EventSelectors:
        - DataResources:
          - Type: AWS::S3::Object
            Values: !Ref TrailBuckets
          ReadWriteType: All
          IncludeManagementEvents: false

Outputs:
  IllegalTraficSnsTopic:
    Value: !Ref IllegalTraficSnsTopic
    Export:
      Name: IllegalTraficSnsTopic
  S3AccesslogLogGroup:
    Value: !Ref  S3AccesslogLogGroup
    Export:
      Name: S3AccesslogLogGroup

```

- Parameters
    - 監視対象のバケットのArnを、カンマ区切りで複数指定し、リスト形式として扱うようにしています。
- Conditions
    - "CreateTrail"という条件キーに対して、「パラメータがセットされていればTrue」というようにしています。
    - `!Not [!Equals [ !Join [ '', !Ref TrailBuckets ] , '']]`について
        - ”パラメータの部分で生成されたリストを連結した文字列”が、”空文字と一致した場合”、”の否定”をセットしています。
            - CloudFormationでは、[リストをEqualsで比較することはできない](https://qiita.com/Morichan/items/c43fec6ac02536dea2a5)ようなのでこういう風にしています。
- 証跡ログバケット
    - 以下のバケットポリシーを設定しています。
        - SSL暗号化通信していないアクセスを禁止
        - CloudTrailの書き込み
            - コンソールから作成した際の自動生成時に倣い、ACLの取得可と、オブジェクト出力可、の2つを付けています。
- CloudTrailと、CloudTrail用ロール
    - 条件キー"CreateTrail"がTrueの場合に作成するようにしました。

## 個別の環境で使うリソース
02_createEachEnvForTrail.yml

```
AWSTemplateFormatVersion: 2010-09-09
Description: Create Each Env for Trail on S3
Parameters:
  EnvId:
    Type: String
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: User Setting.
        Parameters:
          - EnvId

Resources:
##################################################
# S3
##################################################
  PrivateBucket:
    Type: AWS::S3::Bucket
    Properties:
        BucketName: !Sub "for-${EnvId}-${AWS::AccountId}"
        BucketEncryption: 
            ServerSideEncryptionConfiguration: 
              - 
                ServerSideEncryptionByDefault: 
                    SSEAlgorithm: "AES256"
                BucketKeyEnabled: false
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true
  # SSLがないのはNG
  PrivateBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref PrivateBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DenyNotSecureTransport
            Effect: Deny
            Principal: "*"
            Resource:
              - !Sub "arn:aws:s3:::${PrivateBucket}"
              - !Sub "arn:aws:s3:::${PrivateBucket}/*"
            Action: "*"
            Condition:
                Bool:
                    aws:SecureTransport: false
            
##################################################
# MetricFilter
##################################################
  MetricFilter:
    # フィルタ名は [CFnのスタック名]-[リソース名]-[ランダム文字列]が付く
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !ImportValue S3AccesslogLogGroup
      FilterPattern: !Sub '{($.eventName="GetObject") && (($.sourceIPAddress!="123.123.*") && ($.sourceIPAddress!="234.234.*") ) && ($.requestParameters.bucketName="${PrivateBucket}") }'
      MetricTransformations: 
        - MetricValue: 1
          MetricNamespace: Custom/S3
          MetricName: !Sub ${EnvId}-s3-IllegalTrafic
          DefaultValue: 0
##################################################
# Alarm
##################################################
  LogFilterAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      Namespace: Custom/S3
      MetricName: !Sub ${EnvId}-s3-IllegalTrafic
      Statistic: Sum
      Period: 300                                           # の間に
      ComparisonOperator: GreaterThanOrEqualToThreshold     # の状態を
      Threshold: 1                                          # 回超えることが
      EvaluationPeriods: 1                                  # 周期中
      DatapointsToAlarm: 1                                  # 回なら
      TreatMissingData: notBreaching
      AlarmActions: 
        - !ImportValue IllegalTraficSnsTopic
      AlarmName: !Sub ${EnvId}-s3-IllegalTraficAlarm
      AlarmDescription: !Sub "arn:aws:s3:::${PrivateBucket} にて、不正トラフィック検出"

# CloudTrailを作成するCFnのパラメータに渡す文字列を出力
# （指定時に末尾スラッシュ必要）
Outputs:
  PrivateBucket:
    Value: !Sub "arn:aws:s3:::${PrivateBucket}"
    Export:
      Name: !Sub "PrivateBucket-${EnvId}"
```

- S3は、SSL暗号化通信されている通信のみ可としました。
- フィルターは、「対象のバケットに対しての、特定のIP以外からの、取得アクセス」としました。
- アラームの設定値の説明は推測です。わかる方はご指摘いただけると助かります。
- 出力するS3のArnは、"01_createBaseEnvForTrail.yml"のパラメータにセットします。

# 環境作成手順

## 共通で使うリソース作成
1. 01_createBaseEnvForTrail.ymlを実行。
1. パラメータには、この時点では何も指定しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ff429d8b-70e9-6c80-73d1-c246da16d255.png)

## 個別の環境で使うリソース
1. 02_createEachEnvForTrail.ymlを、構築したい環境分だけ、EnvIdを指定して実行。今回は「gamagama」と「tingara」という環境を作りました。
1. 出力されたS3のArnを控えておきます。

## 共通で使うリソースの、CloudTrail更新
1. ”共通で使うリソース作成”のスタックを更新。"現在のテンプレートの使用"を選びます。
1. パラメータに、"個別の環境で使うリソース"で出力されたS3のArnを、**末尾にスラッシュ(/)を付与し**、**カンマ区切りで**セットし更新します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/228f7d42-d511-0aad-ac26-4b908bd815ea.png)

# 確認
対象のバケットに何か置いて、フィルターで指定したIP範囲外から取得すると、以下のようになります。
今回は5分程度でCloudTrailからログが配信されましたが、20分近くかかることもありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/e7648bdd-51cd-3c97-3060-765216b46841.png)


# 片づけ
1. 各環境のS3バケットを空にします。
1. CloudFormationで、各環境のスタックを削除します。
1. 証跡ログバケットを空にします。
1. CloudFormationで、共通環境のスタックを削除します。


# まとめ
「S3の不正アクセスを検出できる環境」を、CloudFormationで作成しました。
次は以前作成した、[VPCの不正トラフィック検出](https://qiita.com/a_b_/items/e7d0d60d356355dcf5b5)と合わせて、VPC&S3バケットの不正トラフィックの検出環境の構築をCloudFormation化~~したいと思います。~~したものを[記事](https://qiita.com/a_b_/items/f1d53b70ea7f675e2049)にしました。