Skip to content

Commit 7c61400

Browse files
committed
DC-205: Add automatic SES SMTP credential rotation via Secrets Manager
1 parent b7dc266 commit 7c61400

7 files changed

Lines changed: 163 additions & 9 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ output
454454
**/*.tfplan
455455
**/crash.log
456456
**/crash.*.log
457+
# Lambda zip artifacts created by the archive_file data source during tofu plan/apply
458+
tofu/modules/sebt_ses/lambda/*.zip
457459

458460

459461
# temporary files

tofu/modules/sebt_application/main.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ module "ses" {
158158

159159
sender_email = var.sender_email
160160
allowed_recipients = var.ses_allowed_recipients
161+
162+
ecs_cluster_name = module.api.cluster_name
163+
ecs_service_name = module.api.cluster_name
161164
}
162165

163166
module "cloudfront_waf" {

tofu/modules/sebt_ses/data.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1+
data "aws_caller_identity" "current" {}
12
data "aws_partition" "current" {}
23
data "aws_region" "current" {}
4+
5+
data "archive_file" "rotation_lambda" {
6+
type = "zip"
7+
source_file = "${path.module}/lambda/rotate_smtp_credentials.py"
8+
output_path = "${path.module}/lambda/rotate_smtp_credentials.zip"
9+
}

tofu/modules/sebt_ses/main.tf

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,6 @@ resource "aws_iam_user_policy" "smtp" {
8888
})
8989
}
9090

91-
resource "aws_iam_access_key" "smtp" {
92-
user = aws_iam_user.smtp.name
93-
}
94-
9591
resource "aws_secretsmanager_secret" "smtp" {
9692
name = "${local.prefix}-ses-smtp-credentials"
9793
description = "SES SMTP credentials for ${local.prefix}."
@@ -101,11 +97,132 @@ resource "aws_secretsmanager_secret" "smtp" {
10197
}
10298
}
10399

104-
resource "aws_secretsmanager_secret_version" "smtp" {
105-
secret_id = aws_secretsmanager_secret.smtp.id
100+
# Credential rotation Lambda and supporting resources.
101+
# Based on the AWS sample at:
102+
# https://github.com/aws-samples/serverless-mail/tree/ses-credential-rotation/ses-credential-rotation
103+
104+
resource "aws_iam_role" "rotation_lambda" {
105+
name = "${local.prefix}-ses-smtp-rotation"
106+
path = "/system/"
107+
108+
assume_role_policy = jsonencode({
109+
Version = "2012-10-17"
110+
Statement = [
111+
{
112+
Effect = "Allow"
113+
Principal = {
114+
Service = "lambda.amazonaws.com"
115+
}
116+
Action = "sts:AssumeRole"
117+
}
118+
]
119+
})
120+
121+
tags = {
122+
Name = "${local.prefix}-ses-smtp-rotation"
123+
}
124+
}
125+
126+
resource "aws_iam_role_policy" "rotation_lambda" {
127+
name = "ses-smtp-rotation"
128+
role = aws_iam_role.rotation_lambda.id
106129

107-
secret_string = jsonencode({
108-
username = aws_iam_access_key.smtp.id
109-
password = aws_iam_access_key.smtp.ses_smtp_password_v4
130+
policy = jsonencode({
131+
Version = "2012-10-17"
132+
Statement = [
133+
{
134+
Sid = "ManageIamKeys"
135+
Effect = "Allow"
136+
Action = [
137+
"iam:CreateAccessKey",
138+
"iam:DeleteAccessKey",
139+
"iam:ListAccessKeys",
140+
]
141+
Resource = aws_iam_user.smtp.arn
142+
},
143+
{
144+
Sid = "ManageSecret"
145+
Effect = "Allow"
146+
Action = [
147+
"secretsmanager:DescribeSecret",
148+
"secretsmanager:GetSecretValue",
149+
"secretsmanager:PutSecretValue",
150+
"secretsmanager:UpdateSecretVersionStage",
151+
]
152+
Resource = aws_secretsmanager_secret.smtp.arn
153+
},
154+
{
155+
Sid = "RedeployEcsService"
156+
Effect = "Allow"
157+
Action = "ecs:UpdateService"
158+
Resource = "arn:${data.aws_partition.current.partition}:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/${var.ecs_cluster_name}/${var.ecs_service_name}"
159+
},
160+
{
161+
Sid = "WriteLogs"
162+
Effect = "Allow"
163+
Action = [
164+
"logs:CreateLogStream",
165+
"logs:PutLogEvents",
166+
]
167+
Resource = "${aws_cloudwatch_log_group.rotation_lambda.arn}:*"
168+
},
169+
]
110170
})
111171
}
172+
173+
resource "aws_cloudwatch_log_group" "rotation_lambda" {
174+
name = "/aws/lambda/${local.prefix}-ses-smtp-rotation"
175+
retention_in_days = 30
176+
177+
tags = {
178+
Name = "${local.prefix}-ses-smtp-rotation"
179+
}
180+
}
181+
182+
resource "aws_lambda_function" "rotation" {
183+
function_name = "${local.prefix}-ses-smtp-rotation"
184+
description = "Rotates SES SMTP credentials for ${local.prefix}."
185+
role = aws_iam_role.rotation_lambda.arn
186+
handler = "rotate_smtp_credentials.handler"
187+
runtime = "python3.12"
188+
timeout = 75
189+
architectures = ["arm64"]
190+
filename = data.archive_file.rotation_lambda.output_path
191+
source_code_hash = data.archive_file.rotation_lambda.output_base64sha256
192+
193+
environment {
194+
variables = {
195+
IAM_USERNAME = aws_iam_user.smtp.name
196+
SMTP_ENDPOINT = local.smtp_server
197+
ECS_CLUSTER = var.ecs_cluster_name
198+
ECS_SERVICE = var.ecs_service_name
199+
}
200+
}
201+
202+
depends_on = [aws_cloudwatch_log_group.rotation_lambda]
203+
204+
tags = {
205+
Name = "${local.prefix}-ses-smtp-rotation"
206+
}
207+
}
208+
209+
resource "aws_lambda_permission" "secrets_manager" {
210+
statement_id = "AllowSecretsManagerInvocation"
211+
action = "lambda:InvokeFunction"
212+
function_name = aws_lambda_function.rotation.function_name
213+
principal = "secretsmanager.amazonaws.com"
214+
source_arn = aws_secretsmanager_secret.smtp.arn
215+
}
216+
217+
resource "aws_secretsmanager_secret_rotation" "smtp" {
218+
secret_id = aws_secretsmanager_secret.smtp.id
219+
rotation_lambda_arn = aws_lambda_function.rotation.arn
220+
221+
rotation_rules {
222+
automatically_after_days = var.rotation_interval_days
223+
}
224+
225+
# The existing SMTP credentials are still valid — just enable the schedule
226+
# without forcing an immediate rotation on first deploy.
227+
rotate_immediately = false
228+
}

tofu/modules/sebt_ses/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ output "smtp_server" {
1212
description = "SES SMTP server endpoint."
1313
value = local.smtp_server
1414
}
15+
16+
output "rotation_lambda_arn" {
17+
description = "ARN of the credential rotation Lambda function."
18+
value = aws_lambda_function.rotation.arn
19+
}

tofu/modules/sebt_ses/variables.tf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,19 @@ variable "sender_email" {
2929
type = string
3030
description = "Email address used as the sender for outgoing emails."
3131
}
32+
33+
variable "ecs_cluster_name" {
34+
type = string
35+
description = "Name of the ECS cluster running the API service (for redeployment on credential rotation)."
36+
}
37+
38+
variable "ecs_service_name" {
39+
type = string
40+
description = "Name of the ECS service to redeploy when SMTP credentials are rotated."
41+
}
42+
43+
variable "rotation_interval_days" {
44+
type = number
45+
description = "Number of days between automatic SMTP credential rotations."
46+
default = 30
47+
}

tofu/modules/sebt_ses/versions.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ terraform {
22
required_version = ">= 1.6.0"
33

44
required_providers {
5+
archive = {
6+
source = "hashicorp/archive"
7+
version = ">= 2.0"
8+
}
59
aws = {
610
source = "hashicorp/aws"
711
version = ">= 5.44"

0 commit comments

Comments
 (0)