Unlocking gpg-agent using pam_gnupg
Background: how I manage my passwords
To manage sensitive data I use pass, a password manager that encrypts secrets using asymmetric cryptography implemented by GnuPG. By design this means that whenever I want to use a secret, I am prompted to reveal the passphrase of my private key, so that the secret can be decrypted on the spot. As secure as this practice may be, one easily grows tired of entering one’s passphrase every. single. time. in order to do anything meaningful. In my humble opinion, basic security should not be cumbersome.
GnuPG offers an easy remedy: you can leave gpg-agent running in the
background of your session, and it will securely remember all your previously
entered passphrases. As long as your passphrase is cached, you will not be
bothered to enter it again1. This is convenient for sure but it comes with a
security trade-off: a malicious actor could potentially exploit your laziness by
decrypting all your cached passphrases from a memory dump2. Fortunately,
GnuPG permits to mitigate this risk to some extent by tuning the cache duration
in your ~/.gnupg/gpg-agent.conf
3. This forces gpg-agent to eventually
forget all passphrases that are not used too frequently, thereby limiting the
secrets any attackers may be able to exfiltrate.
For instance, the following configuration will have gpg-agent cache your passphrases for an hour.
default-cache-ttl 3600
max-cache-ttl 3600
Side note: If you just pasted the above lines in your gpg-agent.conf, your gpg-agent is currently running and you expect to see changes immediately, hold your horses! You will need to restart it, or at least force it to reload its settings by running the following command. Coincidentally this command will also clear your gpg-agent’s cache, which will be useful later.
$ gpg-connect-agent --no-autostart reloadagent /bye
Automatic unlocking with pam_gnupg
I have been happily using GnuPG like this for a long time. Recently, however, I discovered a neat improvement: if your GnuPG passphrase can be derived4 from your login password, you can configure PAM to automatically pass it on to gpg-agent upon login. The agent will cache the passphrase, and not bother you to enter it for the first time after rebooting; not even once. This feature is called presetting and can save you some time. Let me demonstrate how it can be set up.
First of all, presetting has to be explicitly enabled in your gpg-agent.conf by adding the following line:
allow-preset-passphrase
Once enabled, passphrases can be preset from many sources. To this end GnuPG
ships a simple program called gpg-preset-passphrase that I found convenient for
testing. You may have to look for it on your file system as it usually lives in
the libexec directory rather than the conventional bin directory. My
distribution places it in /usr/lib/gnupg/
.
When you manage to locate the program, its invocation is simple enough:
$ /usr/lib/gnupg/gpg-preset-passphrase --preset $KEYGRIP <passfile
Here $KEYGRIP
is the hexadecimal string that uniquely identifies the GnuPG key
that you wish to unlock, and for testing I temporarily placed5 my passphrase
in a file called passfile
6.
When you confirm that your gpg-agent can receive and successfully cache passphrases, it is time to cut out the middle man. There is a pluggable authentication module (PAM) called pam_gnupg that can harvest your password at the time of login, and pass it on to gpg-agent just like gpg-preset-passphrase. The way PAM works is outside of the scope of this article (if it is new to you have a look in the manual, here or here), however suffice to say that it is an extendable system that facilitates user authentication in Linux. There are many plug-ins that can perform various tasks and behaviors in PAM, and one of them is pam_gnupg. As it is an open-source project, it is possible to install it without much effort.
First you will need to create the ~/.pam-gnupg
fill that should contain the
value of $KEYGRIP
(if you want to unlock more than one key, you will need to
put their identifiers on separate lines). After that, you will need to spend a
little bit of time as root tinkering in your /etc/pam.d/
directory.
Caution! Modifying PAM chains is an excellent way to lock yourself out of your own computer. As a rule of thumb, when doing these changes I always like to test individual modifications immediately after making them. And just in case, I keep a secondary root shell in the background should I need to roll something back. If you want to err on the side of caution, you should consider backing up your system before you proceed any further. You have been warned!
According to pam_gnupg’s README you need to add the following two lines in your
/etc/pam.d/system-local-login
file. One at the end of the auth
section, the
other at the end of the session
section.
auth optional pam_gnupg.so store-only
session optional pam_gnupg.so
These will act as hooks that grab your password when you login to the system. In
my case, since I also happen to use swaylock, I inserted the following
line at the end of the auth
section in my /etc/pam.d/swaylock
file:
auth optional pam_gnupg.so
The idea behind this is that the same hook can also run when the session is unlocked, e.g. after I stroll away for a long time or after powering up my laptop from sleep. When this happens it is quite likely that gpg-agent will have already forgotten my passphrases, so I may just as well preset them again to save myself some typing.
At this point, it may be wise to verify that your PAM configuration (still)
works. The most convenient place to do this for me was with swaylock. I would
clear my passphrase cache, lock my session, unlock it and check if there were
any passphrases cached. A couple of attempts and one mistyped key identifier
later, everything was working as expected (if your mileage differs it is useful
to know that pam_gnupg offers a debug
option that makes it appear in syslog).
Encouraged by this I decided to take a leap of faith and log out completely.
After logging back in I was pleased to see that everything worked reliably out
of the box. If you run into problems, be sure to check pam_gnupg’s README – it
has some suggestions on race conditions etc.
Finally, as a little cherry on top I configured my system to explicitly clear gpg-agent’s cache every time my lockscreen is about to be executed. This should make my laptop less susceptible to memory dumping attacks whenever it is about to go to sleep or I am away from keyboard. This behavior can be implemented using the above-listed gpg-connect-agent command that reloads gpg-agent. In my case I simply wrapped a call to swaylock in a shell script that runs this command first, and substituted it in all references to swaylock in my configuration files.
Final thoughts
And there you have it! With this setup your gpg-agent should be automatically preset with your favorite passphrase when you login, it should forget it whenever you lock your session, and re-discover it after unlocking. Depending on your system, password manager and threat model it may or may not be not be a good solution for you.
This method without doubt introduces some vulnerabilities in the system. Specifically all of this hinges on the ability to derive one passphrase from another, which I would generally never recommend to anyone. For the strongest security, passphrases should be chosen as independently randomly as possible. That being said, rather than achieving the strongest security I was aiming to strike a reasonable balance between acceptable security and day-to-day usability, which is why I believe that in my case this makes sense.
In any case, if nothing else I hope that this helped you learn a bit about GnuPG, PAM and friends.
-
You may recognize this behavior from other password managers, e.g. macOS Keychain or gnome-keyring, which can both be “unlocked” by the user. I may occasionally allude to this terminology from now on. ↩︎
-
It is also worth contemplating whether this could be done after the cached passphrases were evicted from memory. ↩︎
-
Or more generally,
$GNUPGHOME/gpg-agent.conf
. See: gpg-agent(1) ↩︎ -
The referenced implementation of pam_gnupg only supports passing the login password to gpg-agent as-is, which requires both strings to be identical. I am generally opposed to sharing keys and passwords for different purposes, so here is my take: since pam_gnupg is open-source, nobody can stop you from forking it and implementing your own transformation function! ↩︎
-
Note that this is just for testing! I do not store my secrets unencrypted as that defeats the entire purpose of having a password manager in the first place. And even though this is a test, I would certainly recommend setting the mode of
passfile
to 0600 before placing anything sensitive inside. ↩︎ -
gpg-preset-passphrase can also read in your passphrase using the
--passphrase
option. However, I would advise against that as it risks revealing it to all users of your system (generally, anyone with access to your procfs can access the arguments of the process). ↩︎