Skip to main content

Check if RDS Instances Are Deployed Within a VPC

Overview

This check verifies that your Amazon RDS database instances are deployed within a Virtual Private Cloud (VPC). A VPC provides network isolation, allowing you to control who can access your database and how traffic flows to and from it.

Risk

When an RDS instance is not deployed within a VPC, it is exposed to the public internet without network-level controls. This creates several security risks:

  • Unauthorized access: The database endpoint is reachable from anywhere on the internet
  • Data exposure: Attackers can attempt brute-force attacks or exploit vulnerabilities
  • Compliance violations: Many regulatory frameworks require network isolation for databases

Deploying RDS instances in a VPC allows you to use security groups and network ACLs to restrict access to only authorized sources.

Remediation Steps

Prerequisites

  • Access to the AWS Console with permissions to modify RDS instances
  • An existing VPC with at least two subnets in different Availability Zones
  • A DB subnet group (or permission to create one)
Identify your VPC and subnets

Before proceeding, you need to identify which VPC and subnets to use. Ideally, use private subnets (subnets without a route to an internet gateway) for your database.

To list your VPCs:

aws ec2 describe-vpcs --region us-east-1 \
--query "Vpcs[*].{VpcId:VpcId,Name:Tags[?Key=='Name']|[0].Value,CidrBlock:CidrBlock}" \
--output table

To list subnets in a VPC:

aws ec2 describe-subnets --region us-east-1 \
--filters "Name=vpc-id,Values=<your-vpc-id>" \
--query "Subnets[*].{SubnetId:SubnetId,AZ:AvailabilityZone,Name:Tags[?Key=='Name']|[0].Value}" \
--output table

AWS Console Method

Important: Moving an RDS instance to a VPC requires creating a snapshot and restoring it to a new instance. This process involves downtime and you should plan accordingly.

  1. Sign in to the AWS Management Console
  2. Navigate to RDS > Databases
  3. Select the RDS instance that needs to be moved to a VPC
  4. Note down the instance configuration (engine, size, storage, etc.) for later use

Create a DB Subnet Group (if you do not have one):

  1. In the RDS console, click Subnet groups in the left navigation
  2. Click Create DB subnet group
  3. Enter a name and description
  4. Select your VPC from the dropdown
  5. Under Add subnets, select at least two subnets in different Availability Zones
  6. Click Create

Migrate the instance to VPC:

  1. Go back to Databases and select your instance
  2. Click Actions > Take snapshot
  3. Give the snapshot a name and click Take snapshot
  4. Wait for the snapshot to complete
  5. Go to Snapshots, select your snapshot, and click Actions > Restore snapshot
  6. In the restore configuration:
    • Under Connectivity, select your VPC
    • Select the DB subnet group you created
    • Set Public access to No
    • Select or create a security group
  7. Click Restore DB instance
  8. Update your application to use the new instance endpoint
  9. Once verified, you can delete the old non-VPC instance
AWS CLI (optional)

Step 1: Create a DB subnet group

aws rds create-db-subnet-group \
--db-subnet-group-name my-db-subnet-group \
--db-subnet-group-description "Subnet group for RDS instances" \
--subnet-ids subnet-xxxxxxxx subnet-yyyyyyyy \
--region us-east-1

Step 2: Create a snapshot of the existing instance

aws rds create-db-snapshot \
--db-instance-identifier <your-db-instance-id> \
--db-snapshot-identifier <your-db-instance-id>-snapshot \
--region us-east-1

Step 3: Wait for the snapshot to complete

aws rds wait db-snapshot-available \
--db-snapshot-identifier <your-db-instance-id>-snapshot \
--region us-east-1

Step 4: Restore the snapshot to a new VPC-enabled instance

aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier <new-db-instance-id> \
--db-snapshot-identifier <your-db-instance-id>-snapshot \
--db-subnet-group-name my-db-subnet-group \
--no-publicly-accessible \
--vpc-security-group-ids sg-xxxxxxxx \
--region us-east-1

Step 5: Wait for the new instance to become available

aws rds wait db-instance-available \
--db-instance-identifier <new-db-instance-id> \
--region us-east-1

Step 6: Get the new endpoint

aws rds describe-db-instances \
--db-instance-identifier <new-db-instance-id> \
--query "DBInstances[0].Endpoint" \
--region us-east-1
CloudFormation (optional)

Use this template to deploy a new RDS instance within a VPC. This template includes a security group and DB subnet group.

AWSTemplateFormatVersion: '2010-09-09'
Description: RDS instance deployed within a VPC

Parameters:
VPCId:
Type: AWS::EC2::VPC::Id
Description: VPC where the RDS instance will be deployed

PrivateSubnet1:
Type: AWS::EC2::Subnet::Id
Description: First private subnet for the DB subnet group

PrivateSubnet2:
Type: AWS::EC2::Subnet::Id
Description: Second private subnet for the DB subnet group

DBInstanceIdentifier:
Type: String
Description: Identifier for the RDS instance
Default: my-rds-instance

DBInstanceClass:
Type: String
Description: Instance class for the RDS instance
Default: db.t3.micro

DBEngine:
Type: String
Description: Database engine
Default: mysql
AllowedValues:
- mysql
- postgres
- mariadb

DBMasterUsername:
Type: String
Description: Master username for the database

Resources:
RDSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for RDS instance
VpcId: !Ref VPCId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
CidrIp: 10.0.0.0/8
Description: Allow database access from internal network
Tags:
- Key: Name
Value: !Sub ${DBInstanceIdentifier}-sg

DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS instance
DBSubnetGroupName: !Sub ${DBInstanceIdentifier}-subnet-group
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Tags:
- Key: Name
Value: !Sub ${DBInstanceIdentifier}-subnet-group

RDSInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Ref DBInstanceIdentifier
DBInstanceClass: !Ref DBInstanceClass
Engine: !Ref DBEngine
MasterUsername: !Ref DBMasterUsername
ManageMasterUserPassword: true
AllocatedStorage: '20'
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !GetAtt RDSSecurityGroup.GroupId
PubliclyAccessible: false
StorageEncrypted: true
DeletionProtection: true
Tags:
- Key: Name
Value: !Ref DBInstanceIdentifier

Outputs:
DBInstanceEndpoint:
Description: Endpoint of the RDS instance
Value: !GetAtt RDSInstance.Endpoint.Address

DBInstancePort:
Description: Port of the RDS instance
Value: !GetAtt RDSInstance.Endpoint.Port

SecurityGroupId:
Description: Security group ID for the RDS instance
Value: !Ref RDSSecurityGroup

Deploy the stack:

aws cloudformation create-stack \
--stack-name rds-vpc-stack \
--template-body file://template.yaml \
--parameters \
ParameterKey=VPCId,ParameterValue=vpc-xxxxxxxx \
ParameterKey=PrivateSubnet1,ParameterValue=subnet-xxxxxxxx \
ParameterKey=PrivateSubnet2,ParameterValue=subnet-yyyyyyyy \
ParameterKey=DBMasterUsername,ParameterValue=admin \
--region us-east-1
Terraform (optional)
# RDS Instance Deployed Within a VPC
# This configuration ensures your RDS instance is properly isolated within a VPC

variable "vpc_id" {
description = "VPC ID where the RDS instance will be deployed"
type = string
}

variable "private_subnet_ids" {
description = "List of private subnet IDs for the DB subnet group"
type = list(string)
}

variable "db_instance_identifier" {
description = "Identifier for the RDS instance"
type = string
default = "my-rds-instance"
}

variable "db_instance_class" {
description = "Instance class for the RDS instance"
type = string
default = "db.t3.micro"
}

variable "db_engine" {
description = "Database engine"
type = string
default = "mysql"
}

variable "db_username" {
description = "Master username for the database"
type = string
sensitive = true
}

variable "db_password" {
description = "Master password for the database"
type = string
sensitive = true
}

# Security group for the RDS instance
resource "aws_security_group" "rds" {
name = "${var.db_instance_identifier}-sg"
description = "Security group for RDS instance"
vpc_id = var.vpc_id

ingress {
description = "Allow database access from internal network"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}

tags = {
Name = "${var.db_instance_identifier}-sg"
}
}

# DB subnet group - required for VPC deployment
resource "aws_db_subnet_group" "main" {
name = "${var.db_instance_identifier}-subnet-group"
description = "Subnet group for RDS instance"
subnet_ids = var.private_subnet_ids

tags = {
Name = "${var.db_instance_identifier}-subnet-group"
}
}

# RDS instance deployed within the VPC
resource "aws_db_instance" "main" {
identifier = var.db_instance_identifier
instance_class = var.db_instance_class
engine = var.db_engine
allocated_storage = 20
username = var.db_username
password = var.db_password

# VPC configuration - this is what places the instance in a VPC
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]

# Security best practices
publicly_accessible = false
storage_encrypted = true
deletion_protection = true

tags = {
Name = var.db_instance_identifier
}
}

output "db_instance_endpoint" {
description = "Endpoint of the RDS instance"
value = aws_db_instance.main.endpoint
}

output "db_instance_port" {
description = "Port of the RDS instance"
value = aws_db_instance.main.port
}

output "security_group_id" {
description = "Security group ID for the RDS instance"
value = aws_security_group.rds.id
}

Apply the configuration:

terraform init
terraform plan -var="vpc_id=vpc-xxxxxxxx" \
-var='private_subnet_ids=["subnet-xxxxxxxx","subnet-yyyyyyyy"]' \
-var="db_username=admin" \
-var="db_password=your-secure-password"
terraform apply

Verification

After completing the remediation, verify that your RDS instance is now deployed within a VPC:

  1. In the AWS Console, go to RDS > Databases
  2. Select your database instance
  3. In the Connectivity & security tab, confirm that:
    • A VPC is listed (not "Not in VPC")
    • A Subnet group is assigned
    • Publicly accessible shows No
Verify using AWS CLI
aws rds describe-db-instances \
--db-instance-identifier <your-db-instance-id> \
--query "DBInstances[0].{VPC:DBSubnetGroup.VpcId,SubnetGroup:DBSubnetGroup.DBSubnetGroupName,PubliclyAccessible:PubliclyAccessible}" \
--region us-east-1

The output should show a VPC ID and subnet group name, with PubliclyAccessible set to false.

Verify using Prowler

Run the Prowler check to confirm the issue is resolved:

prowler aws --checks rds_instance_inside_vpc

Additional Resources

Notes

  • Downtime required: Migrating an existing non-VPC RDS instance to a VPC requires creating a snapshot and restoring to a new instance. Plan for application downtime and update connection strings.
  • Endpoint changes: The new VPC-enabled instance will have a different endpoint. Update all applications and services that connect to the database.
  • Security groups: Once in a VPC, the instance uses VPC security groups instead of DB security groups. Configure appropriate inbound rules.
  • Private subnets recommended: For maximum security, deploy RDS instances in private subnets (subnets without a route to an internet gateway).
  • Multi-AZ consideration: When creating a DB subnet group, include subnets in multiple Availability Zones to support Multi-AZ deployments.