How to use CloudTrail to alert changes in your infrastructure using Terraform and Lambda


This guide is the first part about how to get useful information of CloudTrail to alert your system when some configuration has changed. All configurations will be done using Terraform and Go and following the PCI DSS of AWS.

Objective

We are going to use an AWS service called CloudTrail to alert our team each time some high severity event happen in our infrastructure. This will be done using CloudTrail, CloudWatch, S3, KMS, Lambda and your metrics or logs system. In this first part we are going to focus on whats in the red box. Here’s a picture of the complete architecture: architecture Now, let’s begin.

What is CloudTrail?

From AWS, CloudTrail is a service that enables governance, compliance, operational auditing, and risk auditing of your AWS account. With CloudTrail, you can log, continuously monitor, and retain account activity related to actions across your AWS infrastructure. Basically, it monitors almost everything that happen in your AWS account and crete a record about it.

How to enable it?

Cloudtrail is enabled by default in your AWS account. If you use multiple regions for your infrastructure, you could create one per region or one for all, its up to you. But what’s enabled is called Event History, which records recent activity and only to be searched via web console. What we want to enable is called a Trail, it records all your activity that persist over time.

Create a Trail with Terraform

I assume that you have worked with Terraform before, but in case you don’t, here’s a guide of the very first steps.

Now, we will create a project that has everything required to use CloudTrail. The project will be created in the right order to give you an idea of the components but you could also use Terraform dependencies and create everything at once. The project has the next structure (I’m going to describe each folder as it appears, don’t worry):

cloudtrail/
    cloudwatch/
        cloudtrail_log_group.tf
        vars.tf
    iam/
        cloudtrail_cloudwatch_role.tf
        vars.tf
    kms/
        cloudtrail_logs_key.tf
        vars.tf
    s3/
        cloudtrail_logs_bucket.tf
        vars.tf
    trails/
        default_trail.tf
        vars.tf
    backend.tf
    main.tf
    project.auto.tfvars
    vars.tf

The first component of our project will be the storage of our logs, we will use CloudWatch and S3.

In the cloudwatch folder create a file called cloudtrail_log_group.tf. There, put this to create a CloudWatch Log Group:

resource "aws_cloudwatch_log_group" "cloudtrail_log_group" {
  name              = "CloudTrailLogs"
  retention_in_days = 30 #Or anything you want
  tags              = var.tags
}

output "cloudtrail_log_group" {
  value = aws_cloudwatch_log_group.cloudtrail_log_group
}

In the s3 folder create a file called cloudtrail_logs_bucket.tf. There, put this to create a S3 bucket with its bucket policy:


resource "aws_s3_bucket" "bucket" {
  bucket              = "cloudtrail-logs-bucket" #CHANGE_ME
  acl                 = "private"
  tags                = var.tags

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }
}

