From afc8f090dcad7b2b15976fc7ec54e805d46d084e Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:24:46 +0100 Subject: [PATCH 01/22] eli-304 adding splunk firehose module --- .../modules/splunk_forwarder/cloudwatch.tf | 8 ++++++ .../modules/splunk_forwarder/eventbridge.tf | 5 ++++ .../modules/splunk_forwarder/firehose.tf | 23 ++++++++++++++++ .../modules/splunk_forwarder/iam.tf | 26 +++++++++++++++++++ .../modules/splunk_forwarder/variables.tf | 19 ++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 infrastructure/modules/splunk_forwarder/cloudwatch.tf create mode 100644 infrastructure/modules/splunk_forwarder/eventbridge.tf create mode 100644 infrastructure/modules/splunk_forwarder/firehose.tf create mode 100644 infrastructure/modules/splunk_forwarder/iam.tf create mode 100644 infrastructure/modules/splunk_forwarder/variables.tf diff --git a/infrastructure/modules/splunk_forwarder/cloudwatch.tf b/infrastructure/modules/splunk_forwarder/cloudwatch.tf new file mode 100644 index 000000000..5f7cabbb1 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/cloudwatch.tf @@ -0,0 +1,8 @@ +resource "aws_cloudwatch_event_rule" "alarm_state_change" { + name = "cloudwatch-alarm-state-change" + description = "Forward CloudWatch alarm state changes to Splunk via Firehose" + event_pattern = jsonencode({ + "source": ["aws.cloudwatch"], + "detail-type": ["CloudWatch Alarm State Change"] + }) +} diff --git a/infrastructure/modules/splunk_forwarder/eventbridge.tf b/infrastructure/modules/splunk_forwarder/eventbridge.tf new file mode 100644 index 000000000..c02e43057 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/eventbridge.tf @@ -0,0 +1,5 @@ +resource "aws_cloudwatch_event_target" "alarm_to_splunk" { + rule = aws_cloudwatch_event_rule.alarm_state_change.name + arn = aws_kinesis_firehose_delivery_stream.splunk_delivery_stream.arn + role_arn = aws_iam_role.eventbridge_to_firehose.arn +} diff --git a/infrastructure/modules/splunk_forwarder/firehose.tf b/infrastructure/modules/splunk_forwarder/firehose.tf new file mode 100644 index 000000000..945d3c379 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/firehose.tf @@ -0,0 +1,23 @@ +resource "aws_kinesis_firehose_delivery_stream" "splunk_delivery_stream" { + name = "splunk-alarm-events" + destination = "splunk" + + # VPC configuration is only supported for HTTP endpoint destinations in Kinesis Firehose + # For Splunk destinations, the service runs in AWS-managed VPC but you can control network access + # via the subnets where EventBridge (the source) runs and IAM policies + + splunk_configuration { + hec_endpoint = var.splunk_hec_endpoint + hec_token = var.splunk_hec_token + hec_endpoint_type = "Event" + s3_backup_mode = "FailedEventsOnly" + + s3_configuration { + role_arn = var.splunk_firehose_s3_role_arn + bucket_arn = var.splunk_firehose_s3_backup_arn + buffering_size = 10 + buffering_interval = 400 + compression_format = "GZIP" + } + } +} diff --git a/infrastructure/modules/splunk_forwarder/iam.tf b/infrastructure/modules/splunk_forwarder/iam.tf new file mode 100644 index 000000000..8fc8f9443 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/iam.tf @@ -0,0 +1,26 @@ +resource "aws_iam_role" "eventbridge_to_firehose" { + name = "eventbridge-to-firehose-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Principal = { Service = "events.amazonaws.com" }, + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_role_policy" "eventbridge_to_firehose_policy" { + role = aws_iam_role.eventbridge_to_firehose.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ], + Resource = aws_kinesis_firehose_delivery_stream.splunk_delivery_stream.arn + }] + }) +} diff --git a/infrastructure/modules/splunk_forwarder/variables.tf b/infrastructure/modules/splunk_forwarder/variables.tf new file mode 100644 index 000000000..3307fb1b5 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/variables.tf @@ -0,0 +1,19 @@ +variable "splunk_hec_endpoint" { + description = "Splunk HEC endpoint URL" + type = string +} + +variable "splunk_hec_token" { + description = "Splunk HEC token" + type = string +} + +variable "splunk_firehose_s3_backup_arn" { + description = "s3 bucket ARN for Firehose backups" + type = string +} + +variable "splunk_firehose_s3_role_arn" { + description = "IAM role ARN for Firehose to access S3" + type = string +} From 26e6f448096793c5b122148f9a06b3763eb1a97a Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:26:03 +0100 Subject: [PATCH 02/22] eli-304 adding splunk firehose to api-layer stack --- infrastructure/stacks/api-layer/data.tf | 9 ++ .../stacks/api-layer/iam_policies.tf | 47 ++++++++++ infrastructure/stacks/api-layer/iam_roles.tf | 15 +++ infrastructure/stacks/api-layer/s3_buckets.tf | 9 ++ .../stacks/api-layer/splunk_forwarder.tf | 8 ++ infrastructure/stacks/api-layer/ssm.tf | 94 +++++++++++++++++++ 6 files changed, 182 insertions(+) create mode 100644 infrastructure/stacks/api-layer/splunk_forwarder.tf create mode 100644 infrastructure/stacks/api-layer/ssm.tf diff --git a/infrastructure/stacks/api-layer/data.tf b/infrastructure/stacks/api-layer/data.tf index 6b159ad98..9318b2acc 100644 --- a/infrastructure/stacks/api-layer/data.tf +++ b/infrastructure/stacks/api-layer/data.tf @@ -28,3 +28,12 @@ data "aws_ssm_parameter" "mtls_api_ca_cert" { name = "/${var.environment}/mtls/api_ca_cert" with_decryption = true } + +data "aws_ssm_parameter" "splunk_hec_token" { + name = "/splunk/hec/token" + with_decryption = true +} +data "aws_ssm_parameter" "splunk_hec_endpoint" { + name = "/splunk/hec/endpoint" + with_decryption = true +} diff --git a/infrastructure/stacks/api-layer/iam_policies.tf b/infrastructure/stacks/api-layer/iam_policies.tf index 5f384895c..23fed6c83 100644 --- a/infrastructure/stacks/api-layer/iam_policies.tf +++ b/infrastructure/stacks/api-layer/iam_policies.tf @@ -284,6 +284,53 @@ resource "aws_kms_key_policy" "s3_rules_kms_key" { policy = data.aws_iam_policy_document.s3_rules_kms_key_policy.json } +resource "aws_iam_role_policy" "splunk_firehose_policy" { + name = "splunk-firehose-policy" + role = aws_iam_role.splunk_firehose_assume_role.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + # Allow Firehose to write to S3 backup bucket + { + Effect = "Allow", + Action = [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetBucketLocation", + "s3:ListBucket" + ], + Resource = [ + module.s3_firehose_backup_bucket.storage_bucket_arn, + "${module.s3_firehose_backup_bucket.storage_bucket_arn}/*" + ] + }, + # Allow Firehose to use KMS key for S3 encryption + { + Effect = "Allow", + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:DescribeKey" + ], + Resource = [module.s3_firehose_backup_bucket.storage_bucket_kms_key_arn] + }, + # Allow Firehose to access Splunk endpoint (no explicit IAM action needed, but you can restrict VPC/network access separately) + # Allow logging to CloudWatch + { + Effect = "Allow", + Action = [ + "logs:PutLogEvents", + "logs:CreateLogStream", + "logs:CreateLogGroup" + ], + Resource = "*" + } + ] + }) +} + data "aws_iam_policy_document" "s3_audit_kms_key_policy" { #checkov:skip=CKV_AWS_111: Root user needs full KMS key management #checkov:skip=CKV_AWS_356: Root user needs full KMS key management diff --git a/infrastructure/stacks/api-layer/iam_roles.tf b/infrastructure/stacks/api-layer/iam_roles.tf index 2fe2618dc..22244bbe7 100644 --- a/infrastructure/stacks/api-layer/iam_roles.tf +++ b/infrastructure/stacks/api-layer/iam_roles.tf @@ -33,6 +33,21 @@ data "aws_iam_policy_document" "firehose_assume_role" { } } +resource "aws_iam_role" "splunk_firehose_assume_role" { + name = "splunk-firehose-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Principal = { + Service = "firehose.amazonaws.com" + }, + Action = "sts:AssumeRole" + }] + }) +} + # Roles resource "aws_iam_role" "eligibility_lambda_role" { diff --git a/infrastructure/stacks/api-layer/s3_buckets.tf b/infrastructure/stacks/api-layer/s3_buckets.tf index a1c554575..ab087b406 100644 --- a/infrastructure/stacks/api-layer/s3_buckets.tf +++ b/infrastructure/stacks/api-layer/s3_buckets.tf @@ -16,3 +16,12 @@ module "s3_audit_bucket" { stack_name = local.stack_name workspace = terraform.workspace } + +module "s3_firehose_backup_bucket" { + source = "../../modules/s3" + bucket_name = "eli-splunk-backup" + environment = var.environment + project_name = var.project_name + stack_name = local.stack_name + workspace = terraform.workspace +} diff --git a/infrastructure/stacks/api-layer/splunk_forwarder.tf b/infrastructure/stacks/api-layer/splunk_forwarder.tf new file mode 100644 index 000000000..24862e592 --- /dev/null +++ b/infrastructure/stacks/api-layer/splunk_forwarder.tf @@ -0,0 +1,8 @@ +module "splunk_forwarder" { + source = "../../modules/splunk_forwarder" + + splunk_hec_endpoint = data.aws_ssm_parameter.splunk_hec_endpoint.value + splunk_hec_token = data.aws_ssm_parameter.splunk_hec_token.value + splunk_firehose_s3_role_arn = aws_iam_role.splunk_firehose_assume_role.arn + splunk_firehose_s3_backup_arn = module.s3_firehose_backup_bucket.storage_bucket_arn +} diff --git a/infrastructure/stacks/api-layer/ssm.tf b/infrastructure/stacks/api-layer/ssm.tf new file mode 100644 index 000000000..5667fba48 --- /dev/null +++ b/infrastructure/stacks/api-layer/ssm.tf @@ -0,0 +1,94 @@ +resource "aws_kms_key" "splunk_hec_kms" { + description = "KMS key for encrypting Splunk HEC SSM parameters" + deletion_window_in_days = 7 + + tags = { + Name = "splunk-hec-ssm-kms-key" + Environment = var.environment + Stack = local.stack_name + Purpose = "Splunk HEC SSM encryption" + ManagedBy = "terraform" + } +} + +resource "aws_kms_key_policy" "splunk_hec_kms_policy" { + key_id = aws_kms_key.splunk_hec_kms.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "AllowRootAccountFullAccess" + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "AllowSSMServiceUseOfKey" + Effect = "Allow" + Principal = { Service = "ssm.amazonaws.com" } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + }, + { + Sid = "AllowFirehoseServiceUseOfKey" + Effect = "Allow" + Principal = { Service = "firehose.amazonaws.com" } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + }, + ] + }) +} + +resource "aws_ssm_parameter" "splunk_hec_token" { + name = "/splunk/hec/token" + description = "Splunk HEC token" + type = "SecureString" + key_id = aws_kms_key.splunk_hec_kms.id + value = "REPLACE_ME" # Set a placeholder value + tier = "Advanced" + + tags = { + Environment = var.environment + Stack = local.stack_name + Purpose = "Splunk HEC token" + ManagedBy = "terraform" + } + + lifecycle { + ignore_changes = [value] + } +} + +resource "aws_ssm_parameter" "splunk_hec_endpoint" { + name = "/splunk/hec/endpoint" + description = "Splunk HEC endpoint" + type = "SecureString" + key_id = aws_kms_key.splunk_hec_kms.id + value = "REPLACE_ME" # Set a placeholder value + tier = "Advanced" + + tags = { + Environment = var.environment + Stack = local.stack_name + Purpose = "Splunk HEC endpoint" + ManagedBy = "terraform" + } + + lifecycle { + ignore_changes = [value] + } +} From c99d0d8c5694f6363bc07a7418347c2adcc5d151 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:33:20 +0100 Subject: [PATCH 03/22] eli-304 adding kms encryption to firehose and ssm, refactoring so specific eventbridge feed is in api-layer, so that we can add additional splunk firehose feeds in future if needed (e.g. keep the module generic) --- .../modules/splunk_forwarder/cloudwatch.tf | 8 -- .../modules/splunk_forwarder/data.tf | 1 + .../modules/splunk_forwarder/eventbridge.tf | 5 -- .../modules/splunk_forwarder/firehose.tf | 28 +++++- .../modules/splunk_forwarder/iam.tf | 72 ++++++++++----- .../modules/splunk_forwarder/outputs.tf | 11 +++ .../stacks/api-layer/eventbridge.tf | 88 +++++++++++++++++++ .../stacks/api-layer/iam_policies.tf | 1 - infrastructure/stacks/api-layer/iam_roles.tf | 7 +- 9 files changed, 184 insertions(+), 37 deletions(-) delete mode 100644 infrastructure/modules/splunk_forwarder/cloudwatch.tf create mode 100644 infrastructure/modules/splunk_forwarder/data.tf delete mode 100644 infrastructure/modules/splunk_forwarder/eventbridge.tf create mode 100644 infrastructure/modules/splunk_forwarder/outputs.tf create mode 100644 infrastructure/stacks/api-layer/eventbridge.tf diff --git a/infrastructure/modules/splunk_forwarder/cloudwatch.tf b/infrastructure/modules/splunk_forwarder/cloudwatch.tf deleted file mode 100644 index 5f7cabbb1..000000000 --- a/infrastructure/modules/splunk_forwarder/cloudwatch.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "aws_cloudwatch_event_rule" "alarm_state_change" { - name = "cloudwatch-alarm-state-change" - description = "Forward CloudWatch alarm state changes to Splunk via Firehose" - event_pattern = jsonencode({ - "source": ["aws.cloudwatch"], - "detail-type": ["CloudWatch Alarm State Change"] - }) -} diff --git a/infrastructure/modules/splunk_forwarder/data.tf b/infrastructure/modules/splunk_forwarder/data.tf new file mode 100644 index 000000000..8fc4b38cc --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/data.tf @@ -0,0 +1 @@ +data "aws_caller_identity" "current" {} diff --git a/infrastructure/modules/splunk_forwarder/eventbridge.tf b/infrastructure/modules/splunk_forwarder/eventbridge.tf deleted file mode 100644 index c02e43057..000000000 --- a/infrastructure/modules/splunk_forwarder/eventbridge.tf +++ /dev/null @@ -1,5 +0,0 @@ -resource "aws_cloudwatch_event_target" "alarm_to_splunk" { - rule = aws_cloudwatch_event_rule.alarm_state_change.name - arn = aws_kinesis_firehose_delivery_stream.splunk_delivery_stream.arn - role_arn = aws_iam_role.eventbridge_to_firehose.arn -} diff --git a/infrastructure/modules/splunk_forwarder/firehose.tf b/infrastructure/modules/splunk_forwarder/firehose.tf index 945d3c379..535569e5b 100644 --- a/infrastructure/modules/splunk_forwarder/firehose.tf +++ b/infrastructure/modules/splunk_forwarder/firehose.tf @@ -1,7 +1,33 @@ + + +# KMS Key for Firehose encryption +resource "aws_kms_key" "firehose_splunk_cmk" { + description = "KMS key for encrypting Kinesis Firehose delivery stream data" + deletion_window_in_days = 7 + enable_key_rotation = true + tags = { + Name = "firehose-splunk-cmk" + Purpose = "Firehose encryption" + ManagedBy = "terraform" + } +} + +# KMS Key Alias for easier identification +resource "aws_kms_alias" "firehose_splunk_cmk_alias" { + name = "alias/firehose-splunk-cmk" + target_key_id = aws_kms_key.firehose_splunk_cmk.key_id +} + +# KMS Key Policy for Firehose + resource "aws_kinesis_firehose_delivery_stream" "splunk_delivery_stream" { name = "splunk-alarm-events" destination = "splunk" - + server_side_encryption { + enabled = true + key_type = "CUSTOMER_MANAGED_CMK" + key_arn = aws_kms_key.firehose_splunk_cmk.arn + } # VPC configuration is only supported for HTTP endpoint destinations in Kinesis Firehose # For Splunk destinations, the service runs in AWS-managed VPC but you can control network access # via the subnets where EventBridge (the source) runs and IAM policies diff --git a/infrastructure/modules/splunk_forwarder/iam.tf b/infrastructure/modules/splunk_forwarder/iam.tf index 8fc8f9443..7b5c5946c 100644 --- a/infrastructure/modules/splunk_forwarder/iam.tf +++ b/infrastructure/modules/splunk_forwarder/iam.tf @@ -1,26 +1,56 @@ -resource "aws_iam_role" "eventbridge_to_firehose" { - name = "eventbridge-to-firehose-role" - assume_role_policy = jsonencode({ - Version = "2012-10-17", - Statement = [{ - Effect = "Allow", - Principal = { Service = "events.amazonaws.com" }, - Action = "sts:AssumeRole" - }] - }) -} +# EventBridge IAM roles now defined in api-layer stack for specific integration -resource "aws_iam_role_policy" "eventbridge_to_firehose_policy" { - role = aws_iam_role.eventbridge_to_firehose.id +resource "aws_kms_key_policy" "firehose_splunk_cmk_policy" { + key_id = aws_kms_key.firehose_splunk_cmk.id policy = jsonencode({ Version = "2012-10-17", - Statement = [{ - Effect = "Allow", - Action = [ - "firehose:PutRecord", - "firehose:PutRecordBatch" - ], - Resource = aws_kinesis_firehose_delivery_stream.splunk_delivery_stream.arn - }] + Statement = [ + { + Sid = "AllowRootAccountFullAccess" + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "AllowFirehoseServiceUseOfKey" + Effect = "Allow" + Principal = { Service = "firehose.amazonaws.com" } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + }, + { + Sid = "AllowEventBridgeUseOfKey" + Effect = "Allow" + Principal = { Service = "events.amazonaws.com" } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + }, + { + Sid = "AllowCloudWatchUseOfKey" + Effect = "Allow" + Principal = { Service = "cloudwatch.amazonaws.com" } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + } + ] }) } diff --git a/infrastructure/modules/splunk_forwarder/outputs.tf b/infrastructure/modules/splunk_forwarder/outputs.tf new file mode 100644 index 000000000..03bcfe1a0 --- /dev/null +++ b/infrastructure/modules/splunk_forwarder/outputs.tf @@ -0,0 +1,11 @@ +# Output the Firehose delivery stream ARN for use by EventBridge +output "firehose_delivery_stream_arn" { + description = "ARN of the Kinesis Firehose delivery stream for Splunk" + value = aws_kinesis_firehose_delivery_stream.splunk_delivery_stream.arn +} + +# Output the KMS key ARN for reference +output "firehose_kms_key_arn" { + description = "ARN of the KMS key used for Firehose encryption" + value = aws_kms_key.firehose_splunk_cmk.arn +} diff --git a/infrastructure/stacks/api-layer/eventbridge.tf b/infrastructure/stacks/api-layer/eventbridge.tf new file mode 100644 index 000000000..e9d013a3e --- /dev/null +++ b/infrastructure/stacks/api-layer/eventbridge.tf @@ -0,0 +1,88 @@ +# IAM role for EventBridge to write to Firehose +resource "aws_iam_role" "eventbridge_firehose_role" { + name = "${var.environment}-eventbridge-to-firehose-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + Service = "events.amazonaws.com" + } + Action = "sts:AssumeRole" + }] + }) + + tags = { + Environment = var.environment + Purpose = "splunk-forwarding" + ManagedBy = "terraform" + } +} + +# IAM policy for EventBridge to access Firehose +resource "aws_iam_role_policy" "eventbridge_to_firehose_policy" { + name = "${var.environment}-eventbridge-to-firehose-policy" + role = aws_iam_role.eventbridge_firehose_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ] + Resource = module.splunk_forwarder.firehose_delivery_stream_arn + }] + }) +} + +# EventBridge rule to capture CloudWatch alarm state changes +resource "aws_cloudwatch_event_rule" "alarm_state_change" { + name = "cloudwatch-alarm-state-change-to-splunk" + description = "Forward CloudWatch alarm state changes to Splunk via Firehose" + + event_pattern = jsonencode({ + source = ["aws.cloudwatch"] + detail-type = ["CloudWatch Alarm State Change"] + }) + + tags = { + Environment = var.environment + Purpose = "splunk-forwarding" + ManagedBy = "terraform" + } +} + +# EventBridge target to send events to Firehose +resource "aws_cloudwatch_event_target" "firehose_target" { + rule = aws_cloudwatch_event_rule.alarm_state_change.name + arn = module.splunk_forwarder.firehose_delivery_stream_arn + role_arn = aws_iam_role.eventbridge_firehose_role.arn + + # Transform the CloudWatch alarm event into a format suitable for Splunk + input_transformer { + input_paths = { + account = "$.account" + region = "$.region" + time = "$.time" + alarm_name = "$.detail.alarmName" + new_state = "$.detail.state.value" + old_state = "$.detail.previousState.value" + reason = "$.detail.state.reason" + } + + input_template = jsonencode({ + timestamp = "