An RDS publicly accessible finding (Security Hub control RDS.2) means a DB instance has PubliclyAccessible set to true, giving it a public IP and an endpoint that resolves to it from the internet. Fix it with aws rds modify-db-instance –no-publicly-accessible and restrict its security group to application tiers. The database stays up, but clients outside the VPC lose access.
The flag itself is a one-line change. The work is finding out who still connects from outside the VPC, giving them a private path first, and making sure templates and new instances do not bring the finding back.
What the scanner is actually detecting
Security Hub does not connect to the database. It reads the recorded configuration of the instance through AWS Config.
| Control | Finding title | What it evaluates |
|---|---|---|
| AWS Security Hub CSPM, RDS.2 | [RDS.2] RDS DB Instances should prohibit public access, as determined by the PubliclyAccessible configuration | Each AWS::RDS::DBInstance through the AWS Config rule rds-instance-public-access-check. Fails when PubliclyAccessible is true in the configuration item. Severity Critical, change triggered, no parameters. |
| AWS Security Hub CSPM, RDS.46 (related) | [RDS.46] RDS DB instances should not be deployed in public subnets with routes to internet gateways | Fails when the instance sits in a subnet with a route to an internet gateway for 0.0.0.0/0 or ::/0. Severity High, periodic. |
RDS.2 checks one boolean. It does not look at security group rules, route tables, or whether anyone can actually open a connection. Aurora instances are DB instances too, and Aurora sets public access per instance, so a cluster produces one result per writer and reader.
Real-world risk, stated honestly
When the flag is true, the endpoint resolves to a public IP from outside the VPC and to the private IP from inside it. Whether the internet can reach the listener depends on two more things: the DB subnet group must contain public subnets, and the security group must allow the source. AWS states that access is ultimately controlled by the security group.
If the security group allows 0.0.0.0/0 on the database port, the login prompt of a production database is on the internet. That exposes it to password guessing, reuse of leaked credentials, and any pre-authentication flaw in the engine. If the group only allows a few office IPs, the practical risk is much lower, but the database is one careless rule away from full exposure. Weigh it against other cloud findings using exposure-based prioritization rather than the Critical label alone.
How to confirm it
List publicly accessible instances in a Region:
aws rds describe-db-instances --region us-east-1
--query 'DBInstances[?PubliclyAccessible==`true`].[DBInstanceIdentifier,Engine,Endpoint.Address,Endpoint.Port]'
--output table
Check what the security group actually allows (look for 0.0.0.0/0 and ::/0):
aws rds describe-db-instances --db-instance-identifier mydb
--query 'DBInstances[0].VpcSecurityGroups[].VpcSecurityGroupId'
aws ec2 describe-security-groups --group-ids sg-0db1111111111111a
--query 'SecurityGroups[].IpPermissions[].[FromPort,IpRanges[].CidrIp,Ipv6Ranges[].CidrIpv6,UserIdGroupPairs[].GroupId]'
From a machine outside the VPC, a public answer confirms the exposure:
dig +short mydb.abcdefghijkl.us-east-1.rds.amazonaws.com
Before changing anything, find out who connects from outside. Current sessions show client addresses on PostgreSQL and MySQL; VPC Flow Logs on the DB subnets give a longer history.
-- PostgreSQL
SELECT client_addr, usename, application_name FROM pg_stat_activity;
-- MySQL / MariaDB
SELECT user, host FROM information_schema.processlist;
Any public source address is a client that will break.
How to remove public access
Step 1: give external clients a private path
Typical options are a VPN or Direct Connect for on-premises systems, VPC attachment for Lambda functions and CI runners, and Session Manager port forwarding for administrators. Port forwarding to a remote host needs SSM Agent 3.1.1374.0 or later on the EC2 node and the Session Manager plugin locally:
aws ssm start-session
--target i-0123456789abcdef0
--document-name AWS-StartPortForwardingSessionToRemoteHost
--parameters '{"host":["mydb.abcdefghijkl.us-east-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
The client then connects to localhost:5432. A bastion host works the same way over SSH.
Step 2: turn off public access
AWS documents that a PubliclyAccessible change occurs immediately, ignores the apply immediately setting, and causes no downtime. Be careful with –apply-immediately: it also applies any pending modifications, such as a queued instance class change, which can restart the database. Check first:
aws rds describe-db-instances --db-instance-identifier mydb
--query 'DBInstances[0].PendingModifiedValues'
aws rds modify-db-instance
--db-instance-identifier mydb
--no-publicly-accessible
--apply-immediately
If anything is pending, drop –apply-immediately; the public access change still happens right away. With AWS Tools for PowerShell:
Edit-RDSDBInstance -DBInstanceIdentifier mydb -PubliclyAccessible $false
In the console: Databases, select the instance, Modify, then under Connectivity open Additional connectivity configuration and choose Not publicly accessible.
For Aurora, repeat for every instance in the cluster:
for id in $(aws rds describe-db-clusters --db-cluster-identifier mycluster
--query 'DBClusters[0].DBClusterMembers[].DBInstanceIdentifier' --output text); do
aws rds modify-db-instance --db-instance-identifier "$id" --no-publicly-accessible
done
Step 3: restrict the security group
Allow the application security group first, then remove the open rule, so the application never loses its path:
aws ec2 authorize-security-group-ingress --group-id sg-0db1111111111111a
--protocol tcp --port 5432 --source-group sg-0app222222222222b
aws ec2 revoke-security-group-ingress --group-id sg-0db1111111111111a
--protocol tcp --port 5432 --cidr 0.0.0.0/0
aws ec2 revoke-security-group-ingress --group-id sg-0db1111111111111a
--ip-permissions 'IpProtocol=tcp,FromPort=5432,ToPort=5432,Ipv6Ranges=[{CidrIpv6=::/0}]'
Step 4: keep new instances private
Set the flag explicitly everywhere. Per the CreateDBInstance API reference, if no DB subnet group is given, it defaults to true for non-Aurora instances, and it also defaults to true when the subnet group is named default.
- CLI: add –no-publicly-accessible and a –db-subnet-group-name that contains only private subnets.
- CloudFormation: PubliclyAccessible: false on AWS::RDS::DBInstance (update requires no interruption).
- Terraform: publicly_accessible = false on aws_db_instance (the provider default is false, but stating it prevents drift).
RDS.2 passes once the flag is false, but an instance in public subnets still fails RDS.46. AWS documents subnet group changes on an existing instance as a move to a different VPC, with downtime, so treat that as a planned migration, for example restoring a snapshot into a private subnet group with –no-publicly-accessible and cutting over.
How to verify the fix and rescan
- aws rds describe-db-instances –db-instance-identifier mydb –query ‘DBInstances[0].PubliclyAccessible’ returns false.
- From outside the VPC, dig +short on the endpoint now returns a private address (10.x, 172.16 to 172.31, or 192.168.x).
- The application still connects, and a connection attempt from outside times out.
- RDS.2 is change triggered. Once AWS Config records the new configuration, the control shows PASSED and Security Hub sets the workflow status to RESOLVED. To list what still fails:
aws securityhub get-findings --region us-east-1 --filters '{ "ComplianceSecurityControlId":[{"Value":"RDS.2","Comparison":"EQUALS"}], "ComplianceStatus":[{"Value":"FAILED","Comparison":"EQUALS"}], "RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}]}' --query 'Findings[].Resources[].Id' --output text
What can break and how to roll back
- Anything outside the VPC: developer laptops, SaaS BI or ETL tools, hosted CI runners running migrations, Lambda functions not attached to the VPC, and third-party monitoring.
- Peered VPCs and on-premises networks that used the public IP now resolve the private one, so they need routes and security group rules for the private path.
- Hardcoded IP addresses. AWS recommends connecting by DNS name because the underlying address changes; clients with a stored IP or a long DNS cache fail.
To roll back, re-enable the flag. It only works if the subnet group is still public and the VPC has DNS hostnames and DNS resolution turned on:
aws rds modify-db-instance --db-instance-identifier mydb --publicly-accessible
If you roll back, allow only specific source addresses in the security group, never 0.0.0.0/0, and restore any rule you revoked with authorize-security-group-ingress.
Common false positive reasons
- Flag on, subnets private. The instance has no route to an internet gateway, so it is not reachable, but RDS.2 still fails. Turn the flag off anyway; it costs nothing.
- Flag on, security group closed. Nothing outside the VPC is allowed in. Real exposure is low, but the control cannot see that.
- Intentionally public databases such as a vendor-accessed read replica. Suppress with a written justification and a narrow security group.
- Aurora readers. Fixing the writer leaves separate findings for each reader.
- Stale results. AWS Config is not recording RDS DB instances in that Region, so the control never re-evaluates.
FAQ
Does disabling public access restart the database?
No. AWS lists the change as immediate with no downtime. Only –apply-immediately combined with other pending changes can cause a restart.
Is a restrictive security group enough?
It controls access, but RDS.2 checks only the flag and keeps failing. Turning the flag off removes the public IP, so a later security group mistake cannot expose the database to the internet.
How do administrators connect afterwards?
Through a VPN or Direct Connect, a bastion host, or Session Manager port forwarding from an EC2 instance in the VPC.
Does RDS.2 cover snapshots?
No. Public snapshots are control RDS.1, and public subnets are RDS.46.
Tracking this finding across many accounts
Across many accounts, the durable fix is explicit PubliclyAccessible: false in every template, with the Security Hub control status as your evidence. If you use SITEY, a self-hosted vulnerability management platform that imports findings from 16 scanners, first confirm your cloud posture source is among them. Duplicates are merged per scanner rather than across scanners, and its AI triage suggests false positives with evidence, such as an instance whose flag is on but whose subnets are private, while a human makes the decision.