Adam Innes · Blog

GitHub Two-Factor Authentication: Turn It On, Then Clean Up Your SSH Keys

· 6 min · security, github, git, ssh

A week ago today, on September 3, GitHub added two-factor authentication. Until now, anyone who got hold of your GitHub password could sign in as you and do whatever your account could do. The new setting puts a second step in front of that, and it’s opt-in, so nothing happens until you turn it on. It’s worth doing, and while you’re in your account settings anyway, it’s a good moment to look hard at the other way people push code to your repositories: SSH keys.

What GitHub shipped

The announcement on the GitHub blog is short. Once you enable it, signing in takes your username and password as before, then GitHub asks for a code. That code comes from your phone in one of two ways, either a text message or a free two-factor app. The point, in GitHub’s words, is that someone who has discovered your password still can’t log in as you. You’ll find the setup link in your account settings, and the GitHub for Mac and GitHub for Windows apps prompt for the code after your password the same way the website does.

The app option deserves a quick explanation because it feels a little magic the first time. GitHub’s post only calls it a free two-factor application, but apps of this kind typically implement TOTP, the time-based one-time password algorithm published as RFC 6238. During setup your phone and the server end up sharing a secret. From then on, both sides take the current Unix time, count how many time steps have passed (the RFC’s default step is 30 seconds), and feed that number and the secret through HMAC to get a short code. The code is worked out on the phone from the secret and the clock, so the app doesn’t need a signal to show you one. The RFC also recommends that servers only tolerate a small, fixed amount of clock drift, so if codes keep getting rejected, check that your phone’s clock is right.

The other thing to settle before you need it is what happens if you lose the phone. GitHub says it provides recovery codes and backup SMS numbers. My advice is to save the recovery codes somewhere that isn’t the phone itself, like a password manager or a printout in a drawer, and add a backup number if you have one you trust. Losing your only second factor is a much worse afternoon than losing a password.

What changes for Git over HTTPS

This is the part that will bite people on day one. The web login is interactive, so GitHub can ask for a code, but Git on the command line just asks for a username and password. GitHub’s answer is simple. If you use SSH for Git, the announcement says you don’t need to do anything. If you use HTTPS, you stop typing your password and enter a personal access token instead.

Personal access tokens aren’t new. GitHub announced personal API tokens back in May, with the pitch that a token beats a password in a script because you can revoke it and you can make lots of them. They’re managed from your account settings. The same post warns that tokens are like passwords, so guard them that way.

A couple of practical notes follow from that. If you’ve had Git cache your GitHub password, for example with the OS X keychain helper, the stored entry is still your old password, and GitHub’s help pages show how to erase it with git credential-osxkeychain erase or through Keychain Access so the next push prompts you and you can paste a token. And don’t be tempted to paste the token into the clone URL. GitHub’s help article on using OAuth tokens with Git warns that doing so makes Git write the token to .git/config in plain text.

I’d also suggest one token per machine or script, named so you can recognize it later. Since tokens can be revoked, the day a laptop goes missing you delete that one token and everything else keeps working.

If you write scripts against the API with Basic Authentication, GitHub’s developer post on 2FA and the API covers it. Apps using the OAuth web flow carry on as usual. For Basic Authentication, the API docs as they read that week say a 2FA account gets a 401 with an X-GitHub-OTP header saying a code is required and whether it’s delivered by SMS or an app, and you send the code back in that same header. Because the codes expire quickly, GitHub recommends using a token for most API access instead.

Why SSH keys deserve a look now

Here’s the uncomfortable flip side of “SSH users don’t need to do anything.” The second factor sits in front of your password, and an SSH key on your account is a separate credential that doesn’t involve your password at all. If you’ve been adding keys for years, every laptop, desktop, VM and old work machine might still be on that list, and any one of them can push as you. Turning on 2FA and then leaving a key from a machine you sold last year is locking the front door and leaving the side door open.

So pull up the SSH Keys page in your account settings and compare it with the keys you actually hold. GitHub’s help article on the “Permission denied (publickey)” error describes the check: print the fingerprint of a local public key and see whether it matches one on that page.

ssh-keygen -lf ~/.ssh/id_rsa.pub

The -l flag, per the ssh-keygen manual from OpenBSD 5.3 (which ships OpenSSH 6.2), shows the fingerprint of the public key file you give it with -f, and adding -v also draws an ASCII art version. The fingerprint is the string of colon separated hex pairs, and that’s what you match against GitHub’s list. If your keys are loaded in ssh-agent, ssh-add -l lists the fingerprints of everything the agent holds. Run it on each machine you still use. Any key on GitHub that doesn’t match something you can put your hands on should go.

If you find a key locally and aren’t sure where it’s attached, GitHub’s help has a trick for that too:

ssh -T -i ~/.ssh/id_rsa git@github.com

The greeting names the account the key belongs to, and if it names username/repo instead, the key is a deploy key on that repository.

One key per machine

The cleanup is much easier next time if each machine has its own key. My suggestion is to generate a key per machine with a comment that says where it lives. GitHub’s own guide uses ssh-keygen -t rsa -C with your email as the label, and nothing stops you from naming the machine instead. The manual lists -C for the comment and -f for the file name.

ssh-keygen -t rsa -C "work-laptop-2013" -f ~/.ssh/github_work_laptop

Then point SSH at that file for GitHub in ~/.ssh/config. The ssh_config manual explains that IdentityFile sets the key to use and IdentitiesOnly yes stops ssh from also trying every other identity the agent offers.

Host github.com
  IdentityFile ~/.ssh/github_work_laptop
  IdentitiesOnly yes

GitHub’s key generation guide also makes the case for a passphrase: a key without one is basically a password written to a file, and ssh-agent (or the keychain on OS X) saves you from typing it constantly. With one key per machine, retiring a machine means deleting exactly one entry on GitHub.

Deploy keys need the same audit

Deploy keys live in each repository’s settings under Deploy Keys rather than on your account. GitHub’s help article on managing deploy keys describes one as an SSH key stored on a server that grants access to a single repository, and it’s blunt about the downsides: the key has full read/write access to that repository, and deploy keys usually don’t have a passphrase, so a compromised server hands over the key. GitHub also won’t let the same key be a deploy key on a second repository.

There’s no read-only deploy key today, so if a server only needs to pull, keep that write access in mind. The same help article lays out alternatives. SSH agent forwarding leaves no key on the server at all, although automated deploys can’t use it. A machine user, a separate account for the server, can cover several repositories, and organization accounts can give that machine user read-only access. Whichever you use, go through your repositories, and delete any deploy key belonging to a server that no longer exists.

The short version

Turn on two-factor authentication, stash your recovery codes somewhere other than your phone, and switch HTTPS Git over to personal access tokens, one per machine. Then treat the SSH Keys page like a guest list: check fingerprints with ssh-keygen -lf, remove anything you can’t match, give each machine its own key, and do the same pass over deploy keys. The second factor only protects the password, so the keys are yours to keep tidy.

← all posts