AWS Builder Center

Securely Accessing an Amazon RDS Database Across VPCs Using RDS Proxy and NLB

Secure method for establishing connections to Amazon Aurora or Amazon RDS databases situated between VPCs and external clients - CLI Implementation Guide

This article was created in collaboration with my colleague,  Sumit Kumar 

Overview

Organizations use multiple VPCs to separate workloads and environments. While databases are typically managed in a dedicated VPC, teams need secure access from other VPCs and external locations. This solution enables secure database access without complex networking setups like VPC peering or Transit Gateway.
This guide provides CLI instructions for securely accessing an Amazon RDS database across multiple VPCs using RDS Proxy and Network Load Balancer (NLB). The solution enables cross-VPC database connectivity without complex VPC peering. The solution works across all RDS database engines (PostgreSQL: 5432, MySQL: 3306, SQL Server: 1433) with minimal configuration changes.
Solution Overview


Architecture Components

  • VPC A: Contains RDS database and RDS Proxy in private subnets
  • VPC B: Hosts cross-VPC RDS Proxy endpoint and NLB in public subnets
  • Security: Managed through security groups and AWS Secrets Manager
  • DNS: Optional Route 53 configuration for custom domain resolution

Prerequisites

Required Components

  • AWS CLI v2 installed and configured with appropriate credentials
  • IAM permissions for:
  • RDS
  • EC2
  • Route 53
  • Secrets Manager
  • Network Load Balancer
  • Two VPCs (VPC A and VPC B) with private and public subnets
  • Database client tools
  • Amazon RDS database in VPC A's private subnet

AWS CLI Configuration

Bash
aws configure --profile your-profile
aws --version


Implementation Steps

Step 1: Configure RDS Proxy in VPC A

Verify Database Setup

Bash
aws rds describe-db-instances \
--region <region> \
--db-instance-identifier my-aurora-cluster-instance-1 \
--query 'DBInstances[0].DBSubnetGroup.Subnets[*].SubnetIdentifier' \
--profile <your-profile>


Create Database User

Connect to your database and execute:
For PostgreSQL:
SQL
CREATE USER proxy_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE mydb TO proxy_user;

For MySQL:
SQL
CREATE USER 'proxy_user'@'%' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'proxy_user'@'%';

For SQL Server:
SQL
USE [mydb];
CREATE LOGIN proxy_user WITH PASSWORD = 'secure_password';
CREATE USER proxy_user FOR LOGIN proxy_user;
GRANT ALL TO proxy_user;


Create Secret in Secrets Manager

Bash
aws secretsmanager create-secret \
--name my-rds-proxy-secret \
--secret-string '{
"username":"proxy_user",
"password":"secure_password",
"engine": "postgres",
"host": "<host_endpoint>",
"port": <database_port>,
"dbname": "db_name"
}' \
--region <region> \
--profile <your-profile>

Note: Replace host_endpoint with your RDS database endpoint, database port with correct port and db_name with database name of your database

Create Security Group in VPC A for RDS Proxy and add inbound rule to it.

Bash
aws ec2 create-security-group \
--group-name rds-proxy-sg \
--description "Security group for RDS Proxy in VPC A" \
--vpc-id <vpc-id-vpc-a> \
--region <region> \
--profile <your-profile>

aws ec2 authorize-security-group-ingress \
--group-id sg-<rds-proxy-sg-id> \
--protocol all \
--source-group sg-<rds-proxy-sg-id> \
--region <region> \
--profile <your-profile>

Note: Replace sg-<rds-proxy-sg-id> in the above command with the actual security group ID returned from the above create security group command.

Create RDS Proxy

Bash
aws rds create-db-proxy \
--db-proxy-name my-rds-proxy \
--engine-family POSTGRESQL \
--role-arn arn:aws:iam::<account-id>:role/my-rds-proxy-role \
--vpc-subnet-ids subnet-<private-subnet-id-1> subnet-<private-subnet-id-2> \
--auth '[
{
"AuthScheme": "SECRETS",
"SecretArn": "arn:aws:secretsmanager:<region>:<account-id>:secret:my-rds-secret",
"IAMAuth": "DISABLED",
"Description": "Proxy database credentials"
}
]' \
--security-group-ids sg-<rds-proxy-sg-id> \
--region <region> \
--profile <your-profile>

