8 min read
How I manage my SSH connections with ~/.ssh/config

Until recently, the way I managed my SSH connections was pretty rudimentary.

For starters, I had the bad habit of using the same SSH key for almost all of my servers.

Something like:

ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100

Since I also didn’t want to memorize IP addresses, usernames, and full commands, my solution was to create aliases directly in my .bashrc:

alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
alias staging="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.200"
alias personal-server="ssh -i ~/.ssh/id_rsa root@203.0.113.10"

That way, I could simply run:

production

And technically, it worked.

But over time, I started running into several problems with this approach.

My dotfiles

I currently keep my dotfiles under version control with Git so I can maintain and reuse my configuration across different machines.

That includes my .bashrc.

And that’s where the problem came in. By defining my SSH connections inside those aliases, they would also become part of my Git repository.

I didn’t want to unnecessarily publish the IP addresses of my servers. Not because they were secret or because hiding them was a security measure, but simply because I didn’t want to keep that information somewhere it didn’t belong.

My first idea was to find a way to separate those aliases from my .bashrc.

Basically, I wanted a local file containing my SSH connections that I could keep outside of my repository. While looking for a way to do that, I realized I was trying to solve a problem that SSH had already solved.

I didn’t need another file full of aliases on my machine. I needed to use the ~/.ssh/config file that SSH already provides for exactly this purpose.

Discovering ~/.ssh/config

SSH lets us define specific configurations for our connections directly in:

~/.ssh/config

For example, one of my aliases:

alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"

could become:

Host production
    HostName 192.168.1.100
    User ubuntu
    IdentityFile ~/.ssh/production

Now I can connect by simply running:

ssh production

production works like an alias for that connection, but unlike my Bash alias, SSH itself now knows about the configuration.

Each option has a specific purpose:

  • Host is the name I’ll use to identify the connection.
  • HostName is the actual address of the server, either an IP address or a domain.
  • User is the user I want to connect as.
  • IdentityFile specifies which private key I want to use.

We can configure other options as well.

For example, if our server uses a port other than the default port 22:

Host production
    HostName 192.168.1.100
    User ubuntu
    Port 2222
    IdentityFile ~/.ssh/production

We can still connect in exactly the same way:

ssh production

A different key for each server

Discovering ~/.ssh/config also made me change another bad habit: using the same SSH key across different servers.

Before, I could have something like this:

alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
alias staging="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.200"
alias personal-server="ssh -i ~/.ssh/id_rsa root@203.0.113.10"

They all used id_rsa.

If that key were ever compromised, I would have to replace it everywhere I was using it.

Instead, we can generate a different key for each server.

Generating a key with Ed25519

For new keys, Ed25519 is a modern option. We can generate a key for our production server with:

ssh-keygen -t ed25519 -f ~/.ssh/production

With -t ed25519, we specify the type of key we want to generate, while -f ~/.ssh/production defines where we want to store it.

After running the command, we’ll have two files:

~/.ssh/production
~/.ssh/production.pub

production contains our private key. This is the file we’ll later reference with IdentityFile, and it should remain private on our machine.

production.pub contains our public key, which is the one we can add to the server we want to connect to.

Generating a key with RSA

We can also use RSA. For example, we can generate a 4096-bit RSA key with:

ssh-keygen -t rsa -b 4096 -f ~/.ssh/production

In this case:

  • -t rsa specifies that we want to generate an RSA key.
  • -b 4096 sets the key size to 4096 bits.
  • -f ~/.ssh/production specifies where we want to store it.

Just like with Ed25519, we’ll get a private and a public key:

~/.ssh/production
~/.ssh/production.pub

For new keys, Ed25519 is generally a good option because of its compact key size and strong security. RSA is still useful when we need compatibility with systems that don’t support Ed25519.

Regardless of the type of key we choose, we can repeat the process for each of our servers:

ssh-keygen -t ed25519 -f ~/.ssh/production
ssh-keygen -t ed25519 -f ~/.ssh/staging
ssh-keygen -t ed25519 -f ~/.ssh/personal-server

And then configure each one in ~/.ssh/config:

