Skip to main content
Current2d ago

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

RouteEC2 needed?CostBest for
SSM Port ForwardingA tiny instance (can be stopped when idle)~$0 when stopped; ~$3/mo if left on 24/7Occasional manual querying
EC2 Instance Connect Endpoint (EIC)NoneNo 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 configure or SSO).

  • The Session Manager plugin for the AWS CLI:

    • macOS: brew install --cask session-manager-plugin
    • Or follow AWS docs for your OS.
  • 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 port 5432.

  • The RDS security group allows inbound 5432 from 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 AmazonSSMManagedInstanceCore managed 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 5432 from 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.

Related Articles