diff --git a/infrastructure/stacks/api-layer/cloudtrail.tf b/infrastructure/stacks/api-layer/cloudtrail.tf new file mode 100644 index 000000000..6d71549be --- /dev/null +++ b/infrastructure/stacks/api-layer/cloudtrail.tf @@ -0,0 +1,101 @@ +resource "aws_cloudtrail" "data_events_trail" { + name = "${var.project_name}-${var.environment}-data-events-trail" + s3_bucket_name = module.s3_cloudtrail_bucket.storage_bucket_name + kms_key_id = aws_kms_key.cloudtrail_kms_key.arn + include_global_service_events = true + is_multi_region_trail = false + enable_log_file_validation = true + + cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch_role.arn + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail_log_group.arn}:*" + + event_selector { + read_write_type = "ReadOnly" + include_management_events = false + + data_resource { + type = "AWS::DynamoDB::Table" + values = [module.eligibility_status_table.arn] + } + } +} + +resource "aws_kms_key" "cloudtrail_kms_key" { + description = "KMS key for CloudTrail log file encryption" + deletion_window_in_days = 14 + enable_key_rotation = true + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EnableRootPermissions" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "AllowCloudTrailEncryptLogs" + Effect = "Allow" + Principal = { + Service = "cloudtrail.amazonaws.com" + } + Action = [ + "kms:GenerateDataKey*", + "kms:DescribeKey", + "kms:Encrypt" + ] + Resource = "*" + } + ] + }) + + tags = { + environment = var.environment + project_name = var.project_name + stack_name = local.stack_name + workspace = terraform.workspace + } + +} + +# KMS key alias +resource "aws_kms_alias" "cloudtrail_kms_alias" { + name = "alias/${var.project_name}-${var.environment}-cloudtrail-cmk" + target_key_id = aws_kms_key.cloudtrail_kms_key.key_id +} + +# KMS key policy to allow CloudTrail and CloudWatch Logs to use the key for encryption and decryption +resource "aws_kms_key_policy" "cloudtrail_kms_key_policy" { + key_id = aws_kms_key.cloudtrail_kms_key.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + Effect = "Allow" + Principal = { + Service = "logs.amazonaws.com" + } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + } + ] + }) +} diff --git a/infrastructure/stacks/api-layer/cloudwatch.tf b/infrastructure/stacks/api-layer/cloudwatch.tf index 6f9ca738a..2b1b603d1 100644 --- a/infrastructure/stacks/api-layer/cloudwatch.tf +++ b/infrastructure/stacks/api-layer/cloudwatch.tf @@ -40,3 +40,10 @@ resource "aws_cloudwatch_log_group" "rotation_sfn_logs" { kms_key_id = module.secrets_manager.rotation_sns_key_arn retention_in_days = 365 } + +# CloudWatch Log Group for CloudTrail +resource "aws_cloudwatch_log_group" "cloudtrail_log_group" { + name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}elid-aws-cloudtrail-logs" + retention_in_days = 365 + kms_key_id = aws_kms_alias.cloudtrail_kms_alias.arn +} diff --git a/infrastructure/stacks/api-layer/cloudwatch_alarms.tf b/infrastructure/stacks/api-layer/cloudwatch_alarms.tf index f85defbae..e4907aa82 100644 --- a/infrastructure/stacks/api-layer/cloudwatch_alarms.tf +++ b/infrastructure/stacks/api-layer/cloudwatch_alarms.tf @@ -172,6 +172,15 @@ locals { alarm_description = "Multiple Lambda function changes detected within 10 minutes" actions_enabled = true } + DynamoDBTableReadOutsideLambdaRole = { + threshold = 1 + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 1 + period = 300 + statistic = "Sum" + alarm_description = "DynamoDB table read detected from non-Lambda execution role" + actions_enabled = true + } } # API Gateway alarm configuration diff --git a/infrastructure/stacks/api-layer/cloudwatch_metrics.tf b/infrastructure/stacks/api-layer/cloudwatch_metrics.tf index d45bddeea..3cbf09b45 100644 --- a/infrastructure/stacks/api-layer/cloudwatch_metrics.tf +++ b/infrastructure/stacks/api-layer/cloudwatch_metrics.tf @@ -114,6 +114,12 @@ locals { filter = "{($.eventSource=lambda.amazonaws.com) && (($.eventName=CreateFunction20150331) || ($.eventName=DeleteFunction20150331) || ($.eventName=UpdateFunctionCode20150331) || ($.eventName=UpdateFunctionConfiguration20150331))}" log_group_name = "NHSDAudit_trail_log_group" }, + { + name = "DynamoDBTableReadOutsideLambdaRole" + namespace = "security" + filter = "{($.eventSource=dynamodb.amazonaws.com) && (($.eventName=GetItem) || ($.eventName=Query) || ($.eventName=Scan) || ($.eventName=BatchGetItem) || ($.eventName=BatchWriteItem)) && ($.requestParameters.tableName=\"${module.eligibility_status_table.table_name}\") && ($.userIdentity.sessionContext.sessionIssuer.arn != \"${aws_iam_role.eligibility_lambda_role.arn}\")}" + log_group_name = aws_cloudwatch_log_group.cloudtrail_log_group.name + }, ] } diff --git a/infrastructure/stacks/api-layer/iam_policies.tf b/infrastructure/stacks/api-layer/iam_policies.tf index 560e9266e..61f803c51 100644 --- a/infrastructure/stacks/api-layer/iam_policies.tf +++ b/infrastructure/stacks/api-layer/iam_policies.tf @@ -813,3 +813,108 @@ resource "aws_iam_role_policy" "external_s3_kms_access_policy" { role = aws_iam_role.write_access_role[count.index].id policy = data.aws_iam_policy_document.s3_dq_kms_access_policy.json } + + +################################## +# Cloudtrail Bucket & KMS Policies +################################## + +# S3 Cloudtrail bucket policy +data "aws_iam_policy_document" "s3_cloudtrail_bucket_policy" { + statement { + sid = "AllowS3SSLRequestsOnly" + actions = [ + "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:PutObject", + "s3:GetBucketAcl" + ] + resources = [ + module.s3_cloudtrail_bucket.storage_bucket_arn, + "${module.s3_cloudtrail_bucket.storage_bucket_arn}/*", + ] + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + condition { + test = "Bool" + values = ["true"] + variable = "aws:SecureTransport" + } + } + statement { + sid = "DenyS3NonSSLRequests" + actions = [ + "s3:*" + ] + effect = "Deny" + resources = [ + module.s3_cloudtrail_bucket.storage_bucket_arn, + "${module.s3_cloudtrail_bucket.storage_bucket_arn}/*", + ] + principals { + type = "*" + identifiers = ["*"] + } + condition { + test = "Bool" + values = ["false"] + variable = "aws:SecureTransport" + } + } +} + +# Attach s3 Cloudtrail bucket policy to Cloudtrail role +resource "aws_s3_bucket_policy" "s3_cloudtrail_bucket_policy" { + bucket = module.s3_cloudtrail_bucket.storage_bucket_id + policy = data.aws_iam_policy_document.s3_cloudtrail_bucket_policy.json +} + +# S3 Cloudtrail bucket KMS access policy +data "aws_iam_policy_document" "s3_cloudtrail_kms_access_policy" { + statement { + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + resources = [ + module.s3_cloudtrail_bucket.storage_bucket_kms_key_arn + ] + } +} + +# Attach S3 Cloudtrail bucket KMS policy to Cloudtrail role +resource "aws_iam_role_policy" "s3_cloudtrail_kms_access_policy" { + name = "S3CloudTrailKMSAccess" + role = aws_iam_role.cloudtrail_cloudwatch_role.id + policy = data.aws_iam_policy_document.s3_cloudtrail_kms_access_policy.json +} + +# CloudWatch Logs permissions policy for CloudTrail +data "aws_iam_policy_document" "cloudtrail_cloudwatch_policy" { + statement { + effect = "Allow" + actions = [ + "logs:PutLogEvents", + "logs:CreateLogGroup", + "logs:CreateLogStream" + ] + resources = [ + aws_cloudwatch_log_group.cloudtrail_log_group.arn, + "${aws_cloudwatch_log_group.cloudtrail_log_group.arn}:*" + ] + + } +} + +# Attach CloudTrail CloudWatch Logs policy to CloudTrail role +resource "aws_iam_role_policy" "cloudtrail_cloudwatch_policy" { + name = "CloudTrailCloudWatchLogsAccess" + role = aws_iam_role.cloudtrail_cloudwatch_role.id + policy = data.aws_iam_policy_document.cloudtrail_cloudwatch_policy.json +} diff --git a/infrastructure/stacks/api-layer/iam_roles.tf b/infrastructure/stacks/api-layer/iam_roles.tf index e4c3dbe23..d66ae516d 100644 --- a/infrastructure/stacks/api-layer/iam_roles.tf +++ b/infrastructure/stacks/api-layer/iam_roles.tf @@ -142,3 +142,29 @@ resource "aws_iam_role_policy_attachment" "rotation_vpc_access" { role = aws_iam_role.rotation_lambda_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" } + +# IAM role for CloudTrail to write to CloudWatch Logs +resource "aws_iam_role" "cloudtrail_cloudwatch_role" { + name = "cloudtrail-cloudwatch-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "cloudtrail.amazonaws.com" + } + } + ] + }) + permissions_boundary = aws_iam_policy.assumed_role_permissions_boundary.arn + + tags = { + environment = var.environment + project_name = var.project_name + stack_name = local.stack_name + workspace = terraform.workspace + } +} diff --git a/infrastructure/stacks/api-layer/s3_buckets.tf b/infrastructure/stacks/api-layer/s3_buckets.tf index c2d924543..8e7b1ab35 100644 --- a/infrastructure/stacks/api-layer/s3_buckets.tf +++ b/infrastructure/stacks/api-layer/s3_buckets.tf @@ -57,3 +57,12 @@ module "s3_dq_metrics_bucket" { stack_name = local.stack_name workspace = terraform.workspace } + +module "s3_cloudtrail_bucket" { + source = "../../modules/s3" + bucket_name = "eli-cloudwatch-logs" + environment = var.environment + project_name = var.project_name + stack_name = local.stack_name + workspace = terraform.workspace +}