Securing your Amazon RDS database is critical for protecting sensitive data. Publicly accessible databases are prime targets for attackers who scan the internet for open database ports. This guide walks you through removing public access, configuring proper security groups, and implementing defense-in-depth for your RDS instances.
This article is part of our comprehensive Cloud Security Tips for 2026 guide covering essential practices for protecting your cloud environment.
Common RDS Security Mistakes
These configurations leave databases vulnerable:
- Public accessibility enabled - Database has a public IP address
- 0.0.0.0/0 in security groups - Allows connections from any IP
- Default security groups - Often overly permissive
- Database in public subnet - Exposed to internet routing
- Unencrypted connections - Data transmitted in clear text
Step 1: Remove Public Accessibility
Using AWS Console
- Open the RDS Console
- Select your database instance
- Click Modify
- Under Connectivity, find Additional configuration
- Set Publicly accessible to No
- Click Continue
- Choose Apply immediately or schedule for maintenance window
- Click Modify DB Instance
Using AWS CLI
# Remove public accessibility
aws rds modify-db-instance \
--db-instance-identifier my-database \
--no-publicly-accessible \
--apply-immediately
# Verify the change
aws rds describe-db-instances \
--db-instance-identifier my-database \
--query 'DBInstances[0].PubliclyAccessible'Note: This change may cause brief connectivity interruption as the public IP is released.
Step 2: Configure Security Groups
Security groups act as virtual firewalls controlling traffic to your database.
Create a Dedicated RDS Security Group
# Create security group
aws ec2 create-security-group \
--group-name rds-database-sg \
--description "Security group for RDS databases" \
--vpc-id vpc-12345678
# Output: sg-0123456789abcdef0Configure Inbound Rules
Allow only necessary sources to connect:
# Allow from application security group only
aws ec2 authorize-security-group-ingress \
--group-id sg-0123456789abcdef0 \
--protocol tcp \
--port 5432 \
--source-group sg-app-servers
# Allow from specific CIDR (private subnet only)
aws ec2 authorize-security-group-ingress \
--group-id sg-0123456789abcdef0 \
--protocol tcp \
--port 5432 \
--cidr 10.0.1.0/24Apply Security Group to RDS
aws rds modify-db-instance \
--db-instance-identifier my-database \
--vpc-security-group-ids sg-0123456789abcdef0 \
--apply-immediatelySecurity Group Best Practices
| Do | Don't |
|---|---|
| Reference other security groups | Use 0.0.0.0/0 (any IP) |
| Use specific CIDR ranges | Allow all ports |
| Document rule purposes | Use default security groups |
| Regularly audit rules | Leave unused rules |
Step 3: Place RDS in Private Subnet
Ensure your RDS instance is in a subnet without internet gateway routing.
Create DB Subnet Group
# Create subnet group with private subnets only
aws rds create-db-subnet-group \
--db-subnet-group-name private-db-subnets \
--db-subnet-group-description "Private subnets for RDS" \
--subnet-ids subnet-private-1a subnet-private-1b subnet-private-1cMove Existing RDS to Private Subnet
aws rds modify-db-instance \
--db-instance-identifier my-database \
--db-subnet-group-name private-db-subnets \
--apply-immediatelyImportant: Changing subnet groups may cause downtime. Schedule during maintenance windows.
Step 4: Enable Encryption
Encryption at Rest
For new databases, enable encryption during creation:
aws rds create-db-instance \
--db-instance-identifier my-secure-database \
--db-instance-class db.t3.medium \
--engine postgres \
--master-username admin \
--master-user-password "SecurePassword123!" \
--storage-encrypted \
--kms-key-id alias/rds-encryption-keyEncryption in Transit (SSL/TLS)
Force SSL connections via parameter groups:
# Create parameter group
aws rds create-db-parameter-group \
--db-parameter-group-name secure-postgres-params \
--db-parameter-group-family postgres15 \
--description "Secure PostgreSQL parameters"
# Require SSL
aws rds modify-db-parameter-group \
--db-parameter-group-name secure-postgres-params \
--parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=pending-reboot"
# Apply to database
aws rds modify-db-instance \
--db-instance-identifier my-database \
--db-parameter-group-name secure-postgres-paramsStep 5: Set Up Secure Access Patterns
Option A: Bastion Host (SSH Tunnel)
# Create SSH tunnel through bastion
ssh -L 5432:my-database.cluster-xyz.us-east-1.rds.amazonaws.com:5432 \
[email protected]
# Connect via tunnel
psql -h localhost -p 5432 -U admin -d mydbOption B: AWS Systems Manager Session Manager
# Start port forwarding session
aws ssm start-session \
--target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{
"host":["my-database.cluster-xyz.us-east-1.rds.amazonaws.com"],
"portNumber":["5432"],
"localPortNumber":["5432"]
}'Option C: AWS Client VPN
For frequent access, set up Client VPN to connect directly to your VPC.
Step 6: Enable IAM Database Authentication
Use AWS IAM instead of passwords for authentication:
# Enable IAM auth
aws rds modify-db-instance \
--db-instance-identifier my-database \
--enable-iam-database-authentication \
--apply-immediately
# Create database user for IAM auth (run in database)
# PostgreSQL:
CREATE USER iam_user WITH LOGIN;
GRANT rds_iam TO iam_user;
# Generate auth token
aws rds generate-db-auth-token \
--hostname my-database.cluster-xyz.us-east-1.rds.amazonaws.com \
--port 5432 \
--region us-east-1 \
--username iam_user
# Connect using token
PGPASSWORD=$(aws rds generate-db-auth-token ...) psql -h hostname -U iam_userStep 7: Enable Logging and Monitoring
# Enable enhanced monitoring
aws rds modify-db-instance \
--db-instance-identifier my-database \
--monitoring-interval 60 \
--monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring-role
# Enable Performance Insights
aws rds modify-db-instance \
--db-instance-identifier my-database \
--enable-performance-insights \
--performance-insights-retention-period 7
# Enable audit logging (PostgreSQL)
aws rds modify-db-parameter-group \
--db-parameter-group-name secure-postgres-params \
--parameters "ParameterName=log_statement,ParameterValue=all,ApplyMethod=immediate"Export logs to CloudWatch:
aws rds modify-db-instance \
--db-instance-identifier my-database \
--cloudwatch-logs-export-configuration '{"EnableLogTypes":["postgresql","upgrade"]}'Security Audit Checklist
Run this audit regularly:
# Check all RDS instances for public accessibility
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,PubliclyAccessible,Endpoint.Address]' \
--output table
# Find security groups with 0.0.0.0/0
aws ec2 describe-security-groups \
--query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
--output table
# Check encryption status
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,StorageEncrypted]' \
--output tableTerraform Example
Infrastructure-as-code for secure RDS:
resource "aws_db_instance" "secure_database" {
identifier = "secure-database"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
allocated_storage = 100
# Security settings
publicly_accessible = false
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
# Network
db_subnet_group_name = aws_db_subnet_group.private.name
vpc_security_group_ids = [aws_security_group.rds.id]
# Authentication
iam_database_authentication_enabled = true
# Monitoring
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
# Maintenance
backup_retention_period = 30
deletion_protection = true
}Best Practices Summary
| Control | Recommendation |
|---|---|
| Public Access | Always disable publicly accessible |
| Security Groups | Reference source security groups, not IP ranges |
| Subnets | Use private subnets only |
| Encryption | Enable at-rest and in-transit encryption |
| Authentication | Use IAM auth for AWS workloads |
| Secrets | Store credentials in Secrets Manager |
| Monitoring | Enable enhanced monitoring and logging |