RDS DB instances should not use public subnet
Databases hold highly sensitive data. An RDS instance in a subnet with an internet gateway route is more exposed than it needs to be, and that exposure compounds if publicly_accessible is enabled later or security group rules drift.
Use private subnets in DB subnet groups to enforce stronger network boundaries. This is a foundational architecture call: moving an instance to a different subnet group later requires a reboot or instance recreation, so getting it right upfront matters.
Retrofit consideration
Moving an existing RDS instance to a different subnet group requires a reboot or, in many cases, recreation of the instance. Plan for downtime, or use a blue-green deployment with read replicas to migrate traffic before cutting over.
Implementation
Choose the approach that matches how you manage Terraform.
If you use terraform-aws-modules/rds/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = ">=7.0.0"
allocated_storage = 20
db_name = "myapp"
db_subnet_group_name = "example-db-subnet-group"
engine = "mysql"
engine_version = "8.0.41"
family = "mysql8.0"
identifier = "abc123"
instance_class = "db.t3.micro"
major_engine_version = "8.0"
password_wo = "change-me-in-production"
skip_final_snapshot = true
username = "dbadmin"
vpc_security_group_ids = ["sg-12345678"]
publicly_accessible = false
}
Use AWS provider resources directly. See docs for the resources involved: aws_db_instance.
resource "aws_db_instance" "this" {
allocated_storage = 20
enabled_cloudwatch_logs_exports = ["general", "slowquery"]
engine = "mysql"
identifier = "pofix-abc123"
instance_class = "db.t3.micro"
monitoring_interval = 60
monitoring_role_arn = "arn:aws:iam::123456789012:role/example-role"
password = "ChangeMe123!"
skip_final_snapshot = true
username = "dbadmin"
publicly_accessible = false
}
What this control checks
This control evaluates whether the subnets in an RDS instance's DB subnet group are public. In Terraform, an aws_db_instance references a db_subnet_group_name, which points to an aws_db_subnet_group listing subnet_ids. Each subnet has an associated route table, either attached explicitly via aws_route_table_association or inherited from the VPC's main route table. It fails if any route table entry for those subnets has a gateway_id pointing to an internet gateway (igw-*). To pass, every subnet in the DB subnet group must use route tables with only routes to NAT gateways, VPC endpoints, transit gateways, or local VPC CIDR. Setting publicly_accessible = false on aws_db_instance matters for full protection, but the subnet route table check is the primary evaluation target.
Common pitfalls
Default VPC subnets are public
If you don't specify
db_subnet_group_name, RDS falls back to the default DB subnet group, which usually includes public-routed subnets. Always create a dedicatedaws_db_subnet_grouppointing to private subnets rather than relying on the default.Subnet group covers multiple AZs with mixed visibility
Get this wrong and RDS may place your primary instance in a public subnet without any error. An
aws_db_subnet_groupspanning multiple AZs might include both public and private subnets, and RDS chooses which subnet to use. You can't control that choice. Verify everysubnet_idsentry in the group resolves to a private subnet.Publicly accessible false does not make the subnet private
publicly_accessible = falseprevents RDS from assigning a public endpoint, but the subnet's route table is unaffected. This control checks the route table, not the flag. Both need to be correct for meaningful network segmentation.Route table changes after initial deployment
A subnet that passes today can fail tomorrow if someone adds an
aws_routewithgateway_idpointing to anaws_internet_gateway. The Terraform plan won't catch that after the fact. Route table drift won't trigger any alarm unless Config or equivalent continuous monitoring is in place.
Audit evidence
Auditors expect evidence that RDS instances are not reachable from the internet at the network layer. Config rule results for each RDS instance are the most direct artifact. VPC Flow Logs showing no inbound traffic from public IP ranges to the RDS ENIs work as supporting evidence. Screenshots of the "Connectivity & security" tab in the RDS console, combined with the route tables for those subnets confirming no internet gateway routes, form a complete point-in-time package.
For ongoing assurance, Config conformance pack evaluation history or a CSPM tool's continuous scan results across the audit period carry more weight than screenshots alone.
Framework-specific interpretation
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
rds_db_instance_no_public_subnetAWS Config Managed Rule:
RDS_INSTANCE_SUBNET_IGW_CHECKPowerpipe Control:
aws_compliance.control.rds_db_instance_no_public_subnetProwler Check:
rds_instance_no_public_accessAWS Security Hub Control:
RDS.46KICS Query:
2f737336-b18a-4602-8ea0-b200312e1ac1
Last reviewed: 2026-03-09