(Basic) SSH Keys Part 1: Simple setup + ProxyJump

Last year, curious, I asked a group of users and developers how they manage their SSH keys. Most people responded that they keep one unique set of keys per machine. Some have a work pair and a home pair. Some have keys specific to automated processes, or particular functions. Keys for common scheduled tasks are popular.

For my part, I have simply been using a single key on my laptop to reach much of the outside world. But how to go the other direction? Today, I'd like to add a keypair onto a machine in a datacenter where I have an account, so I can use that to reach a machine at my home from anywhere.

Here's what I hope is an approachable guide to get a beginner started with keys. We will create a set of keys to use first for a simple connection, then one where a third party is needed to access machines or access files from a private home network (like my folder of Fiona the Hippo baby pictures) from the outside world.

On Keypairs

We create pairs of public and private keys with ssh-keygen(1). By default these both live in ~/.ssh/ on your local machine. You should add the contents of the public key (only!) to the remote authorized_keys file (~/.ssh/authorized_keys) on any machines to which you plan to connect.

Everyone has a tortured metaphor for public key encryption, and here's mine:

Do you remember playing with "invisible ink" as a child? If you mail people a bottle 
of your special ink blend, you can go to one of them, introduce yourself, and ask them to
sign something for you using that ink. Only you know the secret formula, so only you will
know whether to hold the paper to a light bulb, brush grape juice over it or hold
it under a black light to make their signature visible. You can then show them their own 
signature as proof that you knew the secret.

In this metaphor, the remote server was never a child and is therefore very impressed by your wizardry, but you can read about how this actually works in RFC 4252 and friends.

When you connect to a remote server from your local machine, you present public key options to the server to compare with the set of keys they recognize. (Password authentication is your fallback, unless disabled). If a match is found, the remote machine will use the shared public key to randomly generate an encrypted message for your local private key to unlock. If you can decrypt it and present your answer, the remote server then knows you're using the correct private key. It would take it absolutely ages to determine what that private key is, but it will trust that the key is your own and grant you access.

The upshot of all this is that you can share your public key around however you like to gain access without needing a secure method to share a password, and you can memorize a passphrase connected to one key usable in multiple places, rather than individual passwords for each machine. Several people know my laptop's public key and can give me access to any remote machines they wish. (And they are actually doing that, so I'm a big fan!)

Generating Keypairs

My crypto instructor emphasized two things to the point of annoyance:

  • Don't create your own cryptographic solutions in a vacuum. Their weaknesses will include every single thing which might occur to someone else but didn't occur to you.
  • Your perfect solution would be something which can never be guessed. We deal in guesses which take an extremely long time.

Neither point is particularly relevant here, except as an evergreen reminder that it's best to use the cryptographic standards which are well-studied and to accept that they are not infallible. But the weakest link in our privacy might well be us, and whether we successfully keep our private keys inaccessible. Retiring and replacing keys periodically won't hurt.

We're also limited by compatibility... your encryption method of choice may not be available everywhere. At the moment, we have a number of options for use with OpenSSH:

 -t dsa | ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa

If you run ssh-keygen without specifying details, you currently receive rsa keys. I use ed25519, and so do the others I asked, but if you connect to something running a positively ancient incarnation of ssh you might be out of luck with those and need to generate a different type.

I'll make us an example keypair to use, sticking with the defaults.

valefor:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/pamela/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/pamela/.ssh/id_rsa.
Your public key has been saved in /home/pamela/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:G0c/gGyAd2XVGd7QqMvIiLlDCALuK6lht/CpIA5bAhI pamela@valefor.blah
The key's randomart image is:
+---[RSA 2048]----+
|     ..  .o...o= |
|.   . .o.o   .+o.|
|E    . .+ o  .. .|
|.o.    . . o.    |
|+. . . oSo.oo.   |
|o.  . + .+o o.   |
|*+o. . ..        |
|O*+ o o          |
|=o.+   .         |

So now I have two new files corresponding to the two keys stored within ~/.ssh, and a lovely free octopus tentacle artwork. Score!

I also created a second set of keys for more specific purposes at the same time, using ssh-keygen -t to specify ed25519. I'll easily be able to tell these sets apart, since by default they're always named id_(type) and id_(type).pub. OpenSSH will look for files in ~/.ssh named any of the id_* combinations it offers by default. If you choose to rename or move your keypair, use ssh -i (/path/file) to point at it. I know people who separate out keys they expect to rotate frequently, or use at particular events, for example.

