Tarek Cheikh
Founder & AWS Security Expert
Amazon RDS manages patching, backups, and high availability for your relational databases -- but security configuration is your responsibility. A misconfigured RDS instance can expose customer data, financial records, and PII to the internet. The shared responsibility model means AWS secures the hypervisor and host OS, while you secure everything from network access and encryption to authentication and audit logging.
In August 2025, the nx npm supply chain attack demonstrated how quickly database infrastructure can be destroyed. Attackers injected malicious code into the popular nx build tool package, which ran during CI/CD pipelines with production credentials. The compromised pipeline connected to production RDS instances and terminated them, causing extended outages. The incident underscored why defense-in-depth for RDS -- network isolation, credential rotation, deletion protection, and proper backup retention -- is not optional.
AWS Security Hub now includes 48 RDS-specific controls (RDS.1 through RDS.50), making automated compliance monitoring straightforward. Combined with CIS AWS Foundations Benchmark v5.0/v6.0 controls, organizations have a clear roadmap for database security. This guide covers 12 battle-tested best practices with real AWS CLI commands and audit procedures.
RDS encryption at rest protects the underlying storage, automated backups, read replicas, and snapshots using AES-256 encryption. Encryption must be enabled at instance creation time -- you cannot encrypt an existing unencrypted instance in place.
aws/rds key does not allow you to manage rotation or grant cross-account access.# Create a CMK for RDS encryption
aws kms create-key --description "RDS encryption key - production databases" --key-usage ENCRYPT_DECRYPT --origin AWS_KMS --tags TagKey=Environment,TagValue=production
# Enable automatic key rotation (rotates annually)
aws kms enable-key-rotation --key-id arn:aws:kms:us-east-1:123456789012:key/KEY-ID
# Create an encrypted RDS instance with CMK
aws rds create-db-instance --db-instance-identifier prod-db --db-instance-class db.r6g.xlarge --engine postgres --engine-version 16.4 --master-username admin --manage-master-user-password --storage-encrypted --kms-key-id arn:aws:kms:us-east-1:123456789012:key/KEY-ID --allocated-storage 100
# Audit: Find unencrypted RDS instances
aws rds describe-db-instances --query "DBInstances[?StorageEncrypted==`false`].[DBInstanceIdentifier,Engine]" --output table
Note the --manage-master-user-password flag above. Introduced in 2023 and now a best practice, this tells RDS to generate and manage the master password in Secrets Manager automatically, eliminating manual credential management entirely.
Security Hub Controls: RDS.3 (encryption at rest enabled). CIS Benchmark: Control 2.3.1 (RDS encryption enabled).
Encryption at rest protects stored data, but data in transit between your application and the database can be intercepted without TLS. Each RDS engine has its own mechanism for enforcing encrypted connections.
# PostgreSQL: force SSL via parameter group
aws rds modify-db-parameter-group --db-parameter-group-name prod-postgres-params --parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=pending-reboot"
# MySQL / MariaDB: require secure transport
aws rds modify-db-parameter-group --db-parameter-group-name prod-mysql-params --parameters "ParameterName=require_secure_transport,ParameterValue=ON,ApplyMethod=pending-reboot"
# SQL Server: force encryption via rds.force_ssl
aws rds modify-db-parameter-group --db-parameter-group-name prod-sqlserver-params --parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=pending-reboot"
# Oracle: use SSL option in an option group
aws rds add-option-to-option-group --option-group-name prod-oracle-options --options "OptionName=SSL,OptionSettings=[{Name=SQLNET.SSL_VERSION,Value=1.2}]"
# Download the global RDS CA bundle
curl -o global-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
# Connect with SSL verification (PostgreSQL example)
psql "host=prod-db.abc123.us-east-1.rds.amazonaws.com port=5432 dbname=mydb user=admin sslmode=verify-full sslrootcert=global-bundle.pem"
# Audit: Check SSL enforcement in parameter groups
aws rds describe-db-parameters --db-parameter-group-name prod-postgres-params --query "Parameters[?ParameterName=='rds.force_ssl'].[ParameterName,ParameterValue]" --output table
Security Hub Controls: RDS.9 (database logging enabled, relates to connection auditing). Always use sslmode=verify-full (PostgreSQL) or --ssl-ca (MySQL) to prevent man-in-the-middle attacks -- sslmode=require alone does not verify the server certificate.
An RDS instance with PubliclyAccessible=true gets a public IP address resolvable from the internet. Even if security groups are restrictive, a publicly accessible database is one misconfiguration away from exposure. Multiple high-profile breaches have resulted from RDS instances left publicly accessible.
# Create a DB subnet group using private subnets only
aws rds create-db-subnet-group --db-subnet-group-name prod-private-subnets --db-subnet-group-description "Private subnets for production databases" --subnet-ids subnet-0a1b2c3d4e5f6a7b8 subnet-0b2c3d4e5f6a7b8c9
# Create instance in private subnet, explicitly disable public access
aws rds create-db-instance --db-instance-identifier prod-db --db-subnet-group-name prod-private-subnets --no-publicly-accessible --engine postgres --db-instance-class db.r6g.xlarge --manage-master-user-password
# Audit: Find publicly accessible instances
aws rds describe-db-instances --query "DBInstances[?PubliclyAccessible==`true`].[DBInstanceIdentifier,Endpoint.Address]" --output table
# Remediate: Disable public access on an existing instance
aws rds modify-db-instance --db-instance-identifier prod-db --no-publicly-accessible --apply-immediately
Security Hub Controls: RDS.2 (no public access). This is one of the most commonly failed controls across AWS accounts. Ensure route tables for database subnets have no route to an internet gateway.
Security groups act as a virtual firewall for your RDS instance. The principle of least privilege applies to network access just as it does to IAM policies.
# Create a dedicated security group for the database
aws ec2 create-security-group --group-name prod-rds-sg --description "Production RDS - allow only from app tier" --vpc-id vpc-0a1b2c3d4e5f
# Allow inbound only from the application-tier security group
aws ec2 authorize-security-group-ingress --group-id sg-0rds1234567890 --protocol tcp --port 5432 --source-group sg-0app1234567890
# Use a non-default port to reduce automated scanning noise
aws rds modify-db-instance --db-instance-identifier prod-db --db-port-number 5433 --apply-immediately
# Audit: Check for overly permissive security group rules
aws ec2 describe-security-groups --group-ids sg-0rds1234567890 --query "SecurityGroups[].IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]" --output json
Security Hub Controls: RDS.2 (no public access), RDS.23 (no default ports for security groups).
IAM database authentication replaces long-lived database passwords with short-lived authentication tokens generated via STS. Tokens expire after 15 minutes, drastically reducing the window of exposure if intercepted.
# Enable IAM auth on the instance
aws rds modify-db-instance --db-instance-identifier prod-db --enable-iam-database-authentication --apply-immediately
# Create a database user mapped to IAM (PostgreSQL)
# Run this SQL inside the database:
# CREATE USER iam_app_user WITH LOGIN;
# GRANT rds_iam TO iam_app_user;
# IAM policy allowing token generation
# Attach to the application role/user:
# {
# "Version": "2012-10-17",
# "Statement": [{
# "Effect": "Allow",
# "Action": "rds-db:connect",
# "Resource": "arn:aws:rds-db:us-east-1:123456789012:dbuser:dbi-resource-id/iam_app_user"
# }]
# }
# Generate an authentication token
aws rds generate-db-auth-token --hostname prod-db.abc123.us-east-1.rds.amazonaws.com --port 5432 --username iam_app_user --region us-east-1
# Connect using the token (PostgreSQL)
export PGPASSWORD=$(aws rds generate-db-auth-token --hostname prod-db.abc123.us-east-1.rds.amazonaws.com --port 5432 --username iam_app_user --region us-east-1)
psql "host=prod-db.abc123.us-east-1.rds.amazonaws.com port=5432 dbname=mydb user=iam_app_user sslmode=verify-full sslrootcert=global-bundle.pem"
Supported engines: MySQL 5.7+, PostgreSQL 9.6+, MariaDB 10.6+, Aurora MySQL, Aurora PostgreSQL. IAM auth has a limit of 200 new connections per second per instance -- for high-throughput workloads, pair it with RDS Proxy.
Security Hub Controls: RDS.10 (IAM authentication enabled).
RDS Proxy sits between your application and database, providing connection pooling, failover handling, and an additional security layer. It integrates natively with IAM authentication and Secrets Manager.
# Create an RDS Proxy with IAM auth and TLS required
aws rds create-db-proxy --db-proxy-name prod-rds-proxy --engine-family POSTGRESQL --auth '[{
"AuthScheme": "SECRETS",
"SecretArn": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod-db-creds",
"IAMAuth": "REQUIRED",
"Description": "Production DB credentials"
}]' --role-arn arn:aws:iam::123456789012:role/RDSProxyRole --vpc-subnet-ids subnet-0a1b2c3d subnet-0b2c3d4e --vpc-security-group-ids sg-0proxy12345 --require-tls
# Register the target (your RDS instance)
aws rds register-db-proxy-targets --db-proxy-name prod-rds-proxy --db-instance-identifiers prod-db
# Verify TLS enforcement
aws rds describe-db-proxies --db-proxy-name prod-rds-proxy --query "DBProxies[].RequireTLS"
IAMAuth: REQUIRED to ensure all connections use IAM tokens.--require-tls encrypts all proxy-to-client connections.Security Hub Controls: RDS.12 (IAM authentication for RDS clusters).
Automated backups are your last line of defense against ransomware, accidental deletion, and the kind of destructive supply chain attacks like the nx incident in August 2025. A terminated instance with no backups means permanent data loss.
# Set backup retention to 14 days with a preferred backup window
aws rds modify-db-instance --db-instance-identifier prod-db --backup-retention-period 14 --preferred-backup-window "03:00-04:00" --apply-immediately
# Audit: Find instances with insufficient backup retention
aws rds describe-db-instances --query "DBInstances[?BackupRetentionPeriod < `7`].[DBInstanceIdentifier,BackupRetentionPeriod]" --output table
# Use AWS Backup for centralized, cross-account backup management
aws backup create-backup-plan --backup-plan '{
"BackupPlanName": "rds-daily-backup",
"Rules": [{
"RuleName": "DailyBackup",
"TargetBackupVaultName": "prod-backup-vault",
"ScheduleExpression": "cron(0 3 * * ? *)",
"Lifecycle": {
"DeleteAfterDays": 35
},
"CopyActions": [{
"DestinationBackupVaultArn": "arn:aws:backup:eu-west-1:123456789012:backup-vault:dr-vault",
"Lifecycle": {"DeleteAfterDays": 90}
}]
}]
}'
Security Hub Controls: RDS.11 (automated backups enabled), RDS.26 (RDS DB instances should be protected by a backup plan).
RDS snapshots contain a full copy of your database. An unencrypted, publicly shared snapshot is equivalent to publishing your entire database on the internet.
# Check each snapshot for public access (should return empty)
for snap in $(aws rds describe-db-snapshots --snapshot-type manual --query "DBSnapshots[].DBSnapshotIdentifier" --output text); do
aws rds describe-db-snapshot-attributes --db-snapshot-identifier "$snap" --query "DBSnapshotAttributesResult.DBSnapshotAttributes[?AttributeName=='restore'].AttributeValues[]" --output text | grep -q 'all' && echo "PUBLIC: $snap"
done
# Check for unencrypted snapshots
aws rds describe-db-snapshots --query "DBSnapshots[?Encrypted==`false`].[DBSnapshotIdentifier,DBInstanceIdentifier]" --output table
# Copy an unencrypted snapshot to an encrypted one
aws rds copy-db-snapshot --source-db-snapshot-identifier unencrypted-snapshot --target-db-snapshot-identifier encrypted-snapshot --kms-key-id arn:aws:kms:us-east-1:123456789012:key/KEY-ID
# Block public snapshot sharing at the account level
aws rds modify-db-snapshot-attribute --db-snapshot-identifier my-snapshot --attribute-name restore --values-to-remove all
# Use an SCP to prevent public snapshot sharing organization-wide
# {
# "Version": "2012-10-17",
# "Statement": [{
# "Sid": "DenyPublicRDSSnapshots",
# "Effect": "Deny",
# "Action": "rds:ModifyDBSnapshotAttribute",
# "Resource": "*",
# "Condition": {
# "StringEquals": {
# "rds:AddAttributeValue": "all"
# }
# }
# }]
# }
Security Hub Controls: RDS.1 (no public snapshots), RDS.4 (snapshot encryption). These are among the most critical RDS controls -- a public snapshot bypasses all network and authentication controls entirely.
Deletion protection prevents accidental or malicious deletion of your RDS instances. After the nx supply chain attack in August 2025, which terminated production RDS instances via compromised CI/CD credentials, deletion protection became a non-negotiable baseline control.
# Enable deletion protection on an existing instance
aws rds modify-db-instance --db-instance-identifier prod-db --deletion-protection --apply-immediately
# Enable on creation
aws rds create-db-instance --db-instance-identifier prod-db --deletion-protection --engine postgres --db-instance-class db.r6g.xlarge --manage-master-user-password
# Audit: Find instances without deletion protection
aws rds describe-db-instances --query "DBInstances[?DeletionProtection==`false`].[DBInstanceIdentifier,Engine]" --output table
# For Aurora clusters
aws rds modify-db-cluster --db-cluster-identifier prod-cluster --deletion-protection --apply-immediately
Deletion protection requires a two-step process to delete an instance: first disable protection, then delete. This provides a deliberate friction that prevents automated scripts or compromised pipelines from destroying databases in a single API call.
Security Hub Controls: RDS.8 (deletion protection enabled).
Database-level audit logging captures who executed what queries, when, and from where. This is essential for forensics, compliance (PCI-DSS, SOC 2, HIPAA), and detecting anomalous access patterns. RDS logs should be published to CloudWatch Logs for centralized monitoring and alerting.
# PostgreSQL: Enable pgAudit extension
# In your parameter group:
aws rds modify-db-parameter-group --db-parameter-group-name prod-postgres-params --parameters "ParameterName=shared_preload_libraries,ParameterValue=pgaudit,ApplyMethod=pending-reboot" "ParameterName=pgaudit.log,ParameterValue=ddl+role+write,ApplyMethod=immediate" "ParameterName=pgaudit.role,ParameterValue=rds_pgaudit,ApplyMethod=immediate"
# MySQL / MariaDB: Enable MariaDB Audit Plugin
aws rds modify-db-parameter-group --db-parameter-group-name prod-mysql-params --parameters "ParameterName=server_audit_logging,ParameterValue=1,ApplyMethod=immediate" "ParameterName=server_audit_events,ParameterValue=CONNECT+QUERY_DDL+QUERY_DML,ApplyMethod=immediate"
# Aurora MySQL: Enable Advanced Auditing
aws rds modify-db-cluster-parameter-group --db-cluster-parameter-group-name prod-aurora-params --parameters "ParameterName=server_audit_logging,ParameterValue=1,ApplyMethod=immediate" "ParameterName=server_audit_events,ParameterValue=CONNECT+QUERY_DDL+QUERY_DML+QUERY_DCL,ApplyMethod=immediate"
# Publish logs to CloudWatch Logs
aws rds modify-db-instance --db-instance-identifier prod-db --cloudwatch-logs-export-configuration '{"EnableLogTypes":["postgresql","upgrade"]}' --apply-immediately
# For MySQL:
aws rds modify-db-instance --db-instance-identifier prod-mysql-db --cloudwatch-logs-export-configuration '{"EnableLogTypes":["audit","error","slowquery"]}' --apply-immediately
Security Hub Controls: RDS.9 (database logging enabled), RDS.34 (Aurora MySQL audit logging), RDS.36 (Aurora PostgreSQL logging to CloudWatch).
Static database passwords that live in configuration files, environment variables, or worse -- source code -- are a persistent breach vector. AWS Secrets Manager provides automatic rotation with zero application downtime using the alternating users strategy.
# New instances: use managed master user password (recommended)
aws rds create-db-instance --db-instance-identifier prod-db --engine postgres --db-instance-class db.r6g.xlarge --manage-master-user-password --master-user-secret-kms-key-id arn:aws:kms:us-east-1:123456789012:key/KEY-ID
# Existing instances: enable managed password
aws rds modify-db-instance --db-instance-identifier prod-db --manage-master-user-password --master-user-secret-kms-key-id arn:aws:kms:us-east-1:123456789012:key/KEY-ID --apply-immediately
# Verify managed password is active
aws rds describe-db-instances --db-instance-identifier prod-db --query "DBInstances[].MasterUserSecret"
# Create a secret for application database credentials
aws secretsmanager create-secret --name prod/myapp/db-credentials --secret-string '{"username":"app_user","password":"INITIAL_PASSWORD","engine":"postgres","host":"prod-db.abc123.us-east-1.rds.amazonaws.com","port":5432,"dbname":"mydb"}'
# Enable rotation with the alternating users strategy
aws secretsmanager rotate-secret --secret-id prod/myapp/db-credentials --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation --rotation-rules '{"AutomaticallyAfterDays": 30}'
# Retrieve the current secret in your application
aws secretsmanager get-secret-value --secret-id prod/myapp/db-credentials --query "SecretString" --output text
The alternating users strategy maintains two database users (e.g., app_user and app_user_clone). During rotation, Secrets Manager updates the inactive user's password, then switches the active label. This ensures the current password always works, even during rotation.
Managed master passwords with Secrets Manager integration ensure credentials are never exposed and are rotated automatically.
RDS event subscriptions notify you in real time when security-relevant changes occur -- configuration modifications, failovers, deletion protection changes, and security group updates. Without event subscriptions, you are flying blind between periodic audits.
# Create an SNS topic for RDS security alerts
aws sns create-topic --name rds-security-alerts
# Subscribe your security team
aws sns subscribe --topic-arn arn:aws:sns:us-east-1:123456789012:rds-security-alerts --protocol email --notification-endpoint security-team@example.com
# Create event subscriptions for critical categories
aws rds create-event-subscription --subscription-name rds-config-changes --sns-topic-arn arn:aws:sns:us-east-1:123456789012:rds-security-alerts --source-type db-instance --event-categories '["configuration change","deletion","failover","failure","notification"]' --enabled
# Create a subscription for security group changes
aws rds create-event-subscription --subscription-name rds-security-group-changes --sns-topic-arn arn:aws:sns:us-east-1:123456789012:rds-security-alerts --source-type db-security-group --event-categories '["configuration change","failure"]' --enabled
# Create a subscription for snapshot events
aws rds create-event-subscription --subscription-name rds-snapshot-events --sns-topic-arn arn:aws:sns:us-east-1:123456789012:rds-security-alerts --source-type db-snapshot --event-categories '["creation","deletion","restoration"]' --enabled
# List all event subscriptions
aws rds describe-event-subscriptions --query "EventSubscriptionsList[].[CustSubscriptionId,SourceType,EventCategoriesList]" --output table
Route SNS notifications to a security SIEM, a Slack channel, or a Lambda function that triggers automated remediation. For example, a Lambda function can automatically re-enable deletion protection if it detects it was disabled.
Security Hub Controls: RDS.19 (event notifications for critical cluster events), RDS.20 (event notifications for critical database instance events), RDS.21 (event notifications for database parameter groups), RDS.22 (event notifications for database security groups).
| Misconfiguration | Risk | Detection |
|---|---|---|
| PubliclyAccessible = true | Database exposed to the internet | Security Hub RDS.2; aws rds describe-db-instances --query "DBInstances[?PubliclyAccessible==`true`]" |
| Unencrypted storage | Data exposed if underlying storage is compromised | Security Hub RDS.3; aws rds describe-db-instances --query "DBInstances[?StorageEncrypted==`false`]" |
| Public snapshots (restore attribute = "all") | Full database copy accessible to any AWS account | Security Hub RDS.1; aws rds describe-db-snapshot-attributes |
| SSL/TLS not enforced (rds.force_ssl = 0) | Credentials and data transmitted in plaintext | Check parameter group; aws rds describe-db-parameters |
| Default port (3306/5432/1433) | Targeted by automated scanners | Security Hub RDS.23 |
| Backup retention < 7 days | Insufficient recovery window for ransomware or destructive attacks | Security Hub RDS.11, RDS.26 |
| Deletion protection disabled | Instance can be destroyed by compromised CI/CD pipeline | Security Hub RDS.8 |
| Master password in plaintext (not managed) | Long-lived credentials in config files or environment variables | Security Hub RDS.28; aws rds describe-db-instances --query "DBInstances[].MasterUserSecret" |
| Security group allows 0.0.0.0/0 | Database accepts connections from any IP address | Security Hub RDS.2; aws ec2 describe-security-groups |
| # | Practice | Priority |
|---|---|---|
| 1 | Encrypt at rest with customer-managed KMS keys | Critical |
| 2 | Enforce SSL/TLS for all connections | Critical |
| 3 | Deploy in private subnets only | Critical |
| 4 | Configure least-privilege security groups | Critical |
| 5 | Enable IAM database authentication | High |
| 6 | Use RDS Proxy with IAM auth and TLS | High |
| 7 | Automated backups with 7+ day retention | Critical |
| 8 | Encrypt snapshots, block public sharing | Critical |
| 9 | Enable deletion protection | Critical |
| 10 | Enable audit logging per engine | High |
| 11 | Rotate credentials with Secrets Manager | High |
| 12 | Enable event subscriptions for security alerts | Medium |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing AWS Identity and Access Management. Covers MFA enforcement, least privilege, IAM Identity Center, SCPs, Access Analyzer, and credential management.
Comprehensive guide to securing Amazon S3. Covers Block Public Access, encryption (SSE-KMS, SSE-C deprecation), Object Lock, MFA Delete, VPC endpoints, presigned URLs, and GuardDuty S3 Protection.
Comprehensive guide to securing AWS EC2 instances. Covers IMDSv2 enforcement, security groups, EBS encryption, SSM Session Manager, private subnets, VPC Flow Logs, Amazon Inspector, and AMI hardening.