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.
- Sign in to the AWS Management Console
- Navigate to RDS > Databases
- Select the RDS instance that needs to be moved to a VPC
- Note down the instance configuration (engine, size, storage, etc.) for later use
Create a DB Subnet Group (if you do not have one):
- In the RDS console, click Subnet groups in the left navigation
- Click Create DB subnet group
- Enter a name and description
- Select your VPC from the dropdown
- Under Add subnets, select at least two subnets in different Availability Zones
- Click Create
Migrate the instance to VPC:
- Go back to Databases and select your instance
- Click Actions > Take snapshot
- Give the snapshot a name and click Take snapshot
- Wait for the snapshot to complete
- Go to Snapshots, select your snapshot, and click Actions > Restore snapshot
- 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
- Click Restore DB instance
- Update your application to use the new instance endpoint
- 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:
- In the AWS Console, go to RDS > Databases
- Select your database instance
- 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
- Amazon RDS VPCs and Subnets
- Working with a DB Instance in a VPC
- Amazon VPC Documentation
- RDS Security Best Practices
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.