This is a quick tutorial on how to get SSH key forwarding working on Docker Desktop for Mac in situations where you want control of which ssh-agent server is being used, rather than the default.
You might want to do this to e.g. forward a specific SSH key into a docker container.
Docker has support for general SSH key forwarding, but this doesn't respect $SSH_AUTH_SOCK.
When you use ssh
, or git
over the SSH protocol (e.g. when cloning a repo with a [email protected]
URL), Unix systems will use ssh-agent
to find relevant keys. From the man page of ssh-agent
:
ssh-agent is a program to hold private keys used for public key
authentication. Through use of environment variables the agent can be
located and automatically used for authentication when logging in to other
machines using ssh(1).
Under the hood, this uses two key environment variables to control its behavior:
ENVIRONMENT
SSH_AGENT_PID When ssh-agent starts, it stores the name of the agent's
process ID (PID) in this variable.
SSH_AUTH_SOCK When ssh-agent starts, it creates a UNIX-domain socket and
stores its pathname in this variable. It is accessible
only to the current user, but is easily abused by root or
another instance of the same user.
When you start a new SSH server with ssh-agent -s
, it will echo the appropriate variables to see the newly started process ID, and the socket path to communicate with this new server.
Docker's implementation of SSH key forwarding works in two parts via two flags to the docker run
command:
The first mounts a file from the host system into the container file system. The bizarre part here is that this file does not exist on the host system. /run/host-serives/ssh-auth.sock
is a magical file that Docker Desktop for Mac is presumably synthesizing on demand.
--mount type=bind,src=/run/host-services/ssh-auth.sock,target=/run/host-services/ssh-auth.sock
The second is a command to set the SSH_AUTH_SOCK
environment variable inside the DOCKER container:
-e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock"
The combination of these does correctly forward ssh-agent inside the container to the ssh-agent in the host system, but unfortunately gives you no control over which ssh-agent process on the host machine it connects to. Presumably, it's whichever one was set in the environment when docker was launched.
In a fresh shell on my Mac, I get the following:
echo $SSH_AUTH_SOCK
/private/tmp/com.apple.launchd.kqwbiruqUO/Listeners
I could presumably control this by rebooting the docker runtime every time I wanted to change which keys I want to expose to my docker container, but this is pretty annoying, and also doesn't allow me to expose different SSH keys to different docker containers. Let's do better.
Intuitively, the most straightforward way of doing this would be to just mount the socket file from the host system into the docker container. Like this:
$ eval $(ssh-agent -s)
Agent pid 3339
$ echo $SSH_AUTH_SOCK
/var/folders/80/29_gz6bj01nb95w7vf2ty_rw0000gn/T//ssh-dR35S5FWDqJ8/agent.3338
$ docker run --mount type=bind,src=$SSH_AUTH_SOCK,target=/run/host-services/ssh-auth.sock whatever-image-name ssh-add -l
docker: Error response from daemon: invalid mount config for type "bind": stat /host_mnt/private/var/folders/80/29_gz6bj01nb95w7vf2ty_rw0000gn/T/ssh-dR35S5FWDqJ8/agent.3338: operation not supported
Unfortunately, as you can see, this doesn't work. Docker will refuse to mount an macOS socket file as a unit socket file. So what can we do?
We can't mount sockets, but we can create a socket inside the container, forward its connection over TCP to the host system, and then run a TCP server in the host system to connect it to whatever socket we want to.
We could write servers ourselves to do this, but there's already a handy utility called socat
that does just what we need.
Starting on the host system:
$ brew install socat
$ eval $(ssh-agent -s)
Agent pid 3339
$ echo $SSH_AUTH_SOCK
/var/folders/80/29_gz6bj01nb95w7vf2ty_rw0000gn/T//ssh-dR35S5FWDqJ8/agent.3338
$ ssh-add
$ nohup socat \
TCP-LISTEN:2222,bind=127.0.0.1,reuseaddr,fork \
UNIX-CONNECT:$SSH_AUTH_SOCK > /tmp/socat.log 2>&1 &
[1] 10894
Now we have a TCP server running on 2222, accessible only on localhost, which will act as a pipe to the macOS socket referenced by $SSH_AUTH_SOCK
(/var/folders/80/29_gz6bj01nb95w7vf2ty_rw0000gn/T//ssh-dR35S5FWDqJ8/agent.3338
in the case of this example).
Next, we need to set up the socket forwarding on the other side. Let's start a docker container:
$ docker run -it -e SSH_AUTH_SOCK="/tmp/ssh-agent.sock" whatever-image-name bash
container-user@7408b8ad773:/workspace$ apt-get install -y socat
container-user@7408b8ad773:/workspace$ nohup socat \
UNIX-LISTEN:/tmp/ssh-agent.sock,fork,mode=666 \
TCP:host.docker.internal:2222 > /tmp/socat.log 2>&1 &
container-user@7408b8ad773:/workspace$ file /tmp/ssh-agent.sock
/tmp/ssh-agent.sock: socket
container-user@7408b8ad773:/workspace$ ssh [email protected]
Hi jlfwong-bot! You've successfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.