VPC-S3の環境で、不正なデータ送信をAlarmする環境を、CloudFormationで構築する

# はじめに
以前作成した、[VPCの内部から不正なデータ送信のアラームを送信する環境](https://qiita.com/a_b_/items/e7d0d60d356355dcf5b5)と、[S3の不正アクセスを検出する環境](https://qiita.com/a_b_/items/2d2c6de3c4c7353c4dca)を、一つにしたCloudFormationを作成しました。

# 構成
今回も、複数の環境を想定して、「共通で使うリソース」と「個別の環境で使うリソース」で分けて作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/11a360b7-ebf5-401d-5b96-0696f1d5a551.png)

以前と少し異なる点は、VPCとCloudTrailにアタッチさせるIAMロール（CloudWatch Logs出力権限）を共通にしました。
それ以外のリソース等は、以前([VPC](https://qiita.com/a_b_/items/e7d0d60d356355dcf5b5#%E6%A7%8B%E6%88%90)と[S3](https://qiita.com/a_b_/items/2d2c6de3c4c7353c4dca#%E6%A7%8B%E6%88%90))のものと同じです。

# CloudFormation
コードは以下になります、説明等に使われている英語の拙さはご容赦ください。

## 共通で使うリソース

01_createBaseEnvForTrail.yml

```
AWSTemplateFormatVersion: 2010-09-09
Description: Create Base Env for Trail on VPC and 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: 
##################################################
# Common Resource
##################################################
# SNS
  IllegalTraficSnsTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: illegal-trafic-sns-topic
      Tags:
          - Key: "usefor"
            Value: "send e-mail detection of illegal trafic"
# Role for Output to Log Group
  ## ロググループに出力可能なロール作成
  OutputRoleToLogGroup:
    Type: AWS::IAM::Role
    Properties:
      RoleName: for-outputto-loggroup
      Path: /
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "vpc-flow-logs.amazonaws.com"
                - "cloudtrail.amazonaws.com"
            Action: 'sts:AssumeRole'
      Policies:
        # IAMの参照などを追加
        - PolicyName: outputto-loggroup
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "logs:DescribeLogGroups"
                  - "logs:DescribeLogStreams"
                Resource: '*'
      Tags:
          - Key: "usefor"
            Value: "output to LogGroup"

##################################################
# VPC Resource
##################################################
# Cloudwatch log Group
  VpcFlowlogsLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /custom/vpcflowlogs
      RetentionInDays: 3653   # 未指定時は「失効しない」

##################################################
# S3 Resource
##################################################
# 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
# CloudTrail
  Trail:
    Type: AWS::CloudTrail::Trail
    Condition: CreateTrail
    Properties:
      S3BucketName: !Ref OutCloudTrailBucket
      S3KeyPrefix: "s3AccessLogTrail"
      CloudWatchLogsLogGroupArn: !GetAtt S3AccesslogLogGroup.Arn
      CloudWatchLogsRoleArn: !GetAtt OutputRoleToLogGroup.Arn
      EnableLogFileValidation: true
      IsLogging: true
      TrailName: S3AccessLogTrail
      IncludeGlobalServiceEvents: true
      EventSelectors:
        - DataResources:
          - Type: AWS::S3::Object
            Values: !Ref TrailBuckets
          ReadWriteType: All
          IncludeManagementEvents: false

Outputs:
##################################################
# Common Resource
##################################################
  IllegalTraficSnsTopic:
    Value: !Ref IllegalTraficSnsTopic
    Export:
      Name: IllegalTraficSnsTopic
  OutputRoleArnToLogGroup:
    Value: !GetAtt OutputRoleToLogGroup.Arn
    Export:
      Name: OutputRoleArnToLogGroup
##################################################
# VPC Resource
##################################################
  VpcFlowlogsLogGroup:
    Value: !Ref VpcFlowlogsLogGroup
    Export:
      Name: VpcFlowlogsLogGroup
##################################################
# S3 Resource
##################################################
  S3AccesslogLogGroup:
    Value: !Ref  S3AccesslogLogGroup
    Export:
      Name: S3AccesslogLogGroup
```

## 個別の環境で使うリソース

02_createEachEnvForTrail.yml

```
AWSTemplateFormatVersion: 2010-09-09
Description: Create Each Env for Trail on VPC and S3.
Parameters:
  EnvId:
    Type: String
  VpcCidr:
    Type: String
    Default: 192.168.209.0/24
  SubnetACidr:
    Type: String
    Default: 192.168.209.0/26
  SubnetCCidr:
    Type: String
    Default: 192.168.209.64/26
  SubnetDCidr:
    Type: String
    Default: 192.168.209.128/26
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: User Setting.
        Parameters:
          - EnvId
          - VpcCidr
          - SubnetACidr
          - SubnetCCidr
          - SubnetDCidr

Resources:
##################################################
# VPC
##################################################
  Vpc:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref VpcCidr
      # EnableDnsHostnames: true
      # EnableDnsSupport: true
      Tags: 
        - Key: Name
          Value: !Sub "${EnvId}-vpc"
##################################################
# Security Group
##################################################
  SecGrpOnlyInternal:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable access from EC2 Instance Connect IP
      GroupName: !Sub "${EnvId}-sg"
      VpcId: !Ref Vpc
      # For EC2 Instance Connect
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 3.112.23.0/29
  SGBaseIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref SecGrpOnlyInternal
      IpProtocol: -1
      SourceSecurityGroupId: !GetAtt SecGrpOnlyInternal.GroupId
##################################################
# IGW
##################################################
  Igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-igw
  IgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref Igw
      VpcId: !Ref Vpc
##################################################
# Subnets
##################################################
  SubnetA:
    Type: AWS::EC2::Subnet
    Properties: 
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref SubnetACidr
      MapPublicIpOnLaunch: false
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-subnet-a
  SubnetC:
    Type: AWS::EC2::Subnet
    Properties: 
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref SubnetCCidr
      MapPublicIpOnLaunch: false
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-subnet-c
  SubnetD:
    Type: AWS::EC2::Subnet
    Properties: 
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1d
      CidrBlock: !Ref SubnetDCidr
      MapPublicIpOnLaunch: false
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-subnet-d
##################################################
# Routing
##################################################
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref Vpc
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-public-rtb
  RouteTableAssocA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetA
  RouteTableAssocC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetC
  RouteTableAssocD:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetD
  Route:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref Igw

##################################################
# 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

##################################################
# VPC Endpoint to S3
##################################################
  VPCS3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Gateway
      RouteTableIds: 
        - !Ref RouteTable
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcId: !Ref Vpc
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - "s3:*"
            Resource:
              - !Sub "arn:aws:s3:::${PrivateBucket}"
              - !Sub "arn:aws:s3:::${PrivateBucket}/*"
            
##################################################
# VPC FlowLogs
##################################################
  VpcFlowLogs:
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceType: VPC
      ResourceId: !Ref Vpc
      TrafficType: ALL
      DeliverLogsPermissionArn: !ImportValue OutputRoleArnToLogGroup
      LogGroupName: !ImportValue VpcFlowlogsLogGroup
      LogFormat: '${account-id} ${action} ${az-id} ${bytes} ${dstaddr} ${dstport} ${end} ${flow-direction} ${instance-id} ${interface-id} ${log-status} ${packets} ${pkt-dst-aws-service} ${pkt-dstaddr} ${pkt-src-aws-service} ${pkt-srcaddr} ${protocol} ${region} ${srcaddr} ${srcport} ${start} ${sublocation-id} ${sublocation-type} ${subnet-id} ${tcp-flags} ${traffic-path} ${type} ${version} ${vpc-id}'
      Tags: 
        - Key: Name
          Value: !Sub ${EnvId}-flowlog
##################################################
# Role
##################################################
  # EC2アタッチ用ロール作成
  Ec2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub for-ec2-on-${EnvId}
      Path: /
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "ec2.amazonaws.com"
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: s3fullaccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "s3:*"
                Resource:
                  - !Sub "arn:aws:s3:::${PrivateBucket}"
                  - !Sub "arn:aws:s3:::${PrivateBucket}/*"
  ## インスタンスプロファイル作成
  MyInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref Ec2Role

##################################################
# MetricFilter
# フィルタ名は [CFnのスタック名]-[リソース名]-[ランダム文字列]が付く
##################################################
  # VPC FlowLogs
  MetricFilterOfVpcFlowLogs:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !ImportValue VpcFlowlogsLogGroup
      FilterPattern: !Sub "[account_id, action, az_id, bytes > 1000000, dstaddr!=52.219.* && dstaddr!=3.5.* && dstaddr!=35.72.164.* && dstaddr!=35.73.115.* , dstport, end, flow_direction = egress, instance_id, interface_id, log_status, packets, pkt_dst_aws_service!=EC2_INSTANCE_CONNECT, ..., vpc_id=${Vpc}]"
      MetricTransformations: 
        - MetricValue: 1
          MetricNamespace: Custom/VPC
          MetricName: !Sub ${EnvId}-vpc-IllegalTrafic
          DefaultValue: 0
  # S3 AccessLog
  MetricFilterOfS3AccessLog:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !ImportValue S3AccesslogLogGroup
      FilterPattern: !Sub '{($.eventName="GetObject") && (($.sourceIPAddress!="123.123.123.123") && ($.sourceIPAddress!="234.234.*") && ($.vpcEndpointId!="${VPCS3Endpoint}" || $.vpcEndpointId NOT EXISTS)) && ($.requestParameters.bucketName="${PrivateBucket}")}'
      MetricTransformations: 
        - MetricValue: 1
          MetricNamespace: Custom/S3
          MetricName: !Sub ${EnvId}-s3-IllegalTrafic
          DefaultValue: 0
##################################################
# Alarm
##################################################
  # VPC FlowLogs
  LogFilterAlarmOfVpcFlowLogs:
    Type: AWS::CloudWatch::Alarm
    Properties:
      Namespace: Custom/VPC
      MetricName: !Sub ${EnvId}-vpc-IllegalTrafic
      Statistic: Sum
      Period: 300                                           # の間に
      ComparisonOperator: GreaterThanOrEqualToThreshold     # の状態を
      Threshold: 1                                          # 回超えることが
      EvaluationPeriods: 1                                  # 周期中
      DatapointsToAlarm: 1                                  # 回なら
      TreatMissingData: notBreaching
      AlarmActions: 
        - !ImportValue IllegalTraficSnsTopic
      AlarmName: !Sub ${EnvId}-vpc-IllegalTraficAlarm
      AlarmDescription: !Sub "${EnvId} vpc にて、不正トラフィック検出"
  # S3 AccessLog
  LogFilterAlarmOfS3AccessLog:
    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} にて、不正トラフィック検出"

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

以前と異なる点は以下になります。

- VPCフローログのフィルタ
    - サイズを"1MBを超える"にしました。
    - "EC2_INSTANCE_CONNECT経由のトラフィック"を除外
        - `pkt_dst_aws_service!=EC2_INSTANCE_CONNECT,`
        - 使ってみると、ここのトラフィックに流れるデータのサイズが大きかったため除外。
- S3アクセスログのフィルタ
    - 同じスタックで作成したVPCのエンドポイント経由を除外
        - `($.vpcEndpointId!="${VPCS3Endpoint}" || $.vpcEndpointId NOT EXISTS)`
        - `$.vpcEndpointId!=xxxx`だけだと、**VPCエンドポイントを経由しないアクセスにもFalseとなる**ようだったので、`$.vpcEndpointId NOT EXISTS`をORで結合しました。

# 環境作成手順、片づけ手順
作成の仕方は[S3の時](https://qiita.com/a_b_/items/2d2c6de3c4c7353c4dca#%E7%92%B0%E5%A2%83%E4%BD%9C%E6%88%90%E6%89%8B%E9%A0%86)と、片づけの仕方も[S3の時](https://qiita.com/a_b_/items/2d2c6de3c4c7353c4dca#%E7%89%87%E3%81%A5%E3%81%91)と同様です。

# まとめ
PC&S3バケットの不正トラフィックの検出環境の構築をCloudFormation化しました。
課題として「パブリックのサービスからのアクセスに対してアラームになる」という点ありますので、折を見て探ってみたいと思います。