Skip to content

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 dedicated aws_db_subnet_group pointing 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_group spanning multiple AZs might include both public and private subnets, and RDS chooses which subnet to use. You can't control that choice. Verify every subnet_ids entry in the group resolves to a private subnet.

  • Publicly accessible false does not make the subnet private

    publicly_accessible = false prevents 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_route with gateway_id pointing to an aws_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_subnet

  • AWS Config Managed Rule: RDS_INSTANCE_SUBNET_IGW_CHECK

  • Powerpipe Control: aws_compliance.control.rds_db_instance_no_public_subnet

  • Prowler Check: rds_instance_no_public_access

  • AWS Security Hub Control: RDS.46

  • KICS Query: 2f737336-b18a-4602-8ea0-b200312e1ac1

Last reviewed: 2026-03-09