Back to blog
AWSBest PracticesCloud SecurityDatabasesNetworking

RDS Instance Publicly Accessible: Why It Matters and How to Fix It

Learn why a publicly accessible RDS instance is a serious risk, how to disable public access with CLI and Terraform, and how to prevent it with policy-as-code.

TL;DR

This check flags any RDS instance with PubliclyAccessible set to true, which assigns it a public IP and exposes the database endpoint to the internet. Set --no-publicly-accessible, place the instance in private subnets, and lock the security group down to known sources.

An RDS database holds the data your application cannot run without: user accounts, orders, session tokens, sometimes payment records. When that database is reachable from the public internet, the only thing standing between an attacker and your data is a security group rule and a password. That is a thin line, and this check exists to make sure you never rely on it by accident.

The rds_public check looks at the PubliclyAccessible attribute on each RDS instance. When it is set to true, AWS resolves the instance endpoint to a public IP address. Anyone on the internet can attempt a connection to that endpoint on the database port.


What this check detects

Every RDS instance has a boolean attribute called PubliclyAccessible. It controls how the instance's DNS endpoint resolves:

  • true — the endpoint resolves to a public IPv4 address. The instance is reachable from outside the VPC, subject to security group and network ACL rules.
  • false — the endpoint resolves only to a private IP inside the VPC. External hosts cannot route to it at all.

The check fails when PubliclyAccessible is true. It does not matter whether your security group is currently tight. Public accessibility plus one over-permissive rule equals exposure, and security groups drift over time.

Note: Public accessibility and security group rules are two separate layers. An instance can be publicly accessible but still unreachable if the security group blocks all inbound traffic. The danger is that these two settings are managed by different people at different times, so the safe combination is fragile.


Why it matters

A publicly accessible database is one of the most reliably exploited misconfigurations in cloud environments. Here is how it typically plays out.

Internet-wide scanning is constant

Tools like Shodan and Censys continuously index every reachable database port on the internet. The moment your RDS endpoint gets a public IP, it appears in those indexes. Automated scanners then probe default ports (5432 for PostgreSQL, 3306 for MySQL, 1433 for SQL Server) looking for instances that respond.

Credentials are the only remaining defense

Once an attacker can reach the database, they start brute-forcing or credential-stuffing the admin user. If you used a weak master password, reused one from another system, or left a default account in place, the database is theirs. With it goes every table inside.

It compounds other mistakes

Public accessibility turns small problems into breaches. A security group rule that allows 0.0.0.0/0 on the database port becomes catastrophic instead of merely sloppy. An unpatched database engine with a known CVE becomes remotely exploitable instead of contained. The public IP is the multiplier.

Warning: Several large data exposures traced back to nothing more sophisticated than a publicly accessible database with a guessable password. There was no zero-day involved, just an endpoint that should never have been reachable.

Compliance implications

Frameworks like PCI DSS, HIPAA, and SOC 2 expect databases holding sensitive data to be isolated from public networks. A publicly accessible RDS instance is a finding that auditors will flag and that you will have to remediate before certification.


How to fix it

The fix has two parts: disable public accessibility, and confirm the instance lives in private subnets with a tight security group.

Step 1: Check the current state

aws rds describe-db-instances \
  --db-instance-identifier my-database \
  --query 'DBInstances[0].{Public:PubliclyAccessible,SubnetGroup:DBSubnetGroup.DBSubnetGroupName,SG:VpcSecurityGroups}'

If Public comes back as true, you have work to do.

Step 2: Disable public accessibility

Danger: Changing public accessibility on a production database can interrupt connections from any clients currently reaching it over the public endpoint. Before you apply this, confirm that every application connecting to the database is inside the VPC or reaches it through a private path. Test in a maintenance window if you are unsure.

aws rds modify-db-instance \
  --db-instance-identifier my-database \
  --no-publicly-accessible \
  --apply-immediately

Without --apply-immediately, the change waits until the next maintenance window. With it, the change applies right away, which can cause a brief connection blip as the endpoint DNS is updated.

Step 3: Confirm the instance is in private subnets

