SSH keys can be used to create incredibly powerful, but also incredibly confusing setups. In my previous post, I gave a really simple and safe three-step key setup for macOS that is likely to meet most folks’ needs, at least at first. But you may need more. So, let me talk to you about how that key system is not just simple, but also very flexible — and how it meets my needs for using a myriad SSH keys in a myriad ways.
First Things First
In the simple setup from my last post, we were putting keys together by hand. In fact, I’ve also automated the process in a tool called “setup-ssh-key”, with a few options for customizing the process along the way.
Once you’ve installed it somewhere convenient, running it with no options will generate the default “id_ed25519” key:
setup-ssh-key
Let’s stop for a moment and talk about what the “default” key means.
Key Selection
If you are ever using “ssh” to connect to a system and you’re not signing on as you expect, the “-v” flag is your friend. The verbose output of “ssh -v” will include lines like:
- “Will attempt key”: ssh wants to try this key
- “Offering public key”: ssh has this key and is trying it
- “Server accepts key”: this key was successfully used
The trick, and the trouble, is controlling this process.
In my previous post, I mentioned that the agent can be a complicating factor in complex setups. This is because whatever keys happen to be loaded into the agent are tried first, followed by any other rules.
That’s bad, because what’s in the agent is hard to manage. Any past invocation of ssh-add, or even ssh if configured a certain way, may have loaded a key. If we’re trying to use a specific key, the agent is not our friend.
If we can avoid using the agent (e.g. by not using “AddKeysToAgent” or anything similar), the rules become very predictable and simple.
Default Keys and Identities
The first rule is that the “id” keys (“id_rsa”, “id_ed25519”, and friends — see the manual page for the full list) are the default keys. All of them that are present will be tried until one is accepted.
I use the default keys for just that — most things I don’t want to configure. They’re for whatever I want to work without explicit configuration.
The next rule is if we specify an identity, the default keys won’t be used at all.
Creating a key that can be used as an identity is simple; it must simply just not be named “id”. That means “setup-ssh-key” just needs a different name. For example, for “atomic_ed25519”:
setup-ssh-key atomic
Identities can be specified two ways — with the “-i” flag, or with the “IdentityFile” config option. This opens up a few possibilities.
Profiles
During my work with several different Atomic clients, I need to use several GitHub accounts with different SSH keys. My most common use of identities is for just this use case.
To pull this off, I can use a unique “GIT_SSH_COMMAND” environment variable, like so:
export GIT_SSH_COMMAND="/usr/bin/ssh -i \"${HOME}/.ssh/atomic_ed25519\""
I drop this into an “.envrc” file in my “atomic” folder which direnv will pick up.
Once inside that directory, I can then easily clone any GitHub project and it will use my Atomic SSH key instead of my default key. Another folder has a similar line with “client_rsa” for the key, which uses the key I’ve reserved for that client—and it works exactly as you’d expect.
I find this works significantly better than the hack of inventing hostnames so I can use “IdentityFile”. direnv handles it so when I leave the “atomic” directory, “GIT_SSH_COMMAND” gets unset, and Git behavior returns to normal. I call these kinds of identity keys “profiles”, since they line up well with the idea of an “Atomic” profile or a “Client A” profile.
Critical Keys
I can also leverage “IdentityFile” in my “.ssh/config” file for actual hosts that require different keys. For this, there’s one specific use case, which I’ve come to call a critical key — a key that is my sole, irreplaceable route into a system.
Unlike my default keys or profile keys, these keys are not replaceable. So, I create separate keys for these setups and use the passphrase to save the whole SSH private key into 1Password. This is also a great way to share the key with my team if needed.
“setup-ssh-key” has a flag for making this easy:
setup-ssh-key -p important
This will create “important_ed25519”, but also output the passphrase to standard output, so I can grab it and preserve this critical key. Beyond the creation process, this key is used very similarly to any other identity key. I would add a Host section to “.ssh/config”:
Host important.example.com
IdentityFile ~/.ssh/important_ed25519
Building a Key System: Thinking Locally, Not Globally
All the above works because of a core principle I use in my system setups. I try to isolate as much as possible, avoiding configuration that affects everything. direnv is a fantastic tool for that. The SSH agents that operating systems and software like 1Password offer may seem convenient at first glance, but they work globally, and complicate that picture.
When you think locally, you can do all kinds of neat stuff. For example, I can launch a separate ssh-agent inside your shell just for that session, add keys to it, then use SSH’s agent forwarding feature to allow me to use only those specific keys from a remote server (as opposed to every key you may have loaded). And it all works without impacting my ability to use my machine to do literally anything else.
How to do this, I’ll leave as an exercise to the reader. (Hint: read the fine manual; specifically the “command” argument.) You can do a lot when you think locally. I encourage you to do it whenever you can.