When generating keys, you're given the chance to password-protect access to the file containing your private key (and should probably do so). That's just done locally. The passphrase can be updated later, or even left empty and the key's use restricted in other ways. You can be prompted for your passphrase with each use of the key, or unlock the private key just once at the start of each X session thanks to ssh-agent(1). This should be offered automatically on your next login if you are using one of the default names for your keys. (If interested, you can see how the keys are loaded in at /etc/X11/xenodm/Xsession, and my next post includes a basic intro to use of ssh-agent/ssh-add on the command line).

A private key file's format is nothing more than the appropriate big long string of gibberish, with ominous BEGIN PRIVATE KEY and END PRIVATE KEY comments on either end. But with this implementation, what information am I actually handing out in my public key to all my troublemaker friends?

valefor:~$ cat .ssh/id_rsa.pub                                                     
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCz0SXRq6tRtc+kXFeT6TTD4ryJpi+11Z3W53gbHgv
NMRD+BOXYsNz3GfVDLF8lMc6YmFJJEbheFNHtlJChEGQM2AR5EecVNShI8AT9 pamela@valefor

Clear as mud. Take the contents of your .pub file and edit ~/.ssh/authorized_keys on your destination to add them, or ask whoever is creating your account to do it for you. (Protip: use :r in vi, r in ed, C-x i in emacs, C-r in nano to just slap in the entire contents of a file. You laugh, but it took me over a year to notice I could do this. If that's also you, you're welcome!)

Your authorized_keys file expects each public key on a separate line (not required but please put a space between, yikes!) and ultimately takes the following form:

[options, or leave blank] key-type KEY user@keyorigin

The last field is just a simple generated comment. You can change it to anything which will remind you where the associated private key lives, or even add in a more traditional comment using # on its own line.

That's all I needed. Now I can ssh or scp to that remote machine and I'll be asked for the passphrase for my key if it's not already loaded into my agent, and then I'm in! All done!


Oh no! I'm not actually in! This only takes me as far as the firewall at my house. The machines inside, including the one containing the folder of adorable baby hippo pics I'm trying to reach, are on a local subnet. I can't just type in the IP address I know... it's internal to the wrong network.

I'm extremely lazy. I definitely don't want to ssh into my firewall and ssh from there to the correct machine, yuck! I want a way to punch straight through, and ideally I'd get to set it up and never think about it again.

There are two straightforward ways to deal with this. (There are probably more. I know of two). You can set up port forwarding, where specifying a specific port at the end of your remote IP address will tell the firewall which machine you want to contact on the inside, like dialing an extension on the phone. This is within the realm of pf(4) and we provide some instructions in the FAQ.

Or you can use the ssh ProxyJump configuration directive. It's still port forwarding, but designed for this purpose specifically. At this point we're moving over to looking at information contained within ssh_config(5), but we actually only need a few lines of configuration to get the job done here. (Note that sshd_config(5) also exists, if you want to explore your options on the daemon end of things, too).

Our ultimate goal here is to be able to type "ssh destination" and reach the correct machine, without fussing over a login to the machine(s) in-between, and without exposing the session itself to that intermediary (merely using it for authentication).

We can do this on a single-use basis on the command line by adding our key to the authorized_keys file on the "jump host" machine in the middle as well as the destination, and then making a jump like so:

ssh -J jumphost destination

If your key is authorized on both machines, this should just work. You don't need a private key on the jump host. Generate your local keys, put the public key onto both remote machines' authorized_keys lists and you're there. If you'll need more than one jump, separate with commas.

Let's be real. As simple as that was, after a few times I'll still want a permanent shortcut. Sometimes I work within test VMs and other complicated networks, and I'd rather just type the name of the machine I want and go.

For this case, I will always want the same jump host, and just one jump, so I am first going to edit ~/.ssh/config to create an entry that tells OpenSSH what I want to do whenever I try to connect to the remote machine "antlion".

My bare minimum configuration here would be:

Host antlion
ProxyJump jumphost.example.com

I know antlion by its internal IP on a different subnet, so let's add that in as the HostName for ssh to use when talking to the jumphost.

Host antlion
ProxyJump jumphost.example.com

Now we're getting somewhere. When I type "ssh antlion" on this machine, it will connect to the jumphost machine (my firewall, an ocean away) and then onward to the correct machine on the internal network. With HostName I've given the jumphost an address I'm certain it knows how to reach. This prevents me from needing to fuss with anything on jumphost.example.com, such as adding an /etc/hosts entry, where I might not have root access.

Now I can type "ssh antlion" and connect to it, without thinking about what's in-between. Perfect. Fiona the Hippo awaits!