The bastion is usually the least defensible thing in the account. It has a public IP, an open SSH port, a set of keys that have been copied around for years, and it exists so that two or three people can occasionally run a query against a database that is otherwise perfectly well hidden.
Systems Manager removes the reason for it. Session Manager can forward a port on your laptop through a managed node to any host that node can reach on the network, and the database is one such host. The node has no inbound rules, no public IP and no SSH keys, because the agent on it dials out rather than listening.
What replaces the jump host
You still need one piece of compute inside the VPC. What changes is everything about it. No inbound security group rule, no key pair, no public address, no user accounts, and access controlled by IAM rather than by who has a copy of a private key.
The remote host does not have to be managed by Systems Manager, which is what makes this work for RDS. AWS states that plainly for the port forwarding document, and the only requirement on the RDS side is ordinary network reachability and name resolution from the node.
If you already run ECS, you may not need a new instance at all. Session Manager can target a task directly when ECS Exec is enabled on it, so an existing task can act as the hop.
The prerequisites, in the order they bite
SSM Agent version 3.1.1374.0 or later on the node. That is the minimum for port forwarding to a remote host, and it is higher than the minimum for a plain session. Amazon Linux and recent Ubuntu images ship the agent, but an old AMI will fail here first.
An instance profile with Session Manager permissions. The AmazonSSMManagedInstanceCore managed policy contains everything required.
The Session Manager plugin on your own machine. The AWS CLI cannot run session commands without it, and the error message when it is missing is not obvious.
A security group path from the node to the database. The database security group has to allow the node's security group on the database port. This is the one firewall rule the whole design still needs.
If there is no NAT gateway, add three endpoints
The agent has to reach the Systems Manager service. Where the node sits in a private subnet with no route to the internet, that means interface VPC endpoints.
for SVC in ssm ssmmessages ec2messages; do
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123 \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.eu-central-1.$SVC \
--subnet-ids subnet-0aaa111 subnet-0bbb222 \
--security-group-ids sg-0endpoint \
--private-dns-enabled
done
The security group on those endpoints must allow inbound 443 from the private subnet where the node lives. Skip that and the node simply never registers, with no error anywhere obvious.
From SSM Agent version 3.3.40.0 onwards the agent prefers the ssmmessages endpoint over ec2messages wherever it is available, so a modern fleet leans on the first two. Creating all three costs nothing extra in complexity and keeps older nodes working.
The command
aws ssm start-session \
--target i-1234567890abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.example.eu-central-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
host is the database endpoint, portNumber is the port on the remote host, and localPortNumber is what opens on your machine. Leave portNumber out and Session Manager defaults to 80, which is almost never what you meant. Windows shells want the parameters in key="value" form rather than JSON, because quoting rules differ.
Leave that running in one terminal and connect in another as if the database were local.
psql -h 127.0.0.1 -p 5432 -U app_user -d appdb
For a task instead of an instance, the target takes a composite form.
aws ssm start-session \
--target ecs:my-cluster_my-container-id_my-container-runtime-id \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.example.eu-central-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
That path also needs ssmmessages:CreateControlChannel, ssmmessages:CreateDataChannel, ssmmessages:OpenControlChannel and ssmmessages:OpenDataChannel on the task role.
Permissions for the person, not just the machine
Scope ssm:StartSession to specific instances rather than granting it broadly. AWS's own sample policies use the instance ARN as the resource, and pair it with a rule that lets someone end only their own sessions.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:StartSession"],
"Resource": "*",
"Condition": {
"StringLike": { "ssm:resourceTag/Role": ["db-access"] }
}
},
{
"Effect": "Allow",
"Action": ["ssmmessages:OpenDataChannel"],
"Resource": ["arn:aws:ssm:*:*:session/${aws:userid}-*"]
},
{
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": ["arn:aws:ssm:*:*:session/${aws:userid}-*"]
}
]
}
A tag condition scales better than a list of instance IDs, since tagging one new node grants access without anyone editing a policy. Session documents can also be named as resources in the policy, which is how you allow shell sessions and port forwarding to different groups of people.
Drop the stored database password too
Having removed the SSH key, the password in someone's connection string is the next credential worth deleting. RDS supports IAM database authentication for MariaDB, MySQL and PostgreSQL, where the password is a signed token that expires after fifteen minutes.
aws rds modify-db-instance \
--db-instance-identifier mydb \
--enable-iam-database-authentication \
--apply-immediately
export RDSHOST="mydb.example.eu-central-1.rds.amazonaws.com"
export PGPASSWORD="$(aws rds generate-db-auth-token \
--hostname $RDSHOST --port 5432 --region eu-central-1 --username jane_doe)"
Two documented restrictions shape how you use it. The token must be generated against the DB instance endpoint itself, because a custom Route 53 record will not work. And IAM database authentication needs between 300 and 1000 MiB of extra memory on the instance, which is worth knowing before you enable it on a burstable class.
There is a trade-off in the psql connection too. sslmode=verify-full checks the endpoint you connected to against the certificate the database presents, so connecting to 127.0.0.1 through the tunnel and demanding verify-full will fail on the hostname. Map the real endpoint name to 127.0.0.1 in your hosts file if you want both the tunnel and full certificate verification.
Two things this does not give you
Session logging does not cover port forwarding or SSH sessions. AWS is explicit that the data inside the tunnel is encrypted end to end between the CLI and the Session Manager endpoints, and Session Manager is only carrying it. You will see that a session started, from CloudTrail, but not what was run inside it. If you need query-level auditing, that has to come from the database.
The other is that this is a human access path, not an application one. An application inside the VPC should reach the database directly, with credentials from a task or pod role.
If a public bastion is currently the only route into your database and you would rather have it removed than documented, that work sits inside our AWS cloud management service, and the access review around it is part of security and compliance.
Talk to the engineer who will own your stack.
No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.
Related Articles
AWS Cost Optimization: 10 Things You're Probably Overpaying For
Ten common areas where AWS customers overspend, with practical strategies for right-sizing, reserved capacity, storage lifecycle management, and more.
CloudCloudflare Tunnel vs AWS ALB: When to Use Which
An architecture comparison of Cloudflare Tunnel and AWS Application Load Balancer, covering cost, DDoS protection, SSL termination, latency, and setup complexity.
CloudAWS Cost Optimization Strategies for Growing SaaS
Reduce your AWS bill by 30-50% with Reserved Instances, Spot Fleets, right-sizing, and architectural patterns designed for cost-efficient SaaS growth.