HOWTO: Build CloudTrail Trail with CloudFormation Template for easy builds and tear down

This post is on how to create a CloudTrail Trail with S3 and CloudWatch in CloudFormation.

In my previous article, I had provided 8 tips on how to configure CloudTrail for secure logging and auditing via the AWS management console. In this post, I have covered the same secure options using the much preferred and popular Infrastructure as Code using Cloud Formation.

This template covers the following:

  • Create a CloudTrail Trail to log management events for all accounts under AWS Organization
  • Create an encrypted S3 bucket associated with the trail
  • Create CloudWatch logs and role associated with the trail
  • Create CMK keys (SSE-KMS) for S3 and CloudTrail
AWSTemplateFormatVersion: 2010-09-09
Description: test createtrail function 

Parameters:
  CloudTrailName:
    Type: String
    Default: 'management-events-maghilda'
  CloudTrailBucketName:
    Type: String
    Default: 'cloudtrail-maghilda'
  CloudTrailBucketPrefix:
    Type: String
    Default: 'maghildaevents'
  OrganizationRoot:
    Type: String 
    Default: 'o-2rty7as5fl'

Resources:
  # Create a new log group
  LogGroup: 
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 365 # optional
  #Create a new bucket with all the secure options and encryption
  CloudTrailBucket: 
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain  #delete manually after the delete stack command  
    Properties:
      BucketName: !Ref CloudTrailBucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        RestrictPublicBuckets: true
      ObjectLockEnabled: true
      ObjectLockConfiguration:
        ObjectLockEnabled: 'Enabled'
        Rule:
          DefaultRetention:
            Mode: GOVERNANCE
            Days: 90
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            KMSMasterKeyID: !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:${CloudTrailKeyS3Alias}'
            SSEAlgorithm: 'aws:kms'
    DependsOn:
      - CloudTrailKeyS3
      - CloudTrailKeyS3Alias
  #Create a bucket policy
  CloudTrailBucketPolicy:
    DependsOn:
        - CloudTrailBucket
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref CloudTrailBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: 'cloudtrail.amazonaws.com'
            Action: 's3:GetBucketAcl'
            Resource: !Sub 'arn:aws:s3:::${CloudTrailBucket}'
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: 'cloudtrail.amazonaws.com'
            Action: 's3:PutObject'
            Resource: !Sub 'arn:aws:s3:::${CloudTrailBucket}/${CloudTrailBucketPrefix}/AWSLogs/${AWS::AccountId}/*'
            Condition:
              StringEquals:
                's3:x-amz-acl': 'bucket-owner-full-control'        
                'aws:SourceArn': !Sub 'arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/${CloudTrailName}'
          - Sid: AWSCloudTrailWriteOrg
            Effect: Allow
            Principal:
              Service: 'cloudtrail.amazonaws.com'
            Action: 's3:PutObject'
            Resource: !Sub 'arn:aws:s3:::${CloudTrailBucket}/${CloudTrailBucketPrefix}/AWSLogs/${OrganizationRoot}/*' #log for all accounts in the org
            Condition:
              StringEquals:
                's3:x-amz-acl': 'bucket-owner-full-control'        
                'aws:SourceArn': !Sub 'arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/${CloudTrailName}'
  # Create a role for CloudWatch logs
  CloudTrailLogsRole: 
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
  # Define a policy for the role      
  CloudTrailLogsPolicy: 
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
        - Action:
          - logs:PutLogEvents
          - logs:CreateLogStream
          Effect: Allow
          Resource: !GetAtt LogGroup.Arn
        Version: '2012-10-17'
      PolicyName: DefaultPolicy
      Roles:
      - Ref: CloudTrailLogsRole
  #Create a new trail with secure options and KMS encryption
  CloudTrail: 
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: !Ref CloudTrailName
      CloudWatchLogsLogGroupArn: !GetAtt LogGroup.Arn
      CloudWatchLogsRoleArn: !GetAtt CloudTrailLogsRole.Arn
      EnableLogFileValidation: true
      IncludeGlobalServiceEvents: true
      S3BucketName: !Ref CloudTrailBucket
      S3KeyPrefix: !Ref CloudTrailBucketPrefix
      IsLogging: true
      IsMultiRegionTrail: true
      IsOrganizationTrail: true
      KMSKeyId: !GetAtt CloudTrailKey.Arn
    DependsOn:
    - CloudTrailLogsPolicy
    - CloudTrailLogsRole
    - CloudTrailBucket
    - CloudTrailBucketPolicy
    - CloudTrailKey
  #Create a new CMK for the trail
  CloudTrailKey:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Version: 2012-10-17
        Id: key-cloudtrail
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: 
                - !Sub 'arn:aws:iam::${AWS::AccountId}:root'
                - !Sub 'arn:aws:sts::${AWS::AccountId}:assumed-role/AWSReservedSSO_AdministratorAccess_234hdfj4857555/maghilda_user'
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow CloudTrail to encrypt logs
            Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
            Action: 'kms:GenerateDataKey*'
            Resource: '*'
            Condition:
              StringLike:
                'kms:EncryptionContext:aws:cloudtrail:arn': !Sub 'arn:aws:cloudtrail:*:${AWS::AccountId}:trail/*'
                'aws:SourceArn': !Sub 'arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/${CloudTrailName}'
          - Sid: Allow CloudTrail to describe key
            Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
            Action: 'kms:DescribeKey'
            Resource: '*'
          - Sid: Allow principals in the account to decrypt log files
            Effect: Allow
            Principal:
              AWS: '*'
            Action:
              - 'kms:Decrypt'
              - 'kms:ReEncryptFrom'
            Resource: '*'
            Condition:
              StringEquals:
                'kms:CallerAccount': !Sub '${AWS::AccountId}'
              StringLike:
                'kms:EncryptionContext:aws:cloudtrail:arn': !Sub 'arn:aws:cloudtrail:*:${AWS::AccountId}:trail/*'
          - Sid: Allow alias creation during setup
            Effect: Allow
            Principal:
              AWS: '*'
            Action: 'kms:CreateAlias'
            Resource: '*'
            Condition:
              StringEquals:
                'kms:ViaService': ec2.us-east-1.amazonaws.com
                'kms:CallerAccount': !Sub '${AWS::AccountId}'
          - Sid: Enable cross account log decryption
            Effect: Allow
            Principal:
              AWS: '*'
            Action:
              - 'kms:Decrypt'
              - 'kms:ReEncryptFrom'
            Resource: '*'
            Condition:
              StringEquals:
                'kms:CallerAccount': !Sub '${AWS::AccountId}'
              StringLike:
                'kms:EncryptionContext:aws:cloudtrail:arn': !Sub 'arn:aws:cloudtrail:*:${AWS::AccountId}:trail/*'
  #Create an alias for the CloudTrail key
  CloudTrailKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/cloudtrail
      TargetKeyId:
        Ref: CloudTrailKey
  #Create a new CMK for S3 bucket 
  CloudTrailKeyS3:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Version: 2012-10-17
        Id: key-cloudtrails3
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: 
                - !Sub 'arn:aws:iam::${AWS::AccountId}:root'
                - !Sub 'arn:aws:sts::${AWS::AccountId}:assumed-role/AWSReservedSSO_AdministratorAccess_234hdfj4857555/maghilda_user'
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow VPC Flow Logs to use the key
            Effect: Allow
            Principal:
              Service:
                - delivery.logs.amazonaws.com
            Action: 'kms:GenerateDataKey*'
            Resource: '*'
  #Create an alias for the s3 bucket key
  CloudTrailKeyS3Alias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/cloudtrails3
      TargetKeyId:
        Ref: CloudTrailKeyS3

Outputs:
  CloudTrailLogsRoleArn:
    Value: !GetAtt CloudTrailLogsRole.Arn

  LogGroupArn:
    Value: !GetAtt LogGroup.Arn

Save the file as createtrail-template.yml. Please note that I am using service linked role for IAM Identity Center with the name prefix AWSReservedSSO_

Create the above stack via the CLI

aws cloudformation create-stack --stack-name testcreatetrail --template-body file://createtrail-template.yml --capabilities CAPABILITY_IAM

This stack will take a few minutes to create. Run the following command to check the status

aws cloudformation describe-stacks --stack-name testcreatetrail
Clean up resources
#delete stack
aws cloudformation delete-stack --stack-name testcreatetrail

Since CloudFormation cannot delete a non-empty S3 bucket, you have to manually empty and delete the bucket

#empty
aws s3 rm  s3://cloudtrail-maghilda —recursive

#delete
aws s3api delete-bucket --bucket cloudtrail-maghilda
References

Hope you find this useful.