projects | github | twitter | rss | contact

Making OpenSSH on Mac OS X More Secure

posted to writings on apr 19th, 2011 with tags mac, nerd, security, and ssh

Since 10.5, Mac OS X has had integrated keychain support in OpenSSH that lets one store one's 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 ssh-agent).

The way all of this works on OS X is as such:

  1. When an SSH connection is made, ssh finds a user's public key that the server will accept.

  2. 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 /System/Library/LaunchAgents/org.openbsd.ssh-agent.plist) in /tmp. As soon as any process tries to communicate with that socket, launchd automatically fires up an ssh-agent process.

    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.

  3. Since 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.

  4. 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.

Keeping Passphrases Secure

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 ssh-add after 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

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).

Agent Key Signing Confirmation on Mac OS X

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-agent with 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 /usr/libexec/ssh-askpass.

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.

Pressing 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.

Comments? Contact me via Twitter or e-mail.