Host production
    HostName 192.168.1.100
    User ubuntu
    IdentityFile ~/.ssh/production

Host staging
    HostName 192.168.1.200
    User ubuntu
    IdentityFile ~/.ssh/staging

Host personal-server
    HostName 203.0.113.10
    User root
    IdentityFile ~/.ssh/personal-server

This limits the impact if one of those keys is ever compromised and also makes it much easier to revoke or replace access to a specific server.

And from my perspective, connecting remains just as simple:

ssh production
ssh staging
ssh personal-server

We can also use .pem files

IdentityFile isn’t limited to the keys we typically find inside ~/.ssh, such as id_rsa or id_ed25519. We can also use .pem files.

A common example is AWS EC2.

When we create a key pair to connect to an EC2 instance, AWS lets us download the private key as a .pem file.

Normally, we might connect to an instance with a command like this:

ssh -i ~/.ssh/my-ec2-server.pem ubuntu@203.0.113.10

The user will depend on the image we’re using. In this example, we’re assuming an Ubuntu-based instance.

With ~/.ssh/config, we can move all of that information into our configuration:

Host my-ec2-server
    HostName 203.0.113.10
    User ubuntu
    IdentityFile ~/.ssh/my-ec2-server.pem

And now we can simply run:

ssh my-ec2-server

For SSH, the .pem extension isn’t really what’s important. In this case, the file still contains our private key, and IdentityFile simply tells SSH where to find it.

We can even mix different types of files in our configuration:

Host personal-server
    HostName 192.168.1.100
    User deploy
    IdentityFile ~/.ssh/personal-server

Host aws-production
    HostName 203.0.113.10
    User ubuntu
    IdentityFile ~/.ssh/aws-production.pem

And connect to them in exactly the same way:

ssh personal-server
ssh aws-production

One important detail when working with .pem files downloaded from AWS is making sure they have sufficiently restrictive permissions:

chmod 400 ~/.ssh/aws-production.pem

Otherwise, SSH may refuse to use the private key because its permissions are too open.

It doesn’t only work with ssh

Another advantage over my .bashrc aliases is that this configuration isn’t limited to directly running the ssh command.

My aliases only existed inside my shell. If I wanted to copy a file using scp, I had to write the entire connection again:

scp -i ~/.ssh/id_rsa ./backup.sql ubuntu@192.168.1.100:/tmp/backup.sql

With ~/.ssh/config, scp can use the same host:

scp ./backup.sql production:/tmp/backup.sql

The same applies to rsync:

rsync -av ./dist/ production:/var/www/app/

And to other tools that use SSH under the hood.

The configuration stops being just a shortcut for typing less in the terminal and becomes a centralized place to define how I connect to each server.

Sharing configuration between multiple servers

SSH also lets us avoid repeating configuration when multiple servers share the same options.

For example, if production and staging use the same user:

Host production staging
    User ubuntu

Host production
    HostName 192.168.1.100
    IdentityFile ~/.ssh/production

Host staging
    HostName 192.168.1.200
    IdentityFile ~/.ssh/staging

We can also use wildcards:

Host *.company
    User ubuntu

Host api.company
    HostName 192.168.1.100
    IdentityFile ~/.ssh/api

Host worker.company
    HostName 192.168.1.200
    IdentityFile ~/.ssh/worker

And connect as usual:

ssh api.company

This can be especially useful once we start managing a larger number of servers.

I was solving the problem at the wrong layer

The funny thing is that I wasn’t initially looking for a better way to use SSH. I simply wanted to get my servers’ IP addresses out of my .bashrc so they wouldn’t end up being published along with my dotfiles.

But while looking for a solution, I discovered ~/.ssh/config, and it ended up solving several problems at once.

I went from:

alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"

to simply:

ssh production

My connections were no longer mixed in with my shell configuration, I could start using different keys for each server, and tools like scp and rsync could reuse the exact same configuration.

In the end, my .bashrc aliases were basically a homemade and much more limited version of something SSH had already solved.

And it all started because I didn’t want to push a few IP addresses to my dotfiles repository.