This Time Self-Hosted
dark mode light mode Search

GnuPG Agent Forwarding with OpenPGP cards

Finally, after many months (a year?) absence, I’m officially back as a Gentoo Linux developer with proper tree access. I have not used my powers much yet, but I wanted to at least point out why it took me so long to make it possible for me to come back.

There are two main obstacles that I was facing, the first was that the manifest signing key needed to be replaced for a number of reasons, and I had no easy access to the smartcard with my main key which I’ve been using since 2010. Instead I set myself up with a separate key on a “token”: a SIM-sized OpenPGP card installed into a Gemalto fixed-card reader (IDBridge K30.) Unfortunately this key was not cross-signed (and still isn’t, but we’re fixing that.)

The other problem is that for many (yet not all) packages I worked on, I would work on a remote system, one of the containers in my “testing server”, which also host(ed) the tinderbox. This means that the signing needs to happen on the remote host, although the key cannot leave the smartcard on the local laptop. GPG forwarding is not very simple but it has sort-of-recently become possible without too much intrusion.

The first thing to know is that you really want GnuPG 2.1; this is because it makes your life significantly easier as the key management is handed over to the Agent in all cases, which means there is no need for the “stubs” of the private key to be generated in the remote home. The other improvement in GnuPG 2.1 is that there is better sockets’ handling: on systemd it uses the /run/user path, and in general it uses a standard named socket with no way to opt-out. It also allows you to define an extra socket that is allowed to issue signature requests, but not modify the card or secret keys, which is part of the defence in depth when allowing remote access to the key.

There are instructions which should make it easier to set up, but they don’t quite work the way I read them, in particular because they require a separate wrapper to set up the connection. Instead, together with Robin we managed to figure out how to make this work correctly with GnuPG 2.0. Of course, since that Sunday, GnuPG 2.1 was made stable, and so it stopped working, too.

So, without further ado, let’s see what is needed to get this to work correctly. In the following example we assume we have two hosts, “local” and “remote”; we’ll have to change ~/.gnupg/gpg-agent.conf and ~/.ssh/config on “local”, and /etc/ssh/sshd_config on “remote”.

The first step is to ask GPG Agent to listen to an “extra socket”, which is the restricted socket that we want to forward. We also want for it to keep the display information in memory, I’ll get to explain that towards the end.

# local:~/.gnupg/gpg-agent.conf

extra-socket ~/.gnupg/S.gpg-agent.remote
Code language: PHP (php)

This is particularly important for systemd users because the normal sockets would be in /run and so it’s a bit more complicated to forward them correctly.

Secondly, we need to ask OpenSSH to forward this Unix socket to the remote host; for this to work you need at least OpenSSH 6.7, but since that’s now quite old, we can be mostly safe to assume you are using that. Unlike GnuPG, SSH does not correctly expand tilde for home, so you’ll have to know the actual paths we want to write the unix at the right path.

# local:~/.ssh/config

Host remote
RemoteForward /home/remote-user/.gnupg/S.gpg-agent /home/local-user/.gnupg/S.gpg-agent.remote
ExitOnForwardFailure yes
Code language: PHP (php)

Note that the paths need to be fully qualified and are in the order remote, local. The ExitOnForwardFailure option ensures that you don’t get a silent failure to listen to the socket and fight for an hour trying to figure out what’s going on. Yes, I had that problem. By the way, you can combine this just fine with the now not so unknown SSH tricks I spoke about nearly six years ago.

Now is the slightly trickier part. Unlike the original gpg-agent, OpenSSH will not clean up the socket when it’s closed, which means you need to make sure it gets overwritten. This is indeed the main logic behind the remote-gpg script that I linked earlier, and the reason for that is that the StreamLocalBindUnlink option, which seems like the most obvious parameter to set, does not behave like most people would expect it to.

The explanation for that is actually simple: as the name of the option says, this only works for local sockets. So if you’re using the LocalForward it works exactly as intended, but if you’re using RemoteForward (as we need in this case), the one on the client side is just going to be thoroughly ignored. Which means you need to do this instead:

# remote:/etc/sshd/config

StreamLocalBindUnlink yes
Code language: PHP (php)

Note that this applies to all the requests. You could reduce the possibility of bugs by using the Match directive to reduce them to the single user you care about, but that’s left up to you as an exercise.

At this point, things should just work: GnuPG 2.1 will notice there is a socket already so it will not start up a new gpg-agent process, and it will still start up every other project that is needed. And since as I said the stubs are not needed, there is no need to use --card-edit or --card-status (which, by the way, would not be working anyway as they are forbidden by the extra socket.)

However, if you try at this point to sign anything, it’ll just fail because it does not know anything about the key; so before you use it, you need to fetch a copy of the public key for the key id you want to use:

gpg --recv-key ${yourkeyid}
gpg -u ${yourkeyid} --clearsign --stdin