Disabling public accessibility is the headline fix, but an instance launched in public subnets is still architecturally wrong. Check the subnet group:

aws rds describe-db-subnet-groups \
  --db-subnet-group-name my-subnet-group \
  --query 'DBSubnetGroups[0].Subnets[].SubnetIdentifier'

Then verify each subnet has no route to an internet gateway. A private subnet routes outbound traffic through a NAT gateway or has no internet route at all.

Step 4: Tighten the security group

Restrict inbound access to the specific sources that need it, ideally another security group rather than a CIDR range.

aws ec2 authorize-security-group-ingress \
  --group-id sg-0db1234567890abcd \
  --protocol tcp \
  --port 5432 \
  --source-group sg-0app1234567890abc

Referencing the application's security group as the source means only instances in that group can connect, regardless of how IP addresses change.

Tip: If you need to reach the database from your laptop for debugging, do not make it public. Use a bastion host or AWS Systems Manager Session Manager port forwarding to tunnel through a private path. The database stays internal and you keep your access.

Doing it in Terraform

If you manage RDS with infrastructure as code, the fix belongs in the resource definition so it does not get reverted on the next apply.

resource "aws_db_instance" "main" {
  identifier             = "my-database"
  engine                 = "postgres"
  instance_class         = "db.t3.medium"
  publicly_accessible    = false
  db_subnet_group_name   = aws_db_subnet_group.private.name
  vpc_security_group_ids = [aws_security_group.db.id]
}

resource "aws_db_subnet_group" "private" {
  name       = "my-subnet-group"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
}

How to prevent it from happening again

Fixing one instance is easy. Making sure no one ever launches another public database is the real goal.

Block it with a Service Control Policy

An SCP can deny the creation of any publicly accessible RDS instance across an entire AWS Organization. This is the strongest guardrail because it cannot be bypassed by individual account admins.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "rds:CreateDBInstance",
        "rds:ModifyDBInstance"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "rds:Public": "true"
        }
      }
    }
  ]
}

Gate it in CI/CD

Catch the misconfiguration before it reaches AWS by scanning Terraform plans in your pipeline. A policy-as-code rule with Checkov or OPA fails the build when publicly_accessible is true.

# Run Checkov against your Terraform
checkov -d . --check CKV_AWS_17

CKV_AWS_17 is the built-in Checkov check for publicly accessible RDS instances. Wire it into your pull request workflow so the check runs on every change.

Tip: Layer your defenses. SCPs stop it at the account boundary, CI/CD catches it at code review, and a continuous scanner like Lensix catches anything created out of band through the console or a stray script. No single layer catches everything.

Continuous monitoring

People change settings directly in the console, scripts run outside of CI/CD, and merged exceptions get forgotten. Continuous scanning of your live environment is what catches the drift that static gates miss. Lensix runs the rds_public check on a schedule and surfaces any instance that becomes publicly accessible, so you find out from a dashboard rather than from a breach report.


Best practices

  • Default to private. Treat publicly_accessible = false as the only acceptable setting and require a written justification for any exception.
  • Use private subnets for all databases. A database should never sit in a subnet that routes to an internet gateway, even with public accessibility disabled.
  • Reference security groups, not CIDRs. Allow inbound from the application's security group instead of IP ranges. It is tighter and survives IP changes.
  • Store credentials in Secrets Manager. Use a strong, rotated master password managed by AWS Secrets Manager rather than a static string in your IaC.
  • Enforce encryption in transit. Require SSL/TLS connections so that even private traffic is protected.
  • Use bastions or Session Manager for human access. Never make a database public just to reach it during debugging.
  • Audit regularly. Run a scheduled check across all accounts and regions, because a new account or a forgotten region is exactly where a public database hides.

The cleanest rule of thumb: if you cannot articulate why a specific database needs to be reachable from the internet, it does not. Almost nothing legitimately does.

Public accessibility on RDS is a setting that should be false everywhere, enforced at multiple layers, and verified continuously. Disable it, push the database into private subnets, lock the security group to known sources, and put a guardrail in place so the question never comes up again.