I've used GnuPG (or "GPG") for years now, but until recently I had never
bothered setting up gpg-agent
. I've just never needed it. The agent mediates
access to private keys. These keys are only required for decryption and signing;
encryption and signature verification only require public keys. I've mostly used
GPG in the past for encrypting backups before uploading them to cloud storage,
and this doesn't require using an agent. When I needed to restore backups or
sign Debian and RPM packages I would just enter my password, but this was
infrequent enough to make an agent unnecessary.
A few things happened recently that changed this. I recently switched away from
the Gmail web UI, and I now access my email using
mbsync and
mu4e (a topic for a future blog
post). My dot files are stored in a private git repository I self-host, and I'm
using app passwords
for the IMAP and SMTP credentials. Even with all that, I'm not really a fan of
adding plaintext passwords to git repositories. The alternative is to store
these files in ~/.authinfo.gpg
which can be version controlled in git. Using a
real email client has also made it easier to work with PGP signed (and
encrypted) email, so that was another reason to set up an agent. And finally, I
wanted to start signing my git commits, and I don't want to enter a long
password every time I commit.
SSH Agent Functionality
As I started looking at the gpg-agent
man page the first thing I noticed was
its SSH agent functionality. I've been using
Ed25519 SSH keys since June 2015, and have had a
long and
difficult battle with the GNOME keyring, which
doesn't support these keys. I had to employ crazy
hacks to work
around GNOME when they broke environment variables in
Wayland. Then GNOME broke my agent again
when they changed how sessions are initialized, and after some serious code
spelunking
I found out the source of the issue, and asked them to revert their
"fix". The final place I
ended up was having an ssh-agent
process managed in a systemd user session.
This works, but has a serious drawback. Keys aren't automatically loaded into an
agent during login, and need to be manually added. The SSH protocol is very
basic and doesn't have
pinentry
capabilities. This meant I needed to manually run ssh-add
in a terminal every
time I logged in to load my keys. If I forgot to run this step, other commands
using SSH (e.g. git) would prompt for the key decryption password on their TTY,
and the key wouldn't be loaded into the agent process automatically.
I was curious to see how the gpg-agent
SSH functionality stacked up. It is
really good, and now I'm kicking myself in the foot for not investigating
this possibility earlier. GnuPG added Ed25519 support back in in August 2015, as
part of the v2.1.7 release. This means that like OpenSSH, and unlike
gnome-keyring-daemon
(henceforth "GKD"), it can load my Ed25519 SSH keys.
That's pretty awesome. Even better, GnuPG has real pinentry support. It can ask
for your decryption phrase using a regular pseudo-TTY (like running ssh-add
in
a terminal), using a curses application, or using a modal GNOME 3 dialog.
Ironically, gpg-agent
also has full GKD integration, meaning that it works as
a better GNOME SSH agent than the one GNOME ships. The way this works is that
the pinentry-gnome3
input method can optionally store your key decryption
passphrase in GKD.[^1] If you choose to store your passphrase this way your SSH
key will be unlocked automatically when it's first needed, without manually
entering the passphrase. This reduces security, but it's probably what many
people want and expect.
If you want something more secure you can configure a caching policy for keys.
At the highest level this is controlled by two options. Once an SSH key is
loaded into memory, it will be evicted once it hasn't been used for
default-cache-ttl-ssh
seconds. Regardless of how often it's accessed, once
it's been in memory for max-cache-ttl-ssh
second it will be evicted from
memory, requiring you to re-enter your password. The defaults are pretty
draconian, at 30 minutes and 2 hours respectively. I increased mine to 8 hours,
meaning that I enter my password once or twice a day. It's also possible to set
a caching policy on a per-key basis, as I'll explain later.
Running gpg-agent
A Systemd User Service
I'm a really big fan of systemd user services. This is a feature in systemd that
allows you to run services under the systemd instance managing your login
session. In my opinion this is about 1000x better than the traditional way of
managing user session daemons like ssh-agent
and gpg-agent
. The advantages
of using systemd user services are:
- There's a single consistent way to manage services, check the status of services, enable or disable services, start and stop services, etc.
- Units can be easily version controlled along with your other dot files. It's much easier to version control things like systemd timer units than user crontabs.
- Log data from these services is handled by
journald
, which means you never need to guess where log files are, and you can use all of the powerfuljournalctl
features to search these logs. - In the non-systemd world user daemon processes get parented to the system init process. This causes problems if you want the daemons to terminate when you logout. It also makes it difficult to ensure that you don't have duplicate instances of the daemon processes after logging in and then logging back out. Systemd user services solve this problem completely, and you can control whether you want the services to "linger" or not.
I already manage a number of session services and timers using systemd, so
naturally I set about to write systemd user services to manage my gpg-agent
process. I was pleasantly surprised to see that GnuPG 2.1.16 (released in
November 2016) added native support for systemd user services, meaning I didn't
need to write my own unit files. The process to use the official systemd user
units isn't very well documented, so I'll explain it here.
The basic idea is that GnuPG includes a number of .socket
and .service
files. The sockets are managed by a systemd user service, and a gpg-agent
or
dirmngr
process can be started on-demand when a socket is used. The agent
process stays in the foreground and sends log output to stdout/stderr where it's
intercepted by journald
. You need to copy these files to
~/.config/systemd/user
to activate and use them.[^2]
On Fedora the gnupg2
package will put these files in
/usr/share/doc/gnupg2/examples/systemd-user
. If you're building from source
you can find these files in the doc/examples/systemd-user
directory. You
should see the following files:
# On Fedora you'll find the files here.
$ ls -l /usr/share/doc/gnupg2/examples/systemd-user
total 32
-rw-r--r--. 1 root root 212 Aug 28 2017 dirmngr.service
-rw-r--r--. 1 root root 204 Aug 28 2017 dirmngr.socket
-rw-r--r--. 1 root root 298 Aug 28 2017 gpg-agent-browser.socket
-rw-r--r--. 1 root root 281 Aug 28 2017 gpg-agent-extra.socket
-rw-r--r--. 1 root root 223 Aug 28 2017 gpg-agent.service
-rw-r--r--. 1 root root 234 Aug 28 2017 gpg-agent.socket
-rw-r--r--. 1 root root 308 Aug 28 2017 gpg-agent-ssh.socket
-rw-r--r--. 1 root root 2274 Aug 28 2017 README
Copy them to your systemd user directory:
# Create the directory if it doesn't already exist.
$ mkdir -p ~/.config/systemd/user
# Copy the files.
$ cd /usr/share/doc/gnupg2/examples/systemd-user
$ cp *.{socket,service} ~/.config/systemd/user
# This will save a lot of typing. I have it in my .bashrc.
$ alias sysu='systemctl --user'
# Refresh systemd.
$ sysu daemon-reload
# Enable and start the sockets.
$ sysu enable *.socket
$ sysu start *.socket
There's one final required step: you need to tell gpg-agent
where to ask for
pinentry input. For pinentry in X11 or Wayland you can add the following line to
your agent config:
# Set a default display for gpg-agent.
$ echo "display :0" >> ~/.gnupg/gpg-agent.conf
You can also set the GPG_TTY
environment variable if you're not using a
graphical session.
Using The SSH Agent
To get the SSH agent to work you need to point SSH_AUTH_SOCK
at the new SSH
socket. The simplest way is to add this to your shell profile:
# Add this to ~/.profile or ~/.bash_profile
export SSH_AUTH_SOCK="/run/user/$(id -u)/gnupg/S.gpg-agent.ssh"
If you want to be really fancy you can inject the variable into your session
using the set-environment
command. There's not really any advantage to doing
it this way, but if you want to see what that looks like you'd change the
gpg-agent-ssh.socket
file like
this.
There's one final gotcha: the agent will only act delegate SSH keys with
keygrips listed in ~/.gnupg/sshcontrol
. You can also use this file to set
caching policies on a per-key basis, which could be useful if you have multiple
SSH keys and wanted to cache some of the keys longer or shorter than the others.
The first time you use an SSH key with the agent you need to manually run
ssh-add
in a terminal. This should update the sshcontrol file and afterwards
you should see something like this:
$ cat ~/.gnupg/sshcontrol
# List of allowed ssh keys. Only keys present in this file are used
# in the SSH protocol. The ssh-add tool may add new entries to this
# file to enable them; you may also add them manually. Comment
# lines, like this one, as well as empty lines are ignored. Lines do
# have a certain length limit but this is not serious limitation as
# the format of the entries is fixed and checked by gpg-agent. A
# non-comment line starts with optional white spaces, followed by the
# keygrip of the key given as 40 hex digits, optionally followed by a
# caching TTL in seconds, and another optional field for arbitrary
# flags. Prepend the keygrip with an '!' mark to disable it.
# Ed25519 key added on: 2018-03-21 03:03:49
# Fingerprints: MD5:39:fe:2b:cf:54:3a:c2:db:e4:b1:d2:70:a2:bc:82:fe
# SHA256:LKeBCI5/EDJDMKzH4qBBhzJFJaBObn+APfoOzpLRpCI
A8BB036E2A737F56BB0D0B4F9700BC3DC4A9137F 0
This is only required the very first time you use a key. After the key is
present in this file you can just type ssh
and the GnuPG agent process will
automatically be used.
[^1]: If you want to learn what else is stored in GKD you can use gnome-keyring-dump. I was interested to learn that Chrome stores its master password in GKD, but Firefox does not. [^2]: I've submitted a patch to Fedora that will make this step unnecessary in the future.