Since 10.5, Mac OS X has had integrated keychain support in OpenSSH that lets one store his SSH private key passphrase in the keychain. This makes it easy to securely store the passphrase permanently, instead of just per-session or per-boot as ssh-agent(1) does (unless the "Remember password in my keychain" option is not selected, in which case the passphrase is only stored in the memory of the running
The way all of this works on OS X is as such:
When an SSH connection is made,
ssh finds a user's public key that the server will accept.
sign_and_send_pubkey() is called, which checks to see if a running
ssh-agent process is available at the socket specified by the
SSH_AUTH_SOCK environment variable. On OS X, this socket is created at login time by
launchd, which creates the randomly-named listener socket (controlled by
/tmp. As soon as any process tries to communicate with that socket,
launchd automatically fires up an
It's worth noting that because
ssh-agent is spawned from
launchd, it will not see any environment variables that can affect it, like
SSH_ASKPASS which specifies the path to an
ssh-askpass program used to receive text input under a graphical environment.
ssh-agent does not have the passphrase, it runs
keychain_read_passphrase() (OS X-specific). This function runs
SecPasswordAction() in OS X's
libsecurity which retrieves the passphrase from the keychain or prompts the user for it in a secure fashion (disabling key grabbing utilities, etc.) as shown above.
The passphrase entered is passed to
ssh_add_identity() which stores it in the running SSH agent and uses it to sign the key needed to authenticate to the server.
Due to my high level of paranoia, I've always configured my laptops to erase SSH key passphrases from memory when locking the screen (or suspending) with
ssh-add -D (and
sudo -K, too). On OS X, ScriptSaver can be used to run a script at lock time, and SleepSaver can make sure that the screen saver runs before suspending. Erasing SSH key passphrases is useful in case something were to happen where the screen saver/lock stops running, an in-memory SSH passphrase can't be used to login to all of my remote servers.
On OS X, it would seem easy enough (and extra secure) to just run
security lock-keychain before locking, but after unlocking the screen, the keychain remains locked. If one waits for a program needing keychain access to prompt for the passphrase, that only allows that one program access and does not unlock the keychain completely. Prompting for the keychain passphrase after unlocking could be done (with
security unlock-keychain), but then one has to enter his password twice when unlocking the screen. This was an annoying problem I had on OpenBSD: every time I would unlock the screen, I would have to enter my SSH passphrase right away (due to running
xlock) regardless of whether or not I was going to use SSH. That problem was eventually solved by using this 3rd party patch to OpenSSH which automatically adds passphrases entered at connection-time to a running
ssh-agent (which is how it works on OS X now by default).
So it would seem the most usable solution is to keep SSH key passphrases out of the keychain and just use plain old
ssh-agent. Remove the passphrases at lock time with
ssh-add -D and let SSH re-add them when connecting for the first time after the screen is unlocked.
SSH agent forwarding is a useful feature that lets a user SSH to a server and then SSH from that server to another with the same key without having to keep copies of the private key on each server or enter a passphrase on a possibly-compromised server. I enable this option for each of my servers in my
~/.ssh/config file so that I can bounce around between them and do SSH-tunneled CVS (er, I mean git) updates.
With agent forwarding enabled, the
SSH_AUTH_SOCK environment variable becomes set on the user's session on the server to point to the user's forked
sshd process, which receives agent sign requests and forwards them back through the SSH connection to the
ssh-agent process on the user's machine.
This is a convenient feature, but it also exposes a direct connection to the SSH agent to anyone that can access the socket on one of those remote servers. Normally file permissions would prevent anyone but the user from being able to do this, but not on a compromised server or one with other administrators. To solve this problem,
ssh-agent includes a per-key confirmation option (by using
ssh-add -c) that will require the user to confirm each key signing request through a GUI. On OpenBSD,
/usr/X11R6/bin/ssh-askpass is used by default (and also used to enter passphrases for SSH keys when the
DISPLAY environment variable is set):
Unfortunately, because SSH on Mac OS X is automatically adding passphrases to the agent internally (through
ssh_add_identity()) they can't be added with the confirmation flag. Manually adding them with
ssh-add -c works, but then the on-connection adding feature is lost. The OS X-specific keychain feature could be disabled so that it will fallback to prompting for key passphrases, but that will not work for GUI applications that call-out to SSH without a terminal, like XCode or Sequel Pro, and these requests cannot be passed to an
askpass-style program because
RP_USE_ASKPASS is not set when calling
read_passphrase() (it only gets set for agent signing confirmations like shown above).
With no way to get around this problem using the built-in options, I had to patch OpenSSH. Apple makes available all of its patches to open source software shipped with Mac OS X, so it was easy to change and recompile and still have all of the OS X-specific features. I forked this code and added a
RequireKeyConfirmation option which can be enabled in
~/.ssh/config. With this setting enabled, passphrases are added to the
ssh_add_identity_constrained() with the confirmation flag set.
With agent confirmation enabled, now an
ssh-askpass-style program is needed. I was originally going to write a direct Cocoa replacement of the X11
ssh-askpass program, but I found CocoaDialog which made this much easier.
By default, OpenSSH will try to call
/usr/libexec/ssh-askpass, which doesn't exist on OS X. The
SSH_ASKPASS environment variable can be set before launching
ssh-agent to point to another path, but since
ssh-agent is being launched by
launchd as noted above, this environment variable will not be honored. To avoid this problem, my patched OpenSSH's install routine installs the
cocoa-ssh-askpass script as
This confirmation also works with agent forwarding, so that any remote SSH connection trying to use the agent on my laptop will have to be confirmed first.
escape, clicking Cancel, or any other failure that prevents that program from being run and returning "yes" to
ssh-agent will make it refuse to sign the key.
The code for my OpenSSH modifications can be found on GitHub.