(It will also work without -u if that’s the only key it knows about.)

So what is about keep-display in local:~/.gnupg/gpg-agent.conf? One of the issues I faced with Robin was gpg failing saying something about “file not found”, though obviously the file I was using was found. A bit of fiddling later found these problems:

  • before GnuPG 2.1 I would start up gpg-agent with the wrapper script I wrote, and so it would usually be started by one of my Konsole session;
  • most of the time the Konsole session with the agent would be dead by the time I went to SSH;
  • the PIN for the card has to be typed on the local machine, not remote, so the pinentry binary should always be started locally; but it would get (some of) the environment variables from the session in which gpg is running, which means the shell on “remote”;
  • using DISPLAY=:0 gpg would make it work fine as pinentry would be told to open the local display.

A bit of sniffing around the source code brought up that keep-display option, which essentially tells pinentry to ignore the session where gpg is running and only consider the DISPLAY variable when gpg-agent is started. This works for me, but it has a few drawbacks. It would not work correctly if you tried to use GnuPG out of the X11 session, and it would not work correctly if you have multiple X11 sessions (say through X11 forwarding.) I think this is fine.

There is another general drawback on this solution: if two clients connect to the same SSH server with the same user, the last one connecting is the one that actually gets to provide its gpg-agent. The other one will be silently overruled. I’m afraid there is no obvious way to fix this. The way OpenSSH itself handles this for the SSH Agent forwarding is to provide a randomly-named socket in /tmp, and set the environment variable to point at it. This would not work for GnuPG anymore because it now standardised the socket name, and removed support for passing it in environment variables.

Comments 7
  1. So happy to hear you’re back 😀 Gentoo hasn’t been the same without you

  2. Hum, this doesn’t seem quite correct. You mention systemd’s /run, but your example still forwards the socket to ~/.gnupg. With this, my remote systemd machine ends up starting the agent anyway, as the socket is missing in /run. I guess this could be adjusted with some knowledge of the remote machine (e.g., UID), but it’s starting to require a lot of hard-coded parameters for each remote machine, particularly if some don’t use systemd.I have been struggling with the same issue with a wrapper script that I have been using since pre-2 GPG, and haven’t found a completely satisfying solution yet. The StreamLocalBindUnlink in the sshd config could be it!

  3. At least in my case the remote machine is not using systemd. You’re right that it might be working differently in that case, unfortunately I don’t have an easy way to try this out yet; I’ll see if I can reproduce in a VM later.

  4. I’d bet it’s not even running gpg 2.1. Even on an OpenBSD machine, gpgconf –listdirs shows me that that version expects all the sockets in /var/run, rather than in .gnupg, too.I’m working on the script I mentioned above (which starts the agents on login), so it automatically creates a symlink in the right place if the socket is missing.Testing it now, will report if it works, or doesn’t.

  5. Correction, only *some* of the sockets. Most importantly, the agent socket is still in ~/.gnupg.Anyway, I got to the bottom of my script. The following, sourced somewhere in the init scripts of your shell, should take care of setting up the local SSH-to-GPG agent, and fixup remote SSH-forwarded GPG sockets export GPG_TTY=$(tty) if [ -n “$SSH_CONNECTION” ]; then GPG_AGENT_SOCK=”$(gpgconf –list-dirs agent-socket)” GPG_AGENT_SOCK_FWD=${HOME}/.gnupg/S.gpg-agent if [ ! -e $GPG_AGENT_SOCK -a -S ${GPG_AGENT_SOCK_FWD} ]; then mkdir -p `basename $GPG_AGENT_SOCK` # The link might be present but point to nothing, ln -sf $GPG_AGENT_SOCK_FWD $GPG_AGENT_SOCK fi else unset SSH_AGENT_PID export SSH_AUTH_SOCK=”$(gpgconf –list-dirs agent-ssh-socket)” gpg-connect-agent /bye fiSuccess o/

  6. Thanks for pointing out these options, last time I checked OpenSSH couldn’t forward unix sockets yet. I’ve just also set up GPG agent forwarding, partly basing off your work.I’ve found that gpg (2.1.18 here) already creates an extra socket in `/run` (path can be found with `gpgconf –list-dirs), so there’s no need to manually specify that in the config. I also found that adding `no-autostart` to `~/.gnupg/gpg.conf` on the server can be useful, just in case the forwarding fails somehow, then at least you won’t get an agent running on the server with all kinds of confusing errors from that.I wrote a script integrating this forwarding with remotely attaching a tmux session, which might be useful for people, so I put it up here:

  7. > Note that this [`StreamLocalBindUnlink`] applies to all the requests. You could reduce the possibility of bugs by using the Match directive to reduce them to the single user you care about, but that’s left up to you as an exercise.Actually putting `StreamLocalBindUnlink` in a `host`/`match` section of `/etc/ssh/sshd_config` would not work(!) due to this bug:

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.