Skip to content

Port forwarding to private infrastructure with AWS SSM or SSH

8 Jan 2023Series: How does CloudGlance do it
Written by: Rehan van der Merwe

AWS SSM vs SSH

The AWS recommend method of port forwarding is to use AWS Session Manager (AWS SSM) which is more secure than SSH. AWS SSM allows us to place the bastion host (also known as a jump host) in a private subnet with no open inbound ports (rules in the security group). This is great for your security footprint as there are no publicly available entry points.

AWS SSM also uses IAM authentication which makes it easier to manage authentication if you are already managing users via IAM. To use AWS SSM, you need to install the AWS CLI and the session-manager-plugin locally on your machine. You also need to install the SSM Agent version > 3.1.1374.0 on the EC2 bastion host. Most AMIs (like Amazon Linux 2) already have the Agent installed but last time I checked, the installed version is less than the required version and needs updating. It is easier to update the agent with AWS SSM by using the AWS-UpdateSSMAgent document than doing it manually.

AWS SSM

SSH on the other hand requires your bastion host to be in the public subnet with a public IP and open port for SSH. This increases your surface of attack, anything available on the internet is vulnerable to port enumerations, brute force attacks, DOS attacks and more. It is also more work to manage physical Linux users with their .pem certificates on the bastion host itself.

AWS SSH

SSH has an advantage over AWS SSM in that it is easier to use and you do not have to create an AWS IAM user for an external user that needs access to your private subnet. Creating and restricting the external user via IAM policies is not something to be taken lightly, which makes SSH authentication via a username and .pem certificate a much more attractive option.

On the other hand, SSH can also be misconfigured. Some common mistakes are:

  • Not whitelisting the IPs of the clients that are allowed to connect.
  • Using passwords instead of .pem certificates.
  • If the above two are not implemented then:
    • SSH must certainly not be on port 22.
    • Not using fail2ban to dynamically block clients that repeatedly fail to authenticate correctly.

To summarize:

AWS SSMSSH
Server PlacementPrivate subnetPublic subnet
Has public IPNoYes
AuthenticationIAM.pem Certificate
Well-known and easy to use as a clientNoYes
MaintenanceMinimumModerate
SetupModerateModerate

The difference between port forwarding and remote port forwarding

AWS SSM has supported port forwarding on the instance that you are connecting for quite a while now. But if you wanted to port forward to a remote host, you had to jump through multiple complicated hoops. This is why in my honest opinion it never saw widespread adoption.

It was only until May 2022 that AWS announced port forwarding to remote hosts and it made life so much easier.

AWS SSM Port forwarding

Port forwarding to the EC2 bastion instance you are connecting to. This maps your local port to the port on the EC2 instance.

Port forwarding

Script from the AWS docs.

shell
aws ssm start-session \
    --target instance-id \
    --document-name AWS-StartPortForwardingSession \
    --parameters '{"portNumber":["80"], "localPortNumber":["56789"]}'

AWS SSM Port forwarding to remote host

Port forwarding through the EC2 bastion instance you are connecting to. This maps your local port to the RDS instance that is only available in the private subnet.

Port forwarding to remote host

Script from the AWS docs.

shell
aws ssm start-session \
    --target instance-id \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters '{"host":["mydb.example.us-east-2.rds.amazonaws.com"],"portNumber":["3306"], "localPortNumber":["3306"]}'

Code snippets

AWS SSM

Unfortunately, we can not start a single AWS SSM session that port forwards to multiple remotes. We need to start 1 process per port forward, which even more unfortunately uses quite a bit of memory per process.

Available on GitHub: ssm-port-forward.js

javascript
#!/usr/bin/env node
const shell = require('shelljs');

function tunnelToArg(tunnel) {
  return ` --parameters "{\\"host\\":[\\"${tunnel.remoteAddress}\\"],\\"portNumber\\":[\\"${tunnel.remotePort}\\"], \\"localPortNumber\\":[\\"${tunnel.localPort}\\"]}"`;
}

