Securely Accessing EC2 Windows Instances via SSH Port Forwarding
Reaching an EC2 Windows instance in a private subnet through an SSH bastion host, keeping RDP off the public internet entirely.
Placing an EC2 Windows instance in a private subnet and accessing it through a bastion host in a public subnet is a common pattern for securing remote desktop access without exposing the instance directly to the internet.
This method secures only the connection between the client and the bastion host. No encryption is applied between the bastion host and the EC2 Windows instance.
The overall architecture is illustrated below. The client opens an SSH session to the bastion host in the public subnet, and that session carries the RDP traffic to the Windows instance in the private subnet, which has no route to the internet gateway. Because the SSH encryption terminates at the bastion host, the forwarded segment inside the VPC travels as ordinary RDP traffic.
Setting Up the AWS Environment
VPC
Create a private subnet within the target VPC. This step can be skipped if a suitable subnet already exists.

Create a route table and associate it with the private subnet.

Ensure the internet gateway is removed from this route table; its presence would render the subnet effectively public.

If the EC2 Windows instance requires internet access, create a NAT gateway in a public subnet and attach it to the route table.
Running a NAT gateway incurs additional costs.


SSH Bastion Host
- Launch an EC2 instance in the public subnet to serve as the bastion host.
- Configure the security group to allow inbound traffic on ports 22 (SSH) and 3389 (RDP).
Assigning an Elastic IP (EIP) makes access to the bastion host easy.
EC2 Windows Instance
- Launch an EC2 Windows instance in the private subnet.
- Retrieve the remote desktop credentials using the
Get Windows Passwordoption in the EC2 dashboard. - Restrict security group access to allow inbound traffic on ports 22 and 3389 only from the bastion host.

Testing the Connection
To establish a secure connection to the EC2 Windows instance, execute the following command from a local terminal:
ssh -i <YOUR_PRIVATE_KEY> -L 13389:<YOUR_EC2_WINDOWS_IP>:3389 ec2-user@<YOUR_SSH_BASTION_IP>This command forwards traffic from local port 13389 to the EC2 Windows instance in the private subnet via the bastion host. A remote desktop session can then be initiated by connecting to localhost:13389.


Conclusion
Placing the EC2 Windows instance in a private subnet without a route to an internet gateway, then accessing it through an SSH bastion host in a public subnet, keeps RDP off the public internet. The security group on the private instance is essential: restricting traffic on ports 22 and 3389 to the bastion host, rather than allowing a broader CIDR range, enforces this isolation.
The ssh -L 13389:<EC2_WINDOWS_IP>:3389 option tunnels the RDP session through the bastion host, allowing localhost:13389 on the client machine to reach an instance that cannot be accessed directly from outside the VPC.
Because the SSH tunnel covers only the connection between the client and the bastion host, the bastion must be carefully secured. Restrict its security group to known administrative source addresses and, if necessary, assign an EIP to give the bastion a stable address.
Related posts
Connecting to EC2 with Session Manager (No SSH Required)
Opening a shell on an EC2 instance through Systems Manager's Session Manager, with no SSH keys, bastion host, or open port 22 required.
Debugging PHP Remotely on AWS EC2 with PhpStorm and Xdebug
Remote debugging a PHP application on EC2 with PhpStorm and Xdebug, from the server-side ini settings to the IDE's path mapping.
Running Proxy.py as a Lightweight HTTP Proxy on EC2
Running Proxy.py on an EC2 instance and reaching it safely through an SSH tunnel, since the proxy has no authentication of its own.
Sign in with Slack Using Cognito User Pools and OIDC
Federating Cognito user pools with Slack over OIDC and wiring "Sign in with Slack" into a Next.js app with Amplify.
Deploying FastAPI on AWS Lambda with Lambda Web Adapter
Containerizing a FastAPI backend and deploying it to a single Lambda function with Lambda Web Adapter and AWS CDK.