Note: Adjust --engine-family to MYSQL, POSTGRESQL, or SQLSERVER based on your RDS engine.

Register Database Target

Register the RDS database as a target for the RDS Proxy.
Bash
aws rds register-db-proxy-targets \
--db-proxy-name my-rds-proxy \
--target-group-name default \
--db-cluster-identifiers my-aurora-cluster \
--region <region> \
--profile <your-profile>

Note: Use --db-cluster-identifiers for Aurora or --db-instance identifiers for non-Aurora RDS.

Step 2: Create Cross-VPC RDS Proxy Endpoint in VPC B

This configuration enables secure access to your RDS Proxy from VPC B without complex networking setups.

Create Security Group and add inboud rule for Cross-VPC Endpoint

Bash
aws ec2 create-security-group \
--group-name cross-vpc-proxy-sg \
--description "Security group for cross-VPC RDS Proxy endpoint in VPC B" \
--vpc-id <vpc-id-vpc-b> \
--region <region> \
--profile <your-profile>

aws ec2 authorize-security-group-ingress \
--group-id sg-<cross-vpc-proxy-sg-id> \
--protocol all \
--source-group sg-<cross-vpc-proxy-sg-id> \
--region <region> \
--profile <your-profile>

Note: Replace sg-<cross-vpc-proxy-sg-id> in the above command with the actual security group ID returned from the above create security group command.

Create Cross-VPC Endpoint

Create a cross-VPC endpoint for the RDS Proxy with at least two subnets in VPC B and use describe proxy endpoints to verify the status.
Bash
aws rds create-db-proxy-endpoint \
--db-proxy-name my-rds-proxy \
--endpoint-name my-cross-vpc-endpoint \
--vpc-subnet-ids subnet-<private-subnet-id-1-vpc-b> subnet-<private-subnet-id-2-vpc-b> \
--vpc-security-group-ids sg-<cross-vpc-proxy-sg-id> \
--region <region> \
--profile <your-profile>

aws rds describe-db-proxy-endpoints \
--db-proxy-name my-rds-proxy \
--region <region> --profile <your-profile>

Note: Replace sg-<cross-vpc-proxy-sg-id> in the above command with the actual security group ID returned from the above create security group command.

Step 3: Configure Network Load Balancer(NLB) in VPC B

Create Security Group for NLB and add inbound rules

Bash
aws ec2 create-security-group \
--group-name nlb-sg \
--description "Security group for NLB in VPC B" \
--vpc-id <vpc-id-vpc-b> \
--region <region> \
--profile <your-profile>

aws ec2 authorize-security-group-ingress \
--group-id sg-<nlb-sg-id> \
--protocol all \
--source-group sg-<nlb-sg-id> \
--region <region> \
--profile <your-profile>


Update Cross-VPC Endpoint Security Group in VPC B

Update the inbound rule to allow traffic from the NLB security group to allow traffic from the NLB to reach the RDS Proxy endpoint.
Bash
aws ec2 authorize-security-group-ingress \
--group-id sg-<cross-vpc-proxy-sg-id> \
--protocol tcp \
--port 5432 \
--source-group sg-<nlb-sg-id> \
--region <region> --profile <your-profile>


Create Network Load Balancer

Follow these steps to deploy an NLB in a public subnet of VPC B, providing a stable public entry point for external database access (use the same AZs associated with your RDS Proxy cross-VPC endpoint subnets).
Bash
aws elbv2 create-load-balancer \
--name my-nlb \
--type network \
--subnets subnet-<public-subnet-id-1-vpc-b> subnet-<public-subnet-id-2-vpc-b> \
--scheme internet-facing \
--security-groups sg-<nlb-sg-id> \
--region <region> \
--profile <your-profile>


Create Target Group

Create a target group for the NLB to route traffic to the RDS Proxy endpoint IPs.
Bash
aws elbv2 create-target-group \
--name my-nlb-targets \
--protocol TCP \
--port 5432 \
--vpc-id <vpc-id-vpc-b> \
--target-type ip \
--region <region> \
--profile <your-profile>


