From f37733eee718a181dae1bb0389e6e638f99fa0a7 Mon Sep 17 00:00:00 2001 From: Androz2091 Date: Thu, 30 Apr 2026 22:08:59 -0700 Subject: [PATCH] fix(security): force TLS on the RDS connection Two-sided enforcement so there's no plaintext gap: - New aws_db_parameter_group with rds.force_ssl=1. The server refuses any non-TLS connection. Static parameter; takes effect on next Postgres reboot (RDS does it during the maintenance window or you can force it via the console). - Append ?sslmode=require to the POSTGRES_URL secret. SQLAlchemy / psycopg2 will refuse to negotiate plaintext on its end. Without sslmode=require, psycopg2 will accept plaintext if the server allows it; without rds.force_ssl, the server allows plaintext if the client asks for it. Both flips together close the loop. --- infra/terraform/rds.tf | 22 +++++++++++++++++++--- infra/terraform/secrets.tf | 5 ++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/infra/terraform/rds.tf b/infra/terraform/rds.tf index 5373006..148fe93 100644 --- a/infra/terraform/rds.tf +++ b/infra/terraform/rds.tf @@ -28,6 +28,21 @@ resource "aws_db_subnet_group" "main" { subnet_ids = aws_subnet.private[*].id } +# Custom parameter group so we can flip rds.force_ssl on. The default +# parameter group is read-only. +resource "aws_db_parameter_group" "main" { + name = "${local.name}-postgres17" + family = "postgres17" + + parameter { + name = "rds.force_ssl" + value = "1" + # Static parameter — requires a reboot to take effect. RDS handles + # the reboot during the next maintenance window or you can force it. + apply_method = "pending-reboot" + } +} + resource "aws_db_instance" "main" { identifier = "${local.name}-postgres" engine = "postgres" @@ -39,9 +54,10 @@ resource "aws_db_instance" "main" { storage_type = "gp3" storage_encrypted = true - db_name = var.postgres_db_name - username = var.postgres_username - password = random_password.postgres.result + db_name = var.postgres_db_name + username = var.postgres_username + password = random_password.postgres.result + parameter_group_name = aws_db_parameter_group.main.name db_subnet_group_name = aws_db_subnet_group.main.name vpc_security_group_ids = [aws_security_group.rds.id] diff --git a/infra/terraform/secrets.tf b/infra/terraform/secrets.tf index 51044ff..d32c591 100644 --- a/infra/terraform/secrets.tf +++ b/infra/terraform/secrets.tf @@ -25,8 +25,11 @@ resource "aws_secretsmanager_secret" "postgres_url" { resource "aws_secretsmanager_secret_version" "postgres_url" { secret_id = aws_secretsmanager_secret.postgres_url.id + # sslmode=require pairs with rds.force_ssl=1 in the parameter group: + # the client refuses plaintext, the server refuses plaintext, no + # gap between the two. secret_string = format( - "postgresql+psycopg2://%s:%s@%s:%d/%s", + "postgresql+psycopg2://%s:%s@%s:%d/%s?sslmode=require", var.postgres_username, random_password.postgres.result, aws_db_instance.main.address,