I’ve walked into more than one “secure” startup only to find their crown jewels—the production database—exposed to the world with a publicly_accessible = true
flag. The engineers usually give me the same line: “Don’t worry, the security group is locked down to our office IP.” That’s not security; it’s a landmine waiting for one compromised developer laptop or one slight firewall misconfiguration to blow the whole company sky-high.
Most teams treat their database like just another resource. They spin it up, get the connection string, and move on. This is a catastrophic mistake. Your database isn’t just another server; it’s the vault. A breach there doesn’t just cause downtime; it erodes customer trust, triggers compliance nightmares (hello, SOC 2 auditors), and can be a company-ending event.
The default settings in AWS are designed for ease of use, not for bulletproof security. Today, I’m giving you my blueprint for locking down an AWS RDS instance using Terraform. This isn’t a theoretical exercise; this is the battle-hardened configuration I deploy to survive production and pass stringent audits.
Architecture Context: Defense in Depth
We’re not relying on a single password or a firewall rule. We’re building layers of security around the database, assuming that any single layer could fail. Our strategic approach is clear: make the database invisible to the public internet, encrypt everything, and eliminate static, long-lived credentials wherever possible.
Here’s the high-level architecture we’re building:
- Network Isolation: The RDS instance will live in dedicated private subnets. It will have no direct route to or from the public internet.
- Firewalling: A dedicated Security Group will act as a bouncer, only allowing traffic from our application’s Security Group on the specific database port. Nothing else gets in.
- Encryption at Rest: We’ll use a customer-managed AWS Key Management Service (KMS) key to encrypt the database storage. This gives us full control over the data encryption keys, a critical requirement for compliance frameworks like SOC 2 and ISO 27001.
- Encryption in Transit: We will enforce SSL/TLS for all connections to the database.
- Credential Management: We will enable IAM Database Authentication, a massive security upgrade that replaces static database passwords with temporary, IAM-controlled credentials.
Implementation Details: The Terraform Blueprint
Beyond theory, here’s the practical implementation. This is my baseline main.tf
for a secure PostgreSQL RDS instance.
Step 1: The Network Foundation (Subnets & Security Groups)
First, we define a subnet group to tell RDS which private subnets it can live in. Then, we create its dedicated security group. Notice the ingress rule: it only allows traffic from the var.app_security_group_id
on the PostgreSQL port.
# Assumes you have a VPC with private subnets already defined.
# A dedicated subnet group tells RDS where it can place instances.
resource "aws_db_subnet_group" "default" {
name = "my-app-db-subnet-group"
subnet_ids = var.private_subnet_ids # e.g., ["subnet-0123...", "subnet-4567..."]
tags = {
Name = "My App DB Subnet Group"
}
}
# The bouncer for our database.
resource "aws_security_group" "db" {
name = "my-app-db-sg"
description = "Allow traffic from the application layer"
vpc_id = var.vpc_id
# Ingress from the App Security Group ONLY
ingress {
description = "PostgreSQL from App"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.app_security_group_id] # Pass in your app's SG ID
}
# Egress is typically left open within a VPC, but can be locked down further.
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "My App DB Security Group"
}
}
Step 2: Encryption Key (Customer-Managed KMS)
Don’t settle for the default AWS-managed key. For any serious production workload, especially one needing compliance, you need your own key. This gives you the power to control its rotation policy and, if worst comes to worst, disable it to render the data unreadable.
# A dedicated KMS key for encrypting the RDS instance storage.
resource "aws_kms_key" "db" {
description = "KMS key for RDS encryption"
deletion_window_in_days = 10
enable_key_rotation = true
tags = {
Name = "My App RDS Key"
}
}
Step 3: The RDS Instance Resource
Now we put it all together. This aws_db_instance
resource is packed with security-first configurations. I’ve added comments to highlight the critical lines.
resource "aws_db_instance" "default" {
identifier = "my-app-db-prod"
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
# --- CRITICAL SECURITY CONFIGURATION ---
# 1. Network Security: Place it in the right network cage.
db_subnet_group_name = aws_db_subnet_group.default.name
vpc_security_group_ids = [aws_security_group.db.id]
publicly_accessible = false # NEVER set this to true in production.
# 2. Encryption at Rest: Use our dedicated KMS key.
storage_encrypted = true
kms_key_id = aws_kms_key.db.arn
# 3. Credential Security: Ditch static passwords for IAM.
iam_database_authentication_enabled = true
# 4. Data Protection & Availability
backup_retention_period = 14 # Tune based on RPO
multi_az = true # For high availability
delete_automated_backups = false # Don't lose backups on deletion
deletion_protection = true # Prevent accidental deletion
# --- Secrets Management ---
# DO NOT hardcode the master password. Use a secrets manager.
# This example uses aws_secretsmanager_secret to generate and store it.
username = "masteruser"
password = aws_secretsmanager_secret_version.db_master_password.secret_string
# --- Logging & Maintenance ---
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
auto_minor_version_upgrade = true
skip_final_snapshot = false
lifecycle {
# Ignore password changes outside of Terraform (e.g., rotation in Secrets Manager)
ignore_changes = [password]
}
tags = {
Name = "My App Production DB"
}
}
# Example of generating the password with Secrets Manager
resource "aws_secretsmanager_secret" "db_master_password" {
name = "prod/my-app/db_master_password"
}
resource "aws_secretsmanager_secret_version" "db_master_password" {
secret_id = aws_secretsmanager_secret.db_master_password.id
secret_string = random_password.master_password.result
}
resource "random_password" "master_password" {
length = 20
special = true
}
Architect’s Note
Enabling iam_database_authentication_enabled
is the single most impactful security feature you can turn on for RDS. It completely changes the game. Instead of sharing a database password (db_user
/db_pass
), your engineers and applications authenticate using temporary tokens generated via their IAM roles. You grant database access with an IAM policy, not a GRANT
statement inside the database. This means access is temporary, centrally auditable via CloudTrail, and automatically revoked when an employee leaves. For any company pursuing SOC 2, this is a non-negotiable feature that auditors love to see. It moves access control from a brittle, shared secret to a robust, managed identity framework.
Pitfalls & Optimisations
Building it is one thing; running it in the real world is another. Here are the traps I’ve seen teams fall into:
- Critical Gap: Encryption in Transit: Encryption at rest is great, but what about data on the wire? Forcing SSL is done in the database’s Parameter Group. You must create a custom
aws_db_parameter_group
, set the parameterrds.force_ssl
to1
, and attach it to your instance. Never use the default parameter group. - Leaking the Master Password in Terraform State: The
password
argument in theaws_db_instance
resource will be stored in plain text in your Terraform state file. The code above mitigates this by using AWS Secrets Manager to generate and store the password. The application should then be given IAM permissions to read that secret at runtime. - Snapshot Security: If your database is encrypted with a customer-managed KMS key, its snapshots are automatically encrypted with the same key. This prevents a snapshot from being accidentally restored into a public, unencrypted database, closing a common exfiltration vector.
- IAM Policy Granularity: When using IAM authentication, don’t give everyone admin access. Create specific IAM policies that grant the
rds-db:connect
action to specific database users. You can map IAM roles directly to database roles for fine-grained control.
Unlocked: Your Key Takeaways
This blueprint isn’t about gold-plating your infrastructure. It’s about building a foundation that won’t crumble under the pressure of an audit or an attack.
- Isolate and Constrain: Your production database should never have a public IP. Isolate it in private subnets and use Security Groups to enforce a strict “allow list” for connections.
- Encrypt Everything: Use customer-managed KMS keys for encryption at rest and enforce SSL/TLS for encryption in transit. This is table stakes for compliance.
- Eliminate Static Passwords: Use IAM Database Authentication to move away from shared secrets. Tie database access to auditable, temporary IAM credentials.
- Automate with IaC: Use Terraform to codify this secure configuration. This makes it repeatable, reviewable, and prevents manual configuration drift that introduces vulnerabilities.
A secure database isn’t a feature; it’s the bedrock of a trustworthy product. Don’t let a ticking time bomb sit at the heart of your architecture.
Connect and Collaborate
If your team is facing this challenge, I specialize in architecting these secure, audit-ready systems.
Email me for a strategic consultation: [email protected]
Explore my projects and connect on Upwork: https://www.upwork.com/freelancers/~0118e01df30d0a918e