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:
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.