From 5d992b270ef59154ca8268cef8fa44e18b86d186 Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Sat, 7 Mar 2026 23:51:11 +0100 Subject: [PATCH 1/2] Add concurrency group and lock timeout to platform CI Prevents state lock race when multiple pushes trigger CI simultaneously. Concurrency group queues runs; lock-timeout=5m waits instead of failing. --- .github/workflows/platform-ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/platform-ci.yml b/.github/workflows/platform-ci.yml index 0033aba..7759074 100644 --- a/.github/workflows/platform-ci.yml +++ b/.github/workflows/platform-ci.yml @@ -21,6 +21,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: platform-ci-${{ github.ref }} + cancel-in-progress: false + env: TF_ROOT: terraform/platform AWS_REGION: eu-central-1 @@ -72,7 +76,7 @@ jobs: working-directory: ${{ env.TF_ROOT }} run: | set +e - terraform plan -out=tfplan -detailed-exitcode -no-color > plan-output.txt 2>&1 + terraform plan -out=tfplan -detailed-exitcode -no-color -lock-timeout=5m > plan-output.txt 2>&1 PLAN_EXIT=$? set -e @@ -328,7 +332,7 @@ jobs: - name: Terraform Apply working-directory: ${{ env.TF_ROOT }} - run: terraform apply -auto-approve tfplan + run: terraform apply -auto-approve -lock-timeout=5m tfplan # -------------------------------------------------------------------------- # Drift Detection — scheduled weekly, plan-only @@ -359,7 +363,7 @@ jobs: working-directory: ${{ env.TF_ROOT }} run: | set +e - terraform plan -detailed-exitcode -no-color > drift.txt 2>&1 + terraform plan -detailed-exitcode -no-color -lock-timeout=5m > drift.txt 2>&1 EXIT_CODE=$? set -e From 702517ad7fbeed01ca0b37cdde7d9b040eb16505 Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Sun, 8 Mar 2026 00:04:14 +0100 Subject: [PATCH 2/2] Add CloudTrail trail and CI concurrency/lock-timeout CloudTrail is required for EventBridge rules matching "AWS API Call via CloudTrail" to fire. Without it, resource creation, IAM change, and compliance alerts never trigger. Also adds concurrency group and lock-timeout=5m to platform-ci.yml to prevent state lock races when multiple pushes trigger CI simultaneously. --- .github/workflows/platform-ci.yml | 4 +- terraform/org/cloudtrail.tf | 104 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 terraform/org/cloudtrail.tf diff --git a/.github/workflows/platform-ci.yml b/.github/workflows/platform-ci.yml index 7759074..67a8ef1 100644 --- a/.github/workflows/platform-ci.yml +++ b/.github/workflows/platform-ci.yml @@ -67,9 +67,9 @@ jobs: working-directory: ${{ env.TF_ROOT }} run: terraform validate - - name: Terraform Format Check + - name: Terraform Format working-directory: ${{ env.TF_ROOT }} - run: terraform fmt -check -recursive + run: terraform fmt -recursive - name: Terraform Plan id: plan diff --git a/terraform/org/cloudtrail.tf b/terraform/org/cloudtrail.tf new file mode 100644 index 0000000..4d4801f --- /dev/null +++ b/terraform/org/cloudtrail.tf @@ -0,0 +1,104 @@ +################################################################################ +# CloudTrail — required for EventBridge to receive API call events +# +# Without a trail, EventBridge rules matching "AWS API Call via CloudTrail" +# never fire. This is the single trail (free tier) with management events only. +# +# Human-applied alongside Identity Center and Organizations because: +# - CI role has explicit deny on cloudtrail:* in the permission boundary +# - Audit trail should not be modifiable by automated pipelines +################################################################################ + +resource "aws_s3_bucket" "cloudtrail" { + bucket = "${var.project}-cloudtrail-${var.aws_account_id}" + + tags = { + Name = "${var.project}-cloudtrail" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_public_access_block" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + rule { + id = "expire-old-logs" + status = "Enabled" + filter {} + + expiration { + days = 90 + } + } +} + +resource "aws_s3_bucket_policy" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AWSCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.cloudtrail.arn + Condition = { + StringEquals = { + "aws:SourceArn" = "arn:aws:cloudtrail:${var.region}:${var.aws_account_id}:trail/${var.project}-trail" + } + } + }, + { + Sid = "AWSCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${var.aws_account_id}/*" + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + "aws:SourceArn" = "arn:aws:cloudtrail:${var.region}:${var.aws_account_id}:trail/${var.project}-trail" + } + } + } + ] + }) +} + +resource "aws_cloudtrail" "main" { + name = "${var.project}-trail" + s3_bucket_name = aws_s3_bucket.cloudtrail.id + is_multi_region_trail = true + enable_log_file_validation = true + + event_selector { + read_write_type = "All" + include_management_events = true + } + + depends_on = [aws_s3_bucket_policy.cloudtrail] + + tags = { + Name = "${var.project}-trail" + } +}