Skip to content

Commit 65c13cf

Browse files
Karthikeyannhseddalmond1
authored andcommitted
Eli 387 lambda hardening (#306)
* provisioned concurrency * enable dead letter queue * enhanced monitoring * lambda function versioning * provison concurrancy - alias version fix * removed checkov as we implemented dead letter queue * prod conditions and github roles * github roles * fix for corrupt kms policy * create queue * get the latest function for concurrant provisioning
1 parent dbbbe3f commit 65c13cf

File tree

10 files changed

+174
-58
lines changed

10 files changed

+174
-58
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
data "aws_caller_identity" "current" {}
2+
3+
data "aws_lambda_function" "existing" {
4+
function_name = var.lambda_func_name
5+
qualifier = "$LATEST"
6+
}

infrastructure/modules/lambda/kms.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ resource "aws_kms_key" "lambda_cmk" {
77
}
88

99
resource "aws_kms_alias" "lambda_cmk" {
10-
name = "alias/${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}${var.lambda_func_name}-cmk"
10+
name = "alias/${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}${var.lambda_func_name}-key"
1111
target_key_id = aws_kms_key.lambda_cmk.key_id
1212
}
1313

infrastructure/modules/lambda/lambda.tf

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
resource "aws_lambda_function" "eligibility_signposting_lambda" {
2-
#checkov:skip=CKV_AWS_116: No deadletter queue is configured for this Lambda function, yet
32
#checkov:skip=CKV_AWS_115: Concurrent execution limit will be set at APIM level, not at Lambda level
43
#checkov:skip=CKV_AWS_272: Skipping code signing but flagged to create ticket to investigate on ELI-238
54
# If the file is not in the current working directory you will need to include a
@@ -11,7 +10,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
1110

1211
source_code_hash = filebase64sha256(var.file_name)
1312

14-
runtime = "python3.13"
13+
runtime = var.runtime
1514
timeout = 30
1615
memory_size = 2048
1716

@@ -33,7 +32,37 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
3332
security_group_ids = var.security_group_ids
3433
}
3534

35+
dead_letter_config {
36+
target_arn = aws_sqs_queue.lambda_dlq.arn
37+
}
38+
39+
layers = compact([
40+
var.environment == "prod" ? "arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}" : null
41+
])
42+
3643
tracing_config {
3744
mode = "Active"
3845
}
3946
}
47+
48+
# lambda alias required for provisioning concurrency
49+
resource "aws_lambda_alias" "campaign_alias" {
50+
count = var.environment == "prod" ? 1 : 0
51+
name = "live"
52+
function_name = coalesce(
53+
aws_lambda_function.eligibility_signposting_lambda.function_name,
54+
data.aws_lambda_function.existing.function_name
55+
)
56+
function_version = coalesce(
57+
aws_lambda_function.eligibility_signposting_lambda.version,
58+
data.aws_lambda_function.existing.version
59+
)
60+
}
61+
62+
# provisioned concurrency - number of pre-warmed lambda containers
63+
resource "aws_lambda_provisioned_concurrency_config" "campaign_pc" {
64+
count = var.environment == "prod" ? 1 : 0
65+
function_name = var.lambda_func_name
66+
qualifier = aws_lambda_alias.campaign_alias[0].name
67+
provisioned_concurrent_executions = var.provisioned_concurrency_count
68+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
resource "aws_sqs_queue" "lambda_dlq" {
2+
name = "${var.lambda_func_name}_dead_letter_queue"
3+
kms_master_key_id = aws_kms_key.lambda_cmk.id
4+
tags = var.tags
5+
}
6+
7+
# sql policy attachment
8+
resource "aws_iam_role_policy" "lambda_sqs_send_inline" {
9+
name = "LambdaSQSMessageSendPolicy"
10+
role = var.eligibility_lambda_role_name
11+
12+
policy = jsonencode({
13+
Version = "2012-10-17",
14+
Statement = [
15+
{
16+
Sid = "AllowSQSSendMessage",
17+
Effect = "Allow",
18+
Action = ["sqs:SendMessage"],
19+
Resource = aws_sqs_queue.lambda_dlq.arn
20+
}
21+
]
22+
})
23+
}

infrastructure/modules/lambda/variables.tf

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
variable "eligibility_lambda_role_arn" {
2-
description = "lambda read role arn for dynamodb"
2+
description = "lambda role arn"
3+
type = string
4+
}
5+
6+
variable "eligibility_lambda_role_name" {
7+
description = "lambda role name"
38
type = string
49
}
510

@@ -8,6 +13,12 @@ variable "lambda_func_name" {
813
type = string
914
}
1015

16+
variable "runtime" {
17+
description = "runtime of the Lambda function"
18+
type = string
19+
}
20+
21+
1122
variable "vpc_intra_subnets" {
1223
description = "vpc private subnets for lambda"
1324
type = list(string)
@@ -52,3 +63,13 @@ variable "enable_xray_patching"{
5263
description = "flag to enable xray tracing, which puts an entry for dynamodb, s3 and firehose in trace map"
5364
type = string
5465
}
66+
67+
variable "provisioned_concurrency_count" {
68+
description = "Number of prewarmed Lambda instances"
69+
type = number
70+
}
71+
72+
variable "lambda_insights_extension_version" {
73+
description = "version number of LambdaInsightsExtension"
74+
type = number
75+
}

infrastructure/stacks/api-layer/assumed_role_permissions_boundary.tf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ data "aws_iam_policy_document" "assumed_role_permissions_boundary" {
5252

5353
# X-Ray - Lambda tracing
5454
"xray:PutTraceSegments",
55-
"xray:PutTelemetryRecords"
55+
"xray:PutTelemetryRecords",
56+
57+
#SQS - message management
58+
"sqs:SendMessage"
5659
]
5760

5861
resources = ["*"]

infrastructure/stacks/api-layer/iam_policies.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ resource "aws_iam_role_policy_attachment" "lambda_logs_policy_attachment" {
189189
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
190190
}
191191

192+
#Attach CloudWatchLambdaInsightsExecutionRolePolicy to lambda for enhanced monitoring
193+
resource "aws_iam_role_policy_attachment" "lambda_insights_policy" {
194+
role = aws_iam_role.eligibility_lambda_role.name
195+
policy_arn = "arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy"
196+
}
197+
192198
# Policy doc for S3 Audit bucket
193199
data "aws_iam_policy_document" "s3_audit_bucket_policy" {
194200
statement {

infrastructure/stacks/api-layer/lambda.tf

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ data "aws_subnet" "private_subnets" {
1111
}
1212

1313
module "eligibility_signposting_lambda_function" {
14-
source = "../../modules/lambda"
15-
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
16-
workspace = local.workspace
17-
environment = var.environment
18-
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
19-
security_group_ids = [data.aws_security_group.main_sg.id]
20-
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
21-
file_name = "../../../dist/lambda.zip"
22-
handler = "eligibility_signposting_api.app.lambda_handler"
23-
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
24-
eligibility_status_table_name = module.eligibility_status_table.table_name
25-
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
26-
log_level = "INFO"
27-
enable_xray_patching = "true"
28-
stack_name = local.stack_name
14+
source = "../../modules/lambda"
15+
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
16+
eligibility_lambda_role_name = aws_iam_role.eligibility_lambda_role.name
17+
workspace = local.workspace
18+
environment = var.environment
19+
runtime = "python3.13"
20+
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
21+
security_group_ids = [data.aws_security_group.main_sg.id]
22+
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
23+
file_name = "../../../dist/lambda.zip"
24+
handler = "eligibility_signposting_api.app.lambda_handler"
25+
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
26+
eligibility_status_table_name = module.eligibility_status_table.table_name
27+
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
28+
lambda_insights_extension_version = 38
29+
log_level = "INFO"
30+
enable_xray_patching = "true"
31+
stack_name = local.stack_name
32+
provisioned_concurrency_count = 5
2933
}

infrastructure/stacks/iams-developer-roles/github_actions_policies.tf

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,17 @@ resource "aws_iam_policy" "lambda_management" {
6262
"lambda:ListAliases",
6363
"lambda:AddPermission",
6464
"lambda:RemovePermission",
65-
"lambda:GetPolicy"
65+
"lambda:GetPolicy",
66+
"lambda:GetAlias",
67+
"lambda:GetFunction",
68+
"lambda:GetProvisionedConcurrencyConfig",
69+
"lambda:GetLayerVersion",
70+
"lambda:PutProvisionedConcurrencyConfig"
6671
],
6772
Resource = [
68-
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:*eligibility_signposting_api"
73+
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:eligibility_signposting_api",
74+
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:eligibility_signposting_api:*",
75+
"arn:aws:lambda:*:580247275435:layer:LambdaInsightsExtension:*"
6976
]
7077
}
7178
]
@@ -465,29 +472,6 @@ data "aws_iam_policy_document" "github_actions_assume_role" {
465472
}
466473
}
467474

468-
resource "aws_iam_policy" "cloudwatch_logging" {
469-
name = "cloudwatch-logging-management"
470-
description = "Allow access to logging resources"
471-
path = "/service-policies/"
472-
473-
policy = jsonencode({
474-
Version = "2012-10-17",
475-
Statement = [
476-
{
477-
Effect = "Allow",
478-
Action = [
479-
"logs:ListTagsForResource",
480-
"logs:DescribeLogGroups",
481-
"logs:PutRetentionPolicy"
482-
],
483-
Resource = "arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/*"
484-
}
485-
]
486-
})
487-
488-
tags = merge(local.tags, { Name = "cloudwatch-logging-management" })
489-
}
490-
491475
resource "aws_iam_policy" "firehose_readonly" {
492476
name = "firehose-describe-access"
493477
description = "Allow GitHub Actions to describe Firehose delivery stream"
@@ -518,9 +502,9 @@ resource "aws_iam_policy" "firehose_readonly" {
518502
tags = merge(local.tags, { Name = "firehose-describe-access" })
519503
}
520504

521-
resource "aws_iam_policy" "cloudwatch_alarms" {
522-
name = "cloudwatch-alarms-management"
523-
description = "Allow GitHub Actions to manage CloudWatch alarms and SNS topics"
505+
resource "aws_iam_policy" "cloudwatch_management" {
506+
name = "cloudwatch-management"
507+
description = "Allow GitHub Actions to manage CloudWatch logs, alarms, and SNS topics"
524508
path = "/service-policies/"
525509

526510
policy = jsonencode({
@@ -529,15 +513,18 @@ resource "aws_iam_policy" "cloudwatch_alarms" {
529513
{
530514
Effect = "Allow",
531515
Action = [
532-
# CloudWatch Alarms management
516+
"logs:ListTagsForResource",
517+
"logs:DescribeLogGroups",
518+
"logs:PutRetentionPolicy",
519+
533520
"cloudwatch:PutMetricAlarm",
534521
"cloudwatch:DeleteAlarms",
535522
"cloudwatch:DescribeAlarms",
536523
"cloudwatch:DescribeAlarmsForMetric",
537524
"cloudwatch:ListTagsForResource",
538525
"cloudwatch:TagResource",
539526
"cloudwatch:UntagResource",
540-
# SNS Topic management for alarm notifications
527+
541528
"sns:CreateTopic",
542529
"sns:DeleteTopic",
543530
"sns:GetTopicAttributes",
@@ -552,14 +539,41 @@ resource "aws_iam_policy" "cloudwatch_alarms" {
552539
"sns:ListSubscriptionsByTopic"
553540
],
554541
Resource = [
542+
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/*",
555543
"arn:aws:cloudwatch:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:alarm:*",
556544
"arn:aws:sns:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:cloudwatch-security-alarms*"
557545
]
558546
}
559547
]
560548
})
561549

562-
tags = merge(local.tags, { Name = "cloudwatch-alarms-management" })
550+
tags = merge(local.tags, { Name = "cloudwatch-management" })
551+
}
552+
553+
# SQS Management Policy for GetQueueAttributes
554+
resource "aws_iam_policy" "sqs_management" {
555+
name = "sqs-management"
556+
description = "Policy granting permissions to get SQS queue attributes"
557+
path = "/service-policies/"
558+
559+
policy = jsonencode({
560+
Version = "2012-10-17",
561+
Statement = [
562+
{
563+
Effect = "Allow",
564+
Action = [
565+
"sqs:GetQueueAttributes",
566+
"sqs:listqueuetags",
567+
"sqs:createqueue"
568+
],
569+
Resource = [
570+
"arn:aws:sqs:eu-west-2:${data.aws_caller_identity.current.account_id}:*"
571+
]
572+
}
573+
]
574+
})
575+
576+
tags = merge(local.tags, { Name = "sqs-management" })
563577
}
564578

565579
# Attach the policies to the role
@@ -598,17 +612,18 @@ resource "aws_iam_role_policy_attachment" "iam_management" {
598612
policy_arn = aws_iam_policy.iam_management.arn
599613
}
600614

601-
resource "aws_iam_role_policy_attachment" "cloudwatch_logging" {
615+
resource "aws_iam_role_policy_attachment" "firehose_readonly_attach" {
602616
role = aws_iam_role.github_actions.name
603-
policy_arn = aws_iam_policy.cloudwatch_logging.arn
617+
policy_arn = aws_iam_policy.firehose_readonly.arn
604618
}
605619

606-
resource "aws_iam_role_policy_attachment" "firehose_readonly_attach" {
620+
resource "aws_iam_role_policy_attachment" "cloudwatch_management" {
607621
role = aws_iam_role.github_actions.name
608-
policy_arn = aws_iam_policy.firehose_readonly.arn
622+
policy_arn = aws_iam_policy.cloudwatch_management.arn
609623
}
610624

611-
resource "aws_iam_role_policy_attachment" "cloudwatch_alarms" {
625+
resource "aws_iam_role_policy_attachment" "sqs_management" {
612626
role = aws_iam_role.github_actions.name
613-
policy_arn = aws_iam_policy.cloudwatch_alarms.arn
627+
policy_arn = aws_iam_policy.sqs_management.arn
614628
}
629+

infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ data "aws_iam_policy_document" "permissions_boundary" {
150150
"lambda:AddPermission",
151151
"lambda:RemovePermission",
152152
"lambda:GetPolicy",
153+
"lambda:GetAlias",
154+
"lambda:GetProvisionedConcurrencyConfig",
155+
"lambda:GetLayerVersion",
156+
"lambda:PutProvisionedConcurrencyConfig",
153157

154158
# CloudWatch Logs - log management
155159
"logs:CreateLogGroup",
@@ -217,7 +221,13 @@ data "aws_iam_policy_document" "permissions_boundary" {
217221
"ssm:GetParameters",
218222
"ssm:ListTagsForResource",
219223
"ssm:PutParameter",
220-
"ssm:AddTagsToResource"
224+
"ssm:AddTagsToResource",
225+
226+
#SQS - message management
227+
"sqs:SendMessage",
228+
"sqs:GetQueueAttributes",
229+
"sqs:listqueuetags",
230+
"sqs:createqueue"
221231
]
222232

223233
resources = ["*"]

0 commit comments

Comments
 (0)