Skip to content
enes
2026-04-22engineering3 min read

Tunneling to a private RDS without a bastion.

Reach a private RDS instance from your laptop without a bastion EC2. SSM Session Manager port forwarding does it in one command.

The classic answer to "how do I run a migration against a private RDS from my laptop" is to spin up a bastion EC2, open port 22 to your IP, push an SSH key, and tunnel:

ssh -N -L 5433:<rds-endpoint>:5432 bastion-prod

It works. It's also wasteful: a public-facing EC2 you keep alive forever for migrations that happen once a quarter, plus the SSH keys and security group rules that come with it.

If your account is on AWS Systems Manager (SSM) and you have any EC2 with the SSM agent in the same VPC as the RDS, you don't need a bastion. SSM Session Manager has a port forwarding mode that tunnels TCP through that EC2 in one command.

The command

aws ssm start-session \
  --target <ec2-instance-id> \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{
    "host": ["<rds-endpoint>"],
    "portNumber": ["5432"],
    "localPortNumber": ["5433"]
  }'

--target is any EC2 in the VPC that already has network reach to the database. host is the RDS endpoint. The session opens a TCP tunnel from localhost:5433 on your machine to that endpoint, routed through the EC2's network. The EC2 is just a hop, it sees the connection but doesn't touch the protocol.

Run the session, leave it open in one terminal, and from another terminal point your tooling at localhost:5433. Connection string, swap host and port, keep user and password and database name. Run your migration, your psql, your dump, whatever.

When you Ctrl-C the session, the tunnel closes. No cleanup, no orphan rules.

What you need on the AWS side

Three things, all standard:

  1. The target EC2 has the SSM agent running. On Amazon Linux 2 / AL2023 / Ubuntu it's installed by default these days; on older AMIs you install it once.
  2. The target EC2's instance role includes AmazonSSMManagedInstanceCore (or the subset of permissions it grants).
  3. Your IAM principal (the one your aws CLI is authenticated as) has ssm:StartSession on that target.

That's it. No public IP on the EC2, no inbound port open, no SSH key.

What the SSH-bastion approach hides that you stop paying for

  1. A bastion EC2 with public exposure. SSM has none. The session is initiated from your local machine, the AWS API does the routing.
  2. SSH keys to manage and rotate. SSM auth piggybacks on your existing IAM identity. No keys to push, lose, or audit.
  3. A firewall rule that survives the migration. The SSM tunnel closes when the session ends. There's no window where the path stays open after you stopped using it.

Audit trail

Every start-session call logs to CloudTrail with the IAM principal, the target instance, the session ID, and the duration. You get a paper trail by default that the SSH-bastion model doesn't give you cleanly. If your team ever needs to answer "who connected to prod RDS last Tuesday?", CloudTrail has the answer in one query.

When SSM is the wrong tool

A few cases where the bastion model still makes sense:

  • Your account doesn't run any EC2 with the SSM agent. Pure-Lambda or pure-Fargate setups won't have a target. (Fargate can run with the SSM agent too, but it's less common.)
  • You need long-running tunnels measured in hours, not minutes. SSM session timeouts default to 20 minutes idle; configurable but not unlimited.
  • You need non-IAM identity for the operator (e.g., contractors without AWS access). A bastion with SSH keys is sometimes simpler than provisioning IAM.

For everything else, the SSM tunnel pattern is shorter, safer, and audited by default.

The shape that survives

The reflex when you can't reach a private resource is to add infrastructure. The better default, when you're already on AWS, is to ask whether SSM can route the connection without adding anything. Most of the time it can.

One command, one EC2 already running, a CloudTrail entry per session. The bastion you don't have is the bastion you don't need to operate.