resource "aws_s3_bucket_policy" "cloudtrail_logs" {
  bucket = module.cloudtrail_logs.bucket.bucket
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
          "Sid": "Get bucket policy needed for trails",
          "Effect": "Allow",
          "Principal": {
              "Service": "cloudtrail.amazonaws.com"
          },
          "Action": "s3:GetBucketAcl",
          "Resource": "arn:aws:s3:::${var.buckets.aws_cloudtrail.name}"
        },
        {
          "Sid": "Put bucket policy needed for trails",
          "Effect": "Allow",
          "Principal": {
              "Service": "cloudtrail.amazonaws.com"
          },
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::${var.buckets.aws_cloudtrail.name}/*",
          "Condition": {
              "StringEquals": {
                "s3:x-amz-acl": "bucket-owner-full-control"
              }
            }
        }
    ]
}
EOF
}

output "cloudtrail_logs" {
  value = module.cloudtrail_logs.bucket
}

A security best practice with CloudTrail is to use is configured to use the server-side encryption (SSE) AWS KMS customer master key (CMK) encryption. To do this, we have to create a custom KMS and set an special policy. So, in the kms folder create a file called cloudtrail_logs_key.tf put this:

resource "aws_kms_key" "cloudtrail_logs_key" {
  description         = "Custom key to encrypt Cloudtrail logs"
  enable_key_rotation = true
  policy = jsonencode(
    {
      "Version" : "2012-10-17",
      "Id" : "Key policy created by CloudTrail",
      "Statement" : [
        {
          "Sid" : "Enable IAM User Permissions",
          "Effect" : "Allow",
          "Principal" : {
            "AWS" : [
              "arn:aws:iam::${var.account_id}:root"
            ]
          },
          "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" : "arn:aws:cloudtrail:*:${var.account_id}:trail/*"
            }
          }
        },
        {
          "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" : "${var.account_id}"
            },
            "StringLike" : {
              "kms:EncryptionContext:aws:cloudtrail:arn" : "arn:aws:cloudtrail:*:${var.account_id}:trail/*"
            }
          }
        },
        {
          "Sid" : "Allow alias creation during setup",
          "Effect" : "Allow",
          "Principal" : {
            "AWS" : "*"
          },
          "Action" : "kms:CreateAlias",
          "Resource" : "*",
          "Condition" : {
            "StringEquals" : {
              "kms:CallerAccount" : "${var.account_id}",
              "kms:ViaService" : "ec2.us-east-1.amazonaws.com"
            }
          }
        }
      ]
    }
  )
  tags = var.tags
}

resource "aws_kms_alias" "cloudtrail_logs_key" {
  name          = "alias/cloudtrail/logs-key"
  target_key_id = aws_kms_key.cloudtrail_logs_key.key_id
}

output "cloudtrail_logs_key" {
  value = aws_kms_key.cloudtrail_logs_key
}

output "cloudtrail_logs_key_alias" {
  value = aws_kms_alias.cloudtrail_logs_key
}

The last thing to do before creating the Trail, is to create a role to grant permissions for put logs in CloudWatch.

In the iam folder create a file called cloudtrail_cloudwatch_role.tf. There, put this to create an IAM role and policy:

resource "aws_iam_role_policy" "cloudwatch_logs_policy" {
  name = "cloudwatch-logs"
  role = aws_iam_role.test_role.id

  policy = <<-EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "${var.cloudwatch_module.cloudtrail_log_group.arn}"
        }
    ]
}
EOF
}

resource "aws_iam_role" "cloudwatch_logs" {
  name = "cloudwatch-logs"

  assume_role_policy = <<-EOF
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "sts:AssumeRole",
        "Effect": "Allow",
        "Principal": {
          "Service": "cloudtrail.amazonaws.com"
        }
      }
    ]
  }
  EOF
}

At this point we’re ready to create our Trail. In the trails folder create a file called default_trail.tf. There, put this to create multi region Trail:

resource "aws_cloudtrail" "default_trail" {
  name                          = "default-trail"
  enable_logging                = true
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_log_file_validation    = true

  s3_bucket_name             = var.s3_module.cloudtrail_logs.id
  kms_key_id                 = var.kms_module.cloudtrail_logs_key.arn
  cloud_watch_logs_group_arn = var.cloudwatch_module.cloudtrail_log_group.arn
  cloud_watch_logs_role_arn  = var.iam_module.cloudtrail_cloudwatch_role.arn
  tags                       = var.tags
}

To deploy our infrastructure we have to use the main file of our project, where we instance the different modules the architecture. So, in main.tf:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
  required_version = ">= 0.13"
}
provider "aws" {
  version = "~> 3.21"
  region  = var.region
  profile = var.profile
  assume_role {
    role_arn = "arn:aws:iam::${var.account_id}:role/${var.project_role}"
  }
}

locals {
  tags = {
    Service     = var.service
    Environment = var.profile
  }
}

module "cloudwatch" {
  source = "./cloudwatch"

  tags = local.tags
}

module "s3" {
  source = "./s3"

  tags        = local.tags
}

module "kms" {
  source     = "./kms"
  account_id = var.account_id
  tags       = local.tags
}

module "iam" {
  source = "./iam"

  depends_on = [module.cloudwatch]

  region            = var.region
  account_id        = var.account_id
  cloudwatch_module = module.cloudwatch
  tags              = local.tags
}

module "trails" {
  source     = "./trails"
  depends_on = [module.cloudwatch, module.s3, module.kms ,module.iam]

  s3_module         = module.s3.data
  kms_module        = module.kms
  cloudwatch_module = module.cloudwatch
  iam_module        = module.iam.data
  tags              = local.tags
}

And last but not least, don’t forget to set up your variable in the project.auto.tfvars file. There you could define things like account_id, project_role, region and profile.


See also