S3への署名バージョン2を使用したアクセスをCloudTrail、CloudWatch Events、CloudWatch Logsでロギングしてみた

こんにちは、佐伯です。

S3の署名バージョン2(SigV2)の廃止は延期されましたが、2019年6月24日以降SigV2のサポートがS3バケットに依存することになります。また、既存バケットにおいてもいつまでSigV2がサポートされるのかについてはアナウンスされておらず、延期されたことで対応優先度が下がりましたが、SigV2の廃止が中止されたわけではありませんので対応しなくて良いといったものではないと考えています。

S3の署名バージョン2(SigV2)の廃止スケジュールが延期、2020年6月以降も既存のS3バケットはSigV2が継続サポートとなりました!!

今回は、先日CloudWatch Eventsのアップデートがあり、CloudWatch EventsからCloudWatch Logsへの直接出力できるようになったのでS3へのSigV2を使用したアクセスをロギングする仕組みをCloudFormationテンプレートで作成しました。

[アップデート] CloudWatch EventsのターゲットとしてCloudWatch Logsがサポートされました!

ざっくり言うと以下エントリのAmazon SNS、AWS Lambdaがないバージョンです。

署名バージョン2のS3 APIログをCloudWatch Logs Insights で集計してみた

やってみた

概要図

CloudFormationテンプレート

注意点

このCloudFormationテンプレートはひとつのS3バケットのデータイベントを記録するCloudTrailを作成します。複数のS3バケットを対象としたい場合は手動でCloudTrailを編集してください。また、CloudTrailのデータイベントは100,000件のイベントあたり0.10USDが課金されます。S3へのリクエストが極端に多いことが予想できる場合はCloudTrailの利用料にご注意ください。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: Logging of S3 access using SigV2 (CloudTrail,CloudWatchEvent,CloudWatchLogs)

Parameters:
  LogTargetS3Name:
    Description: S3 bucket name that enabled logging of S3 object API
    Type: String
    Default: s3-bucket-name

Resources:
  S3Bucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: 7

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref 'S3Bucket'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:GetBucketAcl
            Resource: !Sub 'arn:aws:s3:::${S3Bucket}'
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:PutObject
            Resource: !Sub 'arn:aws:s3:::${S3Bucket}/AWSLogs/${AWS::AccountId}/*'
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control

  TrailS3Event:
    DependsOn:
      - BucketPolicy
    Type: AWS::CloudTrail::Trail
    Properties:
      S3BucketName: !Ref 'S3Bucket'
      IsLogging: true
      IsMultiRegionTrail: false
      IncludeGlobalServiceEvents: false
      EventSelectors:
        - IncludeManagementEvents: false
          DataResources:
            - Type: AWS::S3::Object
              Values:
                - !Sub 'arn:aws:s3:::${LogTargetS3Name}/'
          ReadWriteType: All

  CloudWatchLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/events/s3-sigv2-access-log

  CloudwatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: EventRule
      EventPattern:
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventSource:
            - s3.amazonaws.com
          additionalEventData:
            SignatureVersion:
              - SigV2
      State: ENABLED
      Targets:
        - Arn: !GetAtt CloudWatchLogGroup.Arn
          Id: EventRule

CloudWatch Logsを確認

AmazonLinux(2016.09)(amzn-ami-hvm-2016.09.0.20160923-x86_64-gp2)からEC2インスタンスを作成してS3へのファイルコピーを実施し、CloudWatch Logs: /aws/events/s3-sigv2-access-log へログが出力されることを確認します。

各種IDなどはマスキングしていますが、EC2インスタンスにIAMロールをアタッチしてアクセスしている場合は、以下のようなJSONが出力されます。アクセスキーを使用したリクエストがある場合は、detail.userIdentityの出力フォーマットが若干違うのでご注意ください。

detail.userIdentity.arnから対象のインスタンスを、detail.userAgentからリクエスト元のエージェントをある程度絞り込むことができます。今回の場合はAWS CLIからのアクセスです。