async function main()
{
  const src = {
    ec2InstanceId: "your-ec2-instance-id",
    profile: "your-aws-profile",
    tunnels: [
      {
        name: "RDS Dev",
        localPort: 5435,
        remoteAddress: "your-rds.eu-west-1.rds.amazonaws.com",
        remotePort: 5432,
      },
      {
        name: "RDS Dev Proxy",
        localPort: 5436,
        remoteAddress: "your-rds-proxy.eu-west-1.rds.amazonaws.com",
        remotePort: 5432,
      }
    ],
  };

  for(let tunnel of src.tunnels)
  {
    const command = `aws ssm start-session --profile ${src.profile} --target ${src.ec2InstanceId} --document-name AWS-StartPortForwardingSessionToRemoteHost ` + tunnelToArg(tunnel);
    // console.log(command);

    shell.exec(command, {async: true});
    console.log(`> Port forward open, you can access ${tunnel.name} on 'localhost:${tunnel.localPort}'`);
  }
}
main().catch(err => console.error(err));

The resulting command variable that is being built up, and logged in the loop, to be executed is as follows:

aws ssm start-session --profile your-aws-profile --target your-ec2-instance-id \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"your-rds.eu-west-1.rds.amazonaws.com\"],\"portNumber\":[\"5432\"], \"localPortNumber\":[\"5435\"]}"

aws ssm start-session --profile your-aws-profile --target your-ec2-instance-id \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"your-rds-proxy.eu-west-1.rds.amazonaws.com\"],\"portNumber\":[\"5432\"], \"localPortNumber\":[\"5436\"]}"

AWS SSH

With SSH we can start a single process with all our port forward tunnels, this is also more memory efficient than the AWS SSM equivalent.

Available on GitHub: ssh-port-forward.js

javascript
#!/usr/bin/env node
const shell = require('shelljs');

function tunnelToArg(tunnel) {
  return ` -L ${tunnel.localPort}:${tunnel.remoteAddress}:${tunnel.remotePort}`
}

async function main()
{
  const src = {
    host: "your-ec2-instance-dns-name-or-ip",
    port: 22,
    cert: "path-to-your-.pem-cert-for-the-instance",
    username: "ec2-user",
    tunnels: [
      {
        name: "RDS Dev",
        localPort: 5435,
        remoteAddress: "your-rds.eu-west-1.rds.amazonaws.com",
        remotePort: 5432,
      },
      {
        name: "RDS Dev Proxy",
        localPort: 5436,
        remoteAddress: "your-rds-proxy.eu-west-1.rds.amazonaws.com",
        remotePort: 5432,
      }
    ],
  };

  let command = [
    `ssh -o StrictHostKeyChecking=no ${src.username}@${src.host} -i "${src.cert}"`,
  ];

  for(let tunnel of src.tunnels)
    command.push(tunnelToArg(tunnel));

  command = command.join(" ");

  // console.log(command);
  shell.exec(command, {async: true});

  for(let tunnel of src.tunnels)
    console.log(`> Port forward open, you can access ${tunnel.name} on 'localhost:${tunnel.localPort}'`);
}
main().catch(err => console.error(err));

The resulting command variable that is being built up, and logged, to be executed is as follows:

ssh -o StrictHostKeyChecking=no ec2-user@your-ec2-instance-dns-name-or-ip -i "your-.pem-cert-for-the-instance" \
-L 5435:your-rds.eu-west-1.rds.amazonaws.com:5432  \
-L 5436:your-rds-proxy.eu-west-1.rds.amazonaws.com:5432

Conclusion

There are many ways to securely connect to your private infrastructure in AWS. Weigh the options and choose the best solution for your situation, but my recommendation, same as AWS, is to always go for the AWS SSM bastion when possible.

Working with the CLI and custom scripts to automate bastion port forwarding for many clients can be a headache.

That is why Cloud Glance puts all your port forwarding connections in a single pane of glass. Not only does it support AWS SSM and SSH, but it visualizes what ports are in use to prevent starting multiple sessions whose ports clash.

Single pane of glass

Creating SSM

Check it out here: https://cloudglance.dev/