SSH Tunneling with Ruby

We may want to perform some sort of update on a remote system using a programming language or we might want to check a status on a remote machine as part of monitoring.  Using Ruby with the net-ssh-gateway gem, we can perform a variety of operations on remote machines and use Ruby logic to implement a DevOps feature.

A Bastion

One common use case for tunneling through one machine to reach another is when we implement a Bastion host in a public network that will be used to access private hosts in a private subnet.  For AWS implementations, this is a common pattern:

https://aws.amazon.com/blogs/security/securely-connect-to-linux-instances-running-in-a-private-amazon-vpc/

One goal of a Bastion, for the purposes of providing an SSH conduit to private instances, is to not contain any secret information such as SSH key files and exist in a DMZ.

When we tunnel through the Bastion manually, we provide the SSH key file that satisfy the Bastion and the private instance we are trying to reach.

Download Example Code

https://github.com/mikejmoore/ruby-ssh-tunneling

Setting Up A Bastion in AWS

You will need to create a configuration

> cd aws_test_environment/
> terraform apply

This will create a bastion and a host in a private subnet.  When the instances finish instantiating, we can test our tunneling.

SSH commands with a terminal can forward their SSH key to the Bastion, without saving it in a file with a -A flag:

> ssh-add -K {path-to-your-keys-pem-file}
> ssh -A -i [email protected] ssh {private-host-address}

We want to perform the same tunneling with Ruby and even perform remote tasks on the target system.

Ruby Tunneling

> cd ./ruby_ssh_tunneling
> ruby main.rb

I have tunneled through 54.191.5.134 to open an SSH session in 10.0.47.136
My user name is: ec2-user
I'm logged into host: ip-10-0-47-136
My home directory is: /home/ec2-user

This is the code that establishes a connection to a bastion as a gateway:

  gateway = Net::SSH::Gateway.new(params[:bastion_ip_address], params[:ssh_user],
        port: bastion_port,
        key_data: params[:keys],
        keys_only: true,
        forward_agent: true,
        verify_host_key: IGNORE_KNOWN_HOST_VALUE)

The key_data parameter specifies the private keys that will be used for authentication.  The forward_agent parameter tells the gateway to keep these private keys in memory to use with the next jump to a specific private instance:

  gateway.open(params[:private_host_ip_address], private_host_ssh_port) do |gateway_port|
    ssh = gateway.ssh(params[:private_host_ip_address], params[:ssh_user],
          key_data: params[:keys],
          port: private_host_ssh_port,
          keys_only: true,
          verify_host_key: IGNORE_KNOWN_HOST_VALUE)

 

All the information is gathered by establishing an SSH session on the private instance and executing Linux commands within that session.

This example does not show the true power of using SSH tunneling into a private instance through a Bastion with a programming language such as Ruby; just how to perform the tunneling.

With this tunneling, there are infinite possibilities of tasks that could benefit from being backed by programming logic.  Examples include:

  • Perform configurations on remote instances.
  • Execute scripts with calculated parameters on remote instances.
  • Monitoring of services on remote instances and proper handling of findings.

The point is that when we want to perform automation of systems, many times we need programming logic combined with tasks performed on remote systems. Ruby is an excellent language for DevOps communication and logic.

Leave a Reply

Your email address will not be published. Required fields are marked *