Register Targets

Register the RDS Proxy endpoint IPs as targets for the NLB. You need to provide IP addresses for RDS Proxy cross-VPC endpoint. Resolve this endpoint by running commands like nslookup or ping on an EC2 instance deployed in VPC B, note down the IP addresses
Bash
aws elbv2 register-targets \
--target-group-arn <target-group-arn> \
--targets Id=<proxy-endpoint-ip-1> Id=<proxy-endpoint-ip-2> \
--region <region> \
--profile <your-profile>


Create Listener

Attach a listener to the NLB to forward traffic to the target group.
Bash
aws elbv2 create-listener \
--load-balancer-arn <load-balancer-arn> \
--protocol TCP \
--port 5432 \
--default-actions Type=forward,TargetGroupArn=<target-group-arn> \
--region <region> \
--profile <your-profile>


Step 4: Configure DNS Resolution (Optional)

Setting up DNS resolution simplifies access for production or testing environments by mapping domain names to your Network Load Balancer (NLB).

Create Public Hosted Zone (for Production)

Bash
aws route53 create-hosted-zone \
--name db.yourcompany.com \
--caller-reference "public-dns-$(date +%s)" \
--region <region> \
--profile <your-profile>

Note: After creation of above public hosted zone, add a CNAME record pointing to you NLB’s DNS endpoint (e.g., my-nlb-123456789012.elb.us-east-1.amazonaws.com).

Create Private Hosted Zone (for Testing)

This option creates a private hosted zone which can be shared with other accounts or VPCs to simulate public access testing for your RDS database. Create a private hosted zone and add a CNAME record for testing access to your RDS database from another VPC or AWS account.
Bash
aws route53 create-hosted-zone \
--name test.db.yourcompany.com \
--hosted-zone-config '{"PrivateZone":true}' \
--vpc VPCRegion=<region>,VPCId=<vpc-id-vpc-b> \
--caller-reference "test-dns-$(date +%s)" \
--region <region> \
--profile <your-profile>


Create DNS Records

Create a file named  private-record.json  with following contents.
JSON
{
"Changes":[
{
"Action":"CREATE",
"ResourceRecordSet":{
"Name":"myrds.db.internal",
"Type":"CNAME",
"TTL":300,
"ResourceRecords":[
{
"Value":"my-nlb-123456789012.elb.us-east-1.amazonaws.com"
}
]
}
}
]
}


Apply the DNS record:=

Bash
aws route53 change-resource-record-sets \
--hosted-zone-id <hosted-zone-id> \
--change-batch file://private-record.json \
--region <region> \
--profile <your-profile>


Authorize Association from Second Account (for Private Hosted Zone)

Authorization enables cross-account VPC association, necessary for multi-account testing. Authorize the second account’s VPC to associate with the private hosted zone. Obtain Hosted zone ID from Hosted zone details section of private hosted zone db.internal.
Bash
# Authorize association from primary account
aws route53 create-vpc-association-authorization \
--hosted-zone-id <hosted-zone-id> \
--vpc VPCRegion=<second-account-region>,VPCId=<vpc-id-second-account> \
--region <primary-account-region> --profile <your-profile>

Note: Run above command from your primary AWS account to authorize the second AWS account for VPC association with the private hosted zone.
Associate the second account VPC with the private hosted zone.
Bash
# Associate VPC from second account
aws route53 associate-vpc-with-hosted-zone \
--hosted-zone-id <hosted-zone-id> \
--vpc VPCRegion=<second-account-region>,VPCId=<vpc-id-second-account> \
--region <second-account-region> --profile <second-account-profile>

Note: Run this command from the second AWS account. It will complete the association of second AWS account VPC with private hosted zone. Ensure the IAM role or user in the second aws account has the route53:AssociateVPCWithHostedZone permission.
Bash
# Verify association
aws route53 list-hosted-zones-by-vpc \
--vpc VPCRegion=<second-account-region>,VPCId=<vpc-id-second-account> \
--region <second-account-region> --profile <second-account-profile>


