There's a lot of information on the intertoobs about getting ssh-agent "working" in OS X and even more articles about when and how the stock behavior of ssh-agent changed (mostly with respect to how ssh-agent interacted with the Keychain).

This article doesn't cover or care about any of that.

This article is concerned with:

  • Enabling ssh-agent in such a way that I can "ssh-add" in one terminal window and that same agent (and the loaded keys) is available in all of my other terminal windows.
  • Enabling use of ssh-agent from MacPorts and/or Homebrew and not the older ssh-agent that OS X ships with in /usr/bin.
  • To avoid having to put my keys in the Keychain (just a matter of preference).

Compatibility

Beware, reader. There's an awful lot of outdated, inaccurate information out there on how to modify ssh-agent behavior on OS X. Guess what? OS X changes from version to version! Many articles out there cater to older versions of the OS and are either no longer applicable (due to changes in OS X behavior) or plain don't work (due to functional changes in the software).

The steps below have been tested with OS X El Capitan (10.11).

What's The Problem?

OS X does ship with ssh-agent (/usr/bin/ssh-agent) and with the necessary launchd glue to start the agent (/System/Library/LaunchAgents/org.openbsd.ssh-agent.plist).

The plist starts ssh-agent on demand so you won't see the process running until something tries to talk to ssh-agent via the agent's socket.

The stock plist tells launchd to generate a secure, random path and use that as the path to the agent's listening socket.

<key>SecureSocketWithKey</key>
<string>SSH_AUTH_SOCK</string>

That path is given to the ssh-agent that launchd starts and is also stuffed into the SSH_AUTH_SOCK environment variable. This variable is inherited by the login shell (the mechanics of which I'm not really certain of, but I can see the results very clearly) which allows ssh-add to pick it up.

The path to the socket looks like this:

~% launchctl getenv SSH_AUTH_SOCK
/private/tmp/com.apple.launchd.1fJxDFgAmp/Listeners
~% echo $SSH_AUTH_SOCK
/private/tmp/com.apple.launchd.1fJxDFgAmp/Listeners

Remember, this is a randomly generated path and changes every time launchd launches the org.openbsd.ssh-agent agent.

If I was satisified with using /usr/bin/ssh-agent then I'd be done. But I want to use /usr/local/bin/ssh-agent from Homebrew.

Customizing the Plist

Update Mar 29 2017: An earlier version of this article gave the impression that the Homebrew agent would spawn on-demand, like the stock ssh-agent. I'm not sure if I just got it wrong or if the behavior changed in a later point release of OS X 10.11, but I don't currently observe that behavior. I've reworded the article to indicate that ssh-agent will spawn immediately and also made some other minor touchups and clarifications.

In order to avoid modifying the stock OS and also due to System Integrity Protection, which prevents editing or even unloading of org.openbsd.ssh-agent, I'm going to leave the org.openbsd.ssh-agent.plist file alone and create a custom plist.

I'm going to create ~/Library/LaunchAgents/org.homebrew.ssh-agent.plist like so:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.homebrew.ssh-agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/ssh-agent</string>
        <string>-D</string>
        <string>-a</string>
        <string>/Users/jknight/.ssh-agent.sock</string>
    </array>
    <key>Sockets</key>
    <dict>
        <key>Listeners</key>
        <dict>
            <key>SockPathName</key>
            <string>/Users/jknight/.ssh-agent.sock</string>
            <key>SockPathMode</key>
            <integer>384</integer> <!-- octal 0600 -->
            <key>SockPathGroup</key>
            <integer>0</integer>
        </dict>
    </dict>
    <key>EnableTransactions</key>
    <true/>
</dict>
</plist>

The plist calls /usr/local/bin/ssh-agent with these arguments:

  • -D β€” don't daemonize (a requirement for working with launchd); note this is different than the stock plist that uses -l which is an Apple-specific flag
  • -a β€” use a specific path to the socket, instead of coming up with a random path
  • /Users/jknight/.ssh-agent.sock β€” the static path to the socket

Load this plist and verify it's loaded:

~% launchctl load ~/Library/LaunchAgents/org.homebrew.ssh-agent.plist
~% launchctl list | grep ssh
2086       0          org.openbsd.ssh-agent
-          0          org.homebrew.ssh-agent

Now stop the org.openbsd.ssh-agent service and start the homebrew one. You should now see a - in the first colum of the former and a number in the first column of the later (the number is the process ID of ssh-agent).

~% launchctl list | grep ssh
-        0    org.openbsd.ssh-agent
14693    1    org.homebrew.ssh-agent

The ssh-agent service is now ready to use.

Talking to the Agent

The ssh-add program talks to ssh-agent over the agent's socket (that's why it's so important we know the path of the socket that the agent is using). The reason I gave ssh-agent a static socket path is so that I can instruct ssh-add to talk to the same socket. I'll do that by manually setting SSH_AUTH_SOCK in my shell's config which in this case is ~/.zshrc.

export SSH_AUTH_SOCK=/Users/jknight/.ssh-agent.sock

This will override the value that launchd is setting via the org.openbsd.ssh-agent launch agent and cause ssh-add to speak to the Homebrew ssh-agent and not the stock agent.

Final Results

When I launch a terminal window now, the environment variable points to the static socket location:

~% echo $SSH_AUTH_SOCK
/Users/jknight/.ssh-agent.sock

Next I can verify that ssh-add can talk to the agent:

~% ssh-add -l

The agent has no identities.

If ssh-add hangs or gives an error about being unable to talk to the agent, then something is wrong somewhere. Possibly, it's because the socket file already exists. Stop the service (launchctl stop org.homebrew.ssh-agent), delete the .ssh-agent.sock file and start the server (launchctl start org.homebrew.ssh-agent).

An optional verification is to see that launchd spawned ssh-agent and recorded its PID:

~% launchctl list | grep homebrew.ssh
14693      1          org.homebrew.ssh-agent

Finally, add the keys:

~% ssh-add
Enter passphrase for /Users/jknight/.ssh/id_rsa:
Identity added: /Users/jknight/.ssh/id_rsa
Identity added: /Users/jknight/.ssh/id_ed25519

Now, the big test: start a new terminal window/tab and verify that it is talking to the same agent that just loaded the keys:

~% echo $SSH_AUTH_SOCK
/Users/jknight/.ssh-agent.sock
~% ssh-add -l
2048 SHA256:QQbRpaJ6Ln...256 SHA256:uUZW892FQI8...

Done!