SShing to unpredictable IPs behind a bastion

Feb 7, 2022

A pretty common security measure for cloud infrastructure is an SSH bastion: you have a bunch of servers you may need to SSH to at times, but you obviously don’t want all your servers open to the public Internet, so you designate one server as the SSH bastion, which can be appropriately hardened and is open to public SSH connections for proxying ssh connections into your private network.

I recently found myself needing to manage multiple environments (production and staging) like this, and transitioning a legacy configuration with a small number of static servers to a “cattle, not pets” approach where individual servers were coming and going all the time. Under the old system, because the list of servers was static ops people would give each server a DNS entry pointing at the private IP and share snippets for everyone to put in their ~/.ssh/config: a manual process that worked fine when servers were manually configured and long-lived but not one that fits comfortably in a world where the number of servers may be much higher and individual servers are being automatically provisioned or decommisioned all the time.

I could have also built automatic DNS assignments to the new servers and chosen predictable entries such that a wildcard Host block in ssh_config could match everything, but I was hoping to find a simpler approach. I think I’ve landed on a couple of lines in my ~/.ssh/config that will do what I need:

Match originalhost bastion.staging
  User myuser
  Port 12345 # publicly exposed servers shouldn't use default SSH port
  Hostname my.staging.bastion.domain

# ssh <private-address>.staging to hop via bastion
Match host *.staging
  User myuser
  ProxyCommand ssh bastion.staging -W $(echo "%h" | sed s/.staging//):%p

With this, to ssh to a server in the staging environment with IP, I can type ssh The same two blocks reproduced with the .prod suffix instead of .staging handle production.

This does mean remembering to add the suffix after copy-pasting a private IP and knowing which environment I’m targeting, so it’s not quite as smooth or idiot-proof as DNS entries would be. For a much larger infrastructure that extra effort would probably be worthwhile, but for the scale I expect this system to operate at for the forseeable future I think what I’ve landed on is a simpler and easier solution.