Step 5: Verify Cross-VPC and Public Access

Follow these steps to verify that your configuration allows secure access from both VPC B and external sources.

Test Access from VPC B

Connect to your application server or EC2 instance in VPC B. Use the appropriate database client command based on your database engine
Bash
# For PostgreSQL:
psql -h my-cross-vpc-endpoint.endpoint.proxy-abcdefghijkl.us-east-1.rds.amazonaws.com -p 5432 -U proxy_user

# For MySQL/MariaDB:
mysql -h my-cross-vpc-endpoint.endpoint.proxy-abcdefghijkl.us-east-1.rds.amazonaws.com -P 3306 -u proxy_user -p

# For SQL Server:
sqlcmd -S my-cross-vpc-endpoint.endpoint.proxy-abcdefghijkl.us-east-1.rds.amazonaws.com,1433 -U proxy_user -P secure_password


Test Access from second AWS Account simulating Public Access

Connect to an EC2 instance or other resource such as AWS CloudShell in the second account. Use the appropriate database client command based on your database engine
Bash
# For PostgreSQL:
psql -h myrds.db.internal -p 5432 -U proxy_user

# For MySQL/MariaDB:
mysql -h myrds.db.internal -P 3306 -u proxy_user -p

# For SQL Server:
sqlcmd -S myrds.db.internal,1433 -U proxy_user -P secure_password


Monitoring, Optimization and Security

Follow these steps to monitor the performance of your Network Load Balancer (NLB) and RDS Proxy and optimize your setup for security and efficiency. Create a file named  metrics.json  with appropriate metric queries and the following command

CloudWatch Metrics

Bash
aws cloudwatch get-metric-data \
--metric-data-queries file://metrics.json \
--start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--region <region> \
--profile <your-profile>


Optimize Performance

  • For RDS Proxy, adjust performance settings. Maximum connection pool size, Connection borrow timeout, Idle client connection timeout
  • For NLB, configure health check protocol, health check port, health check path, interval, healthy/unhealthy thresholds

Security

  • Modify NLB rules to allow access only from trusted IP ranges
  • Enable TLS encryption for RDS Proxy
  • Enable automatic rotation, set rotation schedule, select rotation function for Secrets.

Cleanup

Execute these commands in reverse order to clean up resources:
Bash
# Delete Route 53 records
aws route53 delete-hosted-zone --id <hosted-zone-id>

# Delete NLB
aws elbv2 delete-load-balancer --load-balancer-arn <load-balancer-arn>

# Delete target group
aws elbv2 delete-target-group --target-group-arn <target-group-arn>

# Delete RDS Proxy endpoint
aws rds delete-db-proxy-endpoint \
--db-proxy-name my-rds-proxy \
--endpoint-name my-cross-vpc-endpoint

# Delete RDS Proxy
aws rds delete-db-proxy --db-proxy-name my-rds-proxy

# Delete security groups
aws ec2 delete-security-group --group-id sg-<nlb-sg-id>
aws ec2 delete-security-group --group-id sg-<cross-vpc-proxy-sg-id>
aws ec2 delete-security-group --group-id sg-<rds-proxy-sg-id>

# Delete secret
aws secretsmanager delete-secret \
--secret-id my-rds-proxy-secret \
--force-delete-without-recovery


Key Benefits

  • Simplified Networking: Eliminates complex VPC peering configurations
  • Enhanced Security: Database remains in private subnets with controlled access
  • Scalability: Supports multiple client connections and cross-VPC access
  • Flexibility: Works with various RDS database engines (PostgreSQL, MySQL, etc.)

Security Considerations

  • Store all credentials in AWS Secrets Manager
  • Use security groups to restrict access at each layer
  • Enable encryption in transit for database connections
  • Implement least-privilege IAM policies for RDS Proxy role
  • Monitor access patterns using CloudWatch and AWS CloudTrail

Troubleshooting Tips

  • Verify security group rules allow traffic between components
  • Ensure RDS Proxy has correct IAM role permissions
  • Check NLB target health status
  • Validate DNS resolution if using Route 53
  • Review CloudWatch logs for connection errors

Additional Resources

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article