RDS Instance Not Publicly Accessible
Overview
This check verifies that your Amazon RDS database instances are not exposed to the public internet. It examines three factors:
- The
PubliclyAccessiblesetting on the instance - Security group rules that allow access from any IP address (
0.0.0.0/0or::/0) - Whether the instance is in a public subnet
Instances that combine all three conditions are flagged as publicly exposed.
Risk
A publicly accessible database is a critical security risk. When your database is reachable from the internet:
- Brute-force attacks: Attackers can attempt to guess credentials continuously
- Vulnerability scanning: Your database becomes a target for automated vulnerability probes
- Data theft: Unauthorized access can lead to data exfiltration
- Lateral movement: A compromised database can be used to attack other resources in your VPC
Databases should only be accessible from trusted sources within your private network.
Remediation Steps
Prerequisites
- AWS account access with permissions to modify RDS instances
- The database instance identifier you need to remediate
Required IAM permissions
You need the following IAM permissions:
rds:ModifyDBInstancerds:DescribeDBInstancesec2:DescribeSecurityGroups(to review security group rules)
AWS Console Method
- Sign in to the AWS RDS Console
- In the left navigation, click Databases
- Select the database instance you want to modify
- Click the Modify button
- Scroll to the Connectivity section
- Under Public access, select No
- Scroll to the bottom and click Continue
- Choose when to apply the change:
- Apply immediately - Changes take effect right away (may cause brief connectivity interruption)
- Apply during the next scheduled maintenance window - Delays the change
- Click Modify DB Instance
Important: After disabling public access, ensure your applications can still reach the database through private connectivity (VPC, VPN, or AWS PrivateLink).
AWS CLI (optional)
Disable public access on an existing RDS instance:
aws rds modify-db-instance \
--db-instance-identifier <your-db-instance-id> \
--no-publicly-accessible \
--apply-immediately \
--region us-east-1
Replace <your-db-instance-id> with your actual database instance identifier.
To apply during the next maintenance window instead:
aws rds modify-db-instance \
--db-instance-identifier <your-db-instance-id> \
--no-publicly-accessible \
--no-apply-immediately \
--region us-east-1
Check the current status of all RDS instances:
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,PubliclyAccessible]' \
--output table \
--region us-east-1
CloudFormation (optional)
Use this template to create an RDS instance with public access disabled:
AWSTemplateFormatVersion: '2010-09-09'
Description: RDS instance with public access disabled
Parameters:
DBInstanceIdentifier:
Type: String
Description: Identifier for the RDS instance
DBInstanceClass:
Type: String
Default: db.t3.micro
Description: Instance class for the RDS instance
Engine:
Type: String
Default: mysql
AllowedValues:
- mysql
- postgres
- mariadb
Description: Database engine
MasterUsername:
Type: String
Description: Master username for the database
DBSubnetGroupName:
Type: String
Description: Name of an existing DB subnet group in private subnets
VPCSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
Description: Security group ID for the RDS instance
Resources:
RDSInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Ref DBInstanceIdentifier
DBInstanceClass: !Ref DBInstanceClass
Engine: !Ref Engine
MasterUsername: !Ref MasterUsername
ManageMasterUserPassword: true
AllocatedStorage: '20'
PubliclyAccessible: false
DBSubnetGroupName: !Ref DBSubnetGroupName
VPCSecurityGroups:
- !Ref VPCSecurityGroupId
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
Key configuration:
PubliclyAccessible: false- Prevents the instance from being assigned a public IPDBSubnetGroupName- Must reference a subnet group containing only private subnetsManageMasterUserPassword: true- Uses AWS Secrets Manager for password management (recommended)
Deploy the stack:
aws cloudformation create-stack \
--stack-name my-private-rds \
--template-body file://template.yaml \
--parameters \
ParameterKey=DBInstanceIdentifier,ParameterValue=my-database \
ParameterKey=MasterUsername,ParameterValue=admin \
ParameterKey=DBSubnetGroupName,ParameterValue=my-private-subnet-group \
ParameterKey=VPCSecurityGroupId,ParameterValue=sg-0123456789abcdef0 \
--region us-east-1
Terraform (optional)
Use this configuration to create an RDS instance with public access disabled:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
variable "db_instance_identifier" {
description = "Identifier for the RDS instance"
type = string
}
variable "db_instance_class" {
description = "Instance class for the RDS instance"
type = string
default = "db.t3.micro"
}
variable "engine" {
description = "Database engine"
type = string
default = "mysql"
}
variable "engine_version" {
description = "Database engine version"
type = string
default = "8.0"
}
variable "allocated_storage" {
description = "Allocated storage in GB"
type = number
default = 20
}
variable "db_name" {
description = "Name of the database to create"
type = string
}
variable "username" {
description = "Master username"
type = string
}
variable "password" {
description = "Master password"
type = string
sensitive = true
}
variable "db_subnet_group_name" {
description = "Name of the DB subnet group (must be in private subnets)"
type = string
}
variable "vpc_security_group_ids" {
description = "List of VPC security group IDs"
type = list(string)
}
resource "aws_db_instance" "main" {
identifier = var.db_instance_identifier
instance_class = var.db_instance_class
engine = var.engine
engine_version = var.engine_version
allocated_storage = var.allocated_storage
db_name = var.db_name
username = var.username
password = var.password
# Security settings - disable public access
publicly_accessible = false
db_subnet_group_name = var.db_subnet_group_name
vpc_security_group_ids = var.vpc_security_group_ids
# Additional security best practices
storage_encrypted = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.db_instance_identifier}-final-snapshot"
tags = {
Name = var.db_instance_identifier
}
}
output "db_instance_endpoint" {
description = "The connection endpoint for the RDS instance"
value = aws_db_instance.main.endpoint
}
output "db_instance_address" {
description = "The hostname of the RDS instance"
value = aws_db_instance.main.address
}
output "db_instance_port" {
description = "The port the RDS instance is listening on"
value = aws_db_instance.main.port
}
Key configuration:
publicly_accessible = false- Prevents the instance from being assigned a public IPdb_subnet_group_name- Must reference a subnet group containing only private subnets
Apply the configuration:
terraform init
terraform plan -var="db_instance_identifier=my-database" \
-var="db_name=mydb" \
-var="username=admin" \
-var="password=your-secure-password" \
-var="db_subnet_group_name=my-private-subnet-group" \
-var='vpc_security_group_ids=["sg-0123456789abcdef0"]'
terraform apply
Note: For production, use AWS Secrets Manager or Terraform variables files to manage the password securely instead of command-line arguments.
Verification
After making the change, verify that public access is disabled:
- In the RDS Console, select your database instance
- On the Connectivity & security tab, check that Publicly accessible shows No
CLI verification
aws rds describe-db-instances \
--db-instance-identifier <your-db-instance-id> \
--query 'DBInstances[0].PubliclyAccessible' \
--region us-east-1
The output should be false.
To list all publicly accessible instances in your account:
aws rds describe-db-instances \
--query 'DBInstances[?PubliclyAccessible==`true`].[DBInstanceIdentifier,Endpoint.Address]' \
--output table \
--region us-east-1
An empty table means no instances are publicly accessible.
Additional Resources
- AWS Documentation: Hiding a DB instance in a VPC from the internet
- AWS Documentation: Scenarios for accessing a DB instance in a VPC
- AWS Documentation: Controlling access with security groups
- AWS Well-Architected Framework: Security Pillar
Notes
-
Application connectivity: Before disabling public access, ensure your applications can connect to the database through private means (same VPC, VPC peering, AWS PrivateLink, or VPN).
-
Maintenance window: If you choose not to apply the change immediately, it will be applied during the next scheduled maintenance window. Check your maintenance window settings to know when this will occur.
-
Security groups: Even with
PubliclyAccessibleset tofalse, review your security groups to ensure they only allow traffic from trusted sources. Remove any rules that allow access from0.0.0.0/0or::/0. -
Subnet configuration: For defense in depth, place your RDS instances in private subnets (subnets without a route to an internet gateway). This provides an additional layer of protection.
-
Multi-AZ deployments: This setting applies to both primary and standby instances in Multi-AZ deployments.