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:
Hostis the name I’ll use to identify the connection.HostNameis the actual address of the server, either an IP address or a domain.Useris the user I want to connect as.IdentityFilespecifies 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 rsaspecifies that we want to generate an RSA key.-b 4096sets the key size to 4096 bits.-f ~/.ssh/productionspecifies 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
scpandrsynccould 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.