AWS RDS Secure Connection From Local Terminal
A reference for connecting psql (or any Postgres client) to an RDS instance that is not publicly accessible — without exposing it to the internet.
Core idea: RDS only speaks the Postgres protocol, not SSH. So a tunnel always terminates on something inside the VPC that can reach RDS. You then point psql at localhost, and the tunnel ships those packets through to RDS.
You connect to localhost, never to the RDS endpoint directly.
Two routes
| Route | EC2 needed? | Cost | Best for |
| SSM Port Forwarding | A tiny instance (can be stopped when idle) | ~$0 when stopped; ~$3/mo if left on 24/7 | Occasional manual querying |
| EC2 Instance Connect Endpoint (EIC) | None | No hourly endpoint charge | "No instance to babysit" setup |
Pick one. Both end with psql -h localhost.
Prerequisites (both routes)
-
AWS CLI v2 installed and configured (
aws configureor SSO). -
The Session Manager plugin for the AWS CLI:
- macOS:
brew install --cask session-manager-plugin - Or follow AWS docs for your OS.
- macOS:
-
Your IAM principal has permission for SSM / EC2 Instance Connect actions.
-
You know your RDS endpoint, e.g.
mydb.xxxx.us-east-1.rds.amazonaws.com, and port5432. -
The RDS security group allows inbound
5432from whatever sits inside the VPC (the bastion's SG, or the EIC endpoint's SG).
Route A — SSM Port Forwarding (via a stopped-when-idle bastion)
1. Launch a minimal instance (once)
-
Type:
t4g.nano(cheapest; Arm). -
Place it in a subnet in the same VPC as RDS (private subnet is fine).
-
No public IP, no SSH, no inbound ports needed.
-
Attach an IAM instance profile that includes the
AmazonSSMManagedInstanceCoremanaged policy. -
Use a recent Amazon Linux 2023 AMI (SSM agent preinstalled). Confirm it registers with SSM:
aws ssm describe-instance-information \
--query "InstanceInformationList[].InstanceId"
Your instance ID should appear. If it doesn't, the IAM role or subnet routing to the SSM endpoints is the usual culprit.
2. Allow the bastion to reach RDS
In the RDS security group, add an inbound rule: Postgres / TCP 5432, source = the bastion's security group.
3. Start the tunnel
aws ssm start-session \
--target i-0abc123yourinstance \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.xxxx.us-east-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
Leave this terminal open — it holds the tunnel.
4. Connect
psql -h localhost -p 5432 -U myuser -d mydbname
5. Stop the instance when done (this is the cost trick)
aws ec2 stop-instances --instance-ids i-0abc123yourinstance
A stopped instance costs $0 for compute. You only pay a few cents/month for its small EBS root volume. Start it again next time:
aws ec2 start-instances --instance-ids i-0abc123yourinstance
Route B — EC2 Instance Connect Endpoint (no instance at all)
This creates an endpoint ENI in a private subnet that you tunnel through. There is no instance to run or stop.
Note: EIC Endpoint's exact support for forwarding to non-EC2 hosts like RDS has been evolving. Verify the current behavior in the AWS docs for your region before relying on it. The shape below reflects the general approach.
1. Create the endpoint (once)
-
Create an EC2 Instance Connect Endpoint in a subnet in the same VPC as RDS.
-
Give it a security group that is allowed outbound to RDS on
5432. -
In the RDS security group, allow inbound
5432from the EIC endpoint's security group.
2. Open the tunnel
aws ec2-instance-connect open-tunnel \
--instance-connect-endpoint-id eice-0abc123 \
--private-ip-address <rds-private-ip-or-host-target> \
--remote-port 5432 \
--local-port 5432
Leave it open; it holds the tunnel. (Exact flags depend on the current CLI version — check aws ec2-instance-connect open-tunnel help.)
3. Connect
psql -h localhost -p 5432 -U myuser -d mydbname
Common gotchas
Local port already in use. If you run Postgres locally, 5432 is taken. Forward to a different local port and tell psql:
# in the tunnel command, set localPortNumber / --local-port to 5433, then:
psql -h localhost -p 5433 -U myuser -d mydbname
SSL / certificate errors. RDS often enforces SSL. Through the tunnel your client sees the host as localhost, which can fail strict cert hostname verification. Use sslmode=require to encrypt without verifying the hostname:
psql "host=localhost port=5432 dbname=mydbname user=myuser sslmode=require"
For full verification instead, download the RDS CA bundle and use sslmode=verify-full with sslrootcert= — only worth it if you need it.
Passwords. The password psql prompts for is the Postgres user's password, separate from your AWS credentials. (If the DB uses IAM auth, you'd generate a short-lived token with aws rds generate-db-auth-token instead.)
Tunnel drops. The forwarding session can time out or drop on network changes. Just restart the tunnel command.
Quick mental model
psql -> localhost:5432 -> [tunnel] -> RDS:5432
^
SSM bastion OR EIC endpoint
(the thing inside the VPC)
You always talk to localhost. The tunnel is the only moving part.