{
    "version": "0",
    "id": "5b7b3c0c-8cf7-XXXX-1125-XXXXXXXXXXXX",
    "detail-type": "AWS API Call via CloudTrail",
    "source": "aws.s3",
    "account": "XXXXXXXXXXXX",
    "time": "2019-06-30T10:20:03Z",
    "region": "ap-northeast-1",
    "resources": [],
    "detail": {
        "eventVersion": "1.05",
        "userIdentity": {
            "type": "AssumedRole",
            "principalId": "AROAJHX4LXXXXXXXXXXXX:i-00136XXXXXXXXXXXX",
            "arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/AmazonEC2RoleForSSM/i-00136XXXXXXXXXXXX",
            "accountId": "XXXXXXXXXXXX",
            "accessKeyId": "ASIA27MRXXXXXXXXXXXX",
            "sessionContext": {
                "sessionIssuer": {
                    "type": "Role",
                    "principalId": "AROAJHX4LXXXXXXXXXXXX",
                    "arn": "arn:aws:iam::XXXXXXXXXXXX:role/AmazonEC2RoleForSSM",
                    "accountId": "XXXXXXXXXXXX",
                    "userName": "AmazonEC2RoleForSSM"
                },
                "attributes": {
                    "creationDate": "2019-06-30T10:04:16Z",
                    "mfaAuthenticated": "false"
                }
            }
        },
        "eventTime": "2019-06-30T10:20:03Z",
        "eventSource": "s3.amazonaws.com",
        "eventName": "PutObject",
        "awsRegion": "ap-northeast-1",
        "sourceIPAddress": "54.249.95.0",
        "userAgent": "[aws-cli/1.10.56 Python/2.7.12 Linux/4.4.19-29.55.amzn1.x86_64 botocore/1.4.46]",
        "requestParameters": {
            "bucketName": "example-bucket-XXXXXXXXXXXX",
            "Host": "example-bucket-XXXXXXXXXXXX.s3.amazonaws.com",
            "key": "test"
        },
        "responseElements": null,
        "additionalEventData": {
            "SignatureVersion": "SigV2",
            "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
            "bytesTransferredIn": 0,
            "AuthenticationMethod": "AuthHeader",
            "x-amz-id-2": "LXLyJXqrMg9wWTXsiyQNMTJEDZM3jqGFBcHzCfGzzFnvOmfkAtvohkXCbKXXXXXXXXXXXXXXXX=",
            "bytesTransferredOut": 0
        },
        "requestID": "D2A9XXXXXXXXXXXX",
        "eventID": "f54a8cfd-c2f0-4a20-9d76-XXXXXXXXXXXX",
        "readOnly": false,
        "resources": [
            {
                "type": "AWS::S3::Object",
                "ARN": "arn:aws:s3:::example-bucket-XXXXXXXXXXXX/test"
            },
            {
                "accountId": "XXXXXXXXXXXX",
                "type": "AWS::S3::Bucket",
                "ARN": "arn:aws:s3:::example-bucket-XXXXXXXXXXXX"
            }
        ],
        "eventType": "AwsApiCall",
        "recipientAccountId": "XXXXXXXXXXXX"
    }
}

CloudWatch Logs Insightsで検索

CloudWatch Logsに出力したログはCloudWatch Logs Insightsで検索することができます。CloudWatch Logs Insightsはスキャンされたデータ1GBあたり0.0076USDの課金が発生する点にご注意ください。

IAMロールを使用したEC2からのアクセスであれば、以下のクエリを実行することでリクエスト元のEC2インスタンスやエージェント(AWS SDK, AWS CLI)などを特定することができます。アクセスキーを使用したアクセスの場合はキーが変わると思われますので、ご注意ください。

stats count(*) by detail.userIdentity.arn, detail.userAgent, detail.requestParameters.bucketName

調査後のリソース削除

CloudFormationスタックを削除します。CloudTrailのログデータを保存しているS3バケットは手動で削除が必要です。S3バケット内のデータを削除し、S3バケットを削除してください。

最後に

CloudWatch EventsからCloudWatch Logsへ直接出力がサポートされ、AWS Lambdaなどを作成せずともロギングの仕組みを作ることができるようになりました。繰り返しとなりますが、S3署名バージョン2(SigV2)の廃止は延期となりましたが、対応が必要であることは変わらないと考えています。