Skip to content

Commit b51cfe9

Browse files
authored
feat: add CloudTrail module with S3, CloudWatch Logs, and KMS encryption (#196)
1 parent f1698a2 commit b51cfe9

File tree

5 files changed

+215
-1
lines changed

5 files changed

+215
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,10 @@ Lambda functions live in `src/<lambda-name>/` directories with an `index.mjs` (o
172172
- **`lambda-deploy.yaml`** is a manual `workflow_dispatch` workflow for deploying a Lambda to a target environment. It packages the function, uploads the zip to an S3 artifacts bucket, and optionally updates the Lambda function code via the AWS CLI. The S3 bucket and region are configurable per invocation.
173173

174174
## tflint
175+
[tflint](https://github.com/terraform-linters/tflint) is a pluggable linter for Terraform. We use the `tflint-ruleset-aws` plugin to catch AWS-specific issues (invalid instance types, missing tags, deprecated resources) before they reach `terraform plan`. Configuration is in `.tflint.hcl` files per environment.
175176

176177
## checkov
177-
[Checkov]() is an amazing tool to lint terraform (and other) resources, we use the non-official pre-commit hook by antonbabenko
178+
[Checkov](https://www.checkov.io) is an amazing tool to lint terraform (and other) resources, we use the non-official pre-commit hook by antonbabenko
178179

179180
## VPC Flow Logs
180181

@@ -242,6 +243,7 @@ Reusable Terraform modules in `modules/`:
242243
|--------|-------------|
243244
| `aws-bootstrap` | S3 backend + optional DynamoDB table for state management |
244245
| `certificate` | ACM certificate with DNS validation |
246+
| `cloudtrail` | Multi-region CloudTrail with S3 storage, CloudWatch Logs, KMS encryption, and lifecycle rules |
245247
| `cluster-autoscaler` | Kubernetes Cluster Autoscaler with IRSA |
246248
| `eks` | EKS cluster with managed/self-managed node groups |
247249
| `github-oidc` | GitHub Actions OIDC provider + IAM role |

modules/cloudtrail/main.tf

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
data "aws_caller_identity" "current" {}
2+
3+
resource "aws_cloudwatch_log_group" "cloudtrail" {
4+
name = "${var.name_prefix}-cloudtrail-logs"
5+
6+
kms_key_id = var.kms_key_arn
7+
retention_in_days = var.retention_in_days
8+
}
9+
10+
resource "aws_cloudtrail" "main" {
11+
# checkov:skip=CKV_AWS_252: Ensure CloudTrail defines an SNS Topic: We don't need SNS notifications here
12+
name = "${var.name_prefix}-global-events"
13+
s3_bucket_name = aws_s3_bucket.cloudtrail.id
14+
15+
enable_log_file_validation = true
16+
is_multi_region_trail = true
17+
is_organization_trail = false // cannot be true since this is not a management account
18+
19+
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail.arn
20+
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*" # CloudTrail requires the Log Stream wildcard
21+
22+
kms_key_id = var.kms_key_arn
23+
24+
event_selector {
25+
exclude_management_event_sources = [
26+
"kms.amazonaws.com",
27+
"rdsdata.amazonaws.com"
28+
]
29+
}
30+
31+
depends_on = [
32+
aws_s3_bucket_policy.cloudtrail,
33+
]
34+
}
35+
36+
resource "aws_s3_bucket" "cloudtrail" {
37+
# checkov:skip=CKV_AWS_18: Access logging not needed for CloudTrail bucket
38+
# checkov:skip=CKV_AWS_21: Object versioning not needed here
39+
# checkov:skip=CKV_AWS_144: Cross-region replication not needed here
40+
# checkov:skip=CKV2_AWS_62: Event notifications not needed here
41+
bucket = "${var.name_prefix}-cloudtrail-global-events"
42+
}
43+
44+
resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" {
45+
bucket = aws_s3_bucket.cloudtrail.bucket
46+
47+
rule {
48+
apply_server_side_encryption_by_default {
49+
kms_master_key_id = var.kms_key_arn
50+
sse_algorithm = "aws:kms"
51+
}
52+
}
53+
}
54+
55+
resource "aws_s3_bucket_policy" "cloudtrail" {
56+
bucket = aws_s3_bucket.cloudtrail.id
57+
policy = jsonencode({
58+
Version = "2012-10-17"
59+
Statement = [
60+
{
61+
Sid = "AWSCloudTrailAclCheck"
62+
Effect = "Allow"
63+
Principal = {
64+
Service = "cloudtrail.amazonaws.com"
65+
}
66+
Action = "s3:GetBucketAcl"
67+
Resource = aws_s3_bucket.cloudtrail.arn
68+
},
69+
{
70+
Sid = "AWSCloudTrailWrite"
71+
Effect = "Allow"
72+
Principal = {
73+
Service = "cloudtrail.amazonaws.com"
74+
}
75+
Action = "s3:PutObject"
76+
Resource = "${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
77+
Condition = {
78+
StringEquals = {
79+
"s3:x-amz-acl" = "bucket-owner-full-control"
80+
}
81+
}
82+
}
83+
]
84+
})
85+
}
86+
87+
resource "aws_s3_bucket_public_access_block" "cloudtrail" {
88+
bucket = aws_s3_bucket.cloudtrail.id
89+
90+
restrict_public_buckets = true
91+
block_public_policy = true
92+
93+
ignore_public_acls = true
94+
block_public_acls = true
95+
}
96+
97+
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" {
98+
#checkov:skip=CKV_AWS_300:Abort incomplete multipart upload is configured in the 'all' rule
99+
bucket = aws_s3_bucket.cloudtrail.id
100+
101+
rule {
102+
id = "all"
103+
status = "Enabled"
104+
105+
filter {
106+
prefix = ""
107+
}
108+
109+
abort_incomplete_multipart_upload {
110+
days_after_initiation = 3
111+
}
112+
}
113+
114+
rule {
115+
id = "log"
116+
status = "Enabled"
117+
118+
expiration {
119+
days = 90
120+
}
121+
122+
filter {
123+
and {
124+
prefix = "AWSLogs/"
125+
126+
tags = {
127+
rule = "log"
128+
autoclean = "true"
129+
}
130+
}
131+
}
132+
133+
transition {
134+
days = 30
135+
storage_class = "STANDARD_IA"
136+
}
137+
138+
transition {
139+
days = 60
140+
storage_class = "GLACIER"
141+
}
142+
}
143+
}
144+
145+
resource "aws_iam_role" "cloudtrail" {
146+
name = "cloudtrail-cloudwatch"
147+
assume_role_policy = jsonencode({
148+
Version = "2012-10-17"
149+
Statement = [{
150+
Effect = "Allow"
151+
Principal = {
152+
Service = "cloudtrail.amazonaws.com"
153+
}
154+
Action = "sts:AssumeRole"
155+
}]
156+
})
157+
}
158+
159+
resource "aws_iam_role_policy" "cloudtrail_cloudwatch" {
160+
name = "cloudtrail"
161+
role = aws_iam_role.cloudtrail.id
162+
policy = jsonencode({
163+
Version = "2012-10-17"
164+
Statement = [
165+
{
166+
Sid = "AWSCloudTrailCreateLogStream2014110"
167+
Effect = "Allow"
168+
Action = "logs:CreateLogStream"
169+
Resource = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
170+
},
171+
{
172+
Sid = "AWSCloudTrailPutLogEvents20141101"
173+
Effect = "Allow"
174+
Action = "logs:PutLogEvents"
175+
Resource = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
176+
}
177+
]
178+
})
179+
}

modules/cloudtrail/outputs.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "cloudtrail_arn" {
2+
description = "CloudTrail ARN"
3+
value = aws_cloudtrail.main.arn
4+
}
5+
6+
output "s3_bucket_arn" {
7+
description = "CloudTrail S3 bucket ARN"
8+
value = aws_s3_bucket.cloudtrail.arn
9+
}

modules/cloudtrail/variables.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
variable "name_prefix" {
2+
description = "Prefix for resource names"
3+
type = string
4+
}
5+
6+
variable "kms_key_arn" {
7+
description = "KMS key ARN for encryption"
8+
type = string
9+
}
10+
11+
variable "retention_in_days" {
12+
description = "CloudWatch log retention in days"
13+
type = number
14+
default = 365
15+
}

modules/cloudtrail/versions.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
terraform {
2+
required_version = ">= 1.0.0"
3+
required_providers {
4+
aws = {
5+
source = "hashicorp/aws"
6+
version = ">= 4.57.0, < 6.0.0"
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)