GNU/Linux Crypto: Agents

This entry is part 5 of 10 in the series GNU/Linux Crypto.

Now that we have both GnuPG and SSH securely set up, we’re able to encrypt, decrypt, sign, and verify messages, and securely authenticate to remote servers without any risk of exposing passwords and with effectively zero possibility of a brute-force attack. This is all great, but there’s still one more weak link in the chain with which to deal — our passphrases.

If you use these tools often, typing your passphrase for most operations can get annoying. It may be tempting to either include a means of automating the passphrase entry, or not to use a passphrase at all, leaving your private key unencrypted. As security-conscious users, we definitely want to avoid the latter in case our private key file ever gets stolen, which is where the concepts of agents for both SSH and GnuPG comes into play.

An agent is a daemon designed to streamline the process of using decrypted private keys by storing the details securely in memory, ideally for a limited period of time. What this allows you do with both SSH and GnuPG is to type your passphrase just once, and subsequent uses that require the unencrypted private key are managed by the agent.

In this article, we’ll go through the basics of agent setup for both SSH and GnuPG. Once we know how they work, we’ll then introduce a convenient tool to start both of them and manage them for us easily.

SSH agents

The ssh-agent(1) program comes as part of the OpenSSH suite. It can be run in two modes, either as a parent process, or daemonized into the background. We’ll discuss the latter method, as it’s more commonly used and more flexible.


When we run ssh-agent(1) for the first time, its behavior is curious; it appears to do nothing except spit out some cryptic shell script:

$ ssh-agent 
SSH_AUTH_SOCK=/tmp/ssh-EYqoH3qwfvbe/agent.28881; export SSH_AUTH_SOCK;
echo Agent pid 28882;

However, we can see that the daemon is running with the PID it mentions:

$ ps 28882
28882 ?        Ss     0:00 ssh-agent

So if it’s running fine, what’s with all the shell script it outputs? Why doesn’t it just run that for us?

The answer is an interesting workaround to a stricture of the Unix process model; specifically, a process cannot modify its parent environment. The variables SSH_AUTH_SOCK and SSH_AGENT_PID are designed to allow programs like ssh(1) to find the agent so it can communicate with it, so we definitely need them set. However, if ssh-agent(1) were to set these variables itself, it would only apply for its own process, not the shell where we called it.

Therefore, not only do we need to run ssh-agent(1), we need to execute the code it outputs so the variables get assigned in our shell. A good method of doing this in Bash is using eval and command substitution with $(...):

$ eval "$(ssh-agent)"
Agent 3954

If we run this, we can see that not only is ssh-agent(1) running, we have two new variables in our environment identifying its socket path and process ID:

$ pgrep ssh-agent
$ env | grep ^SSH

With this done, the agent is ready, and we can start using it to manage our keys for us.


The next step is to load our keys into the agent with ssh-add(1). Pass this program the full path to the private key you would like to use with the agent. This is likely either ~/.ssh/id_rsa or ~/.ssh/id_dsa:

$ ssh-add ~/.ssh/id_rsa
Enter passphrase for /home/tom/.ssh/id_rsa:
Identity added: /home/tom/.ssh/id_rsa (/home/tom/.ssh/id_rsa)

You can leave out the filename argument if you want ssh-add to add any or all of the default key types in ~/.ssh if they exist (id_dsa, id_rsa, and id_ecdsa):

$ ssh-add

Either way, you should be prompted for your passphrase; this is expected, and you should go ahead and type it in.

If we then ask ssh-add(1) to list the keys it’s managing, we see the key we just added:

$ ssh-add -l
4096 87:ec:57:8b:ea:24:56:0e:f1:54:2f:6b:ab:c0:e8:56 /home/tom/.ssh/id_rsa (RSA)

With this done, if we try to use this key to connect to another server, we no longer need to provide the passphrase; we’re just logged straight in:

tom@local:~$ ssh remote
Welcome to, running GNU/Linux!

The default is to maintain the keys permanently, until the agent is stopped or the keys are explicitly removed one-by-one with ssh-add -d <keyfile> or all at once with ssh-add -D. For the cautious, you can set a time limit in seconds with ssh-add -t. For example, to have ssh-add forget about your keys after two hours, you might use:

$ ssh-add -t 7200 ~/.ssh/id_rsa

To kill the agent completely, you can use ssh-agent -k, again with an eval $(...) wrapper:

$ eval "$(ssh-agent -k)"
Agent pid 4501 killed

You may like to consider adding this to ~/.bash_logout or a similar script to get rid of the running agent after you’re done with your session.

Permanent setup

If you like this and find it makes your key management more convenient, it makes sense to put it into a startup script like ~/.bash_profile. This way, the agent will be started for each login shell, and we will be able to communicate with it from any subshell (xterm, screen, or an appropriately configured tmux):

eval "$(ssh-agent)"
ssh-add ~/.ssh/id_rsa

On our next TTY login, we should be prompted for a passphrase, and from there be able to connect to any machine using the keys managed by the agent:

tom@local:~$ ssh remote
Welcome to, running GNU/Linux!

If you want this to work for a desktop manager like GDM or XDM, you can add a variable pointing to the ssh-askpass(1) program:

eval $(ssh-agent)
export SSH_ASKPASS=/usr/bin/ssh-askpass
ssh-add ~/.ssh/id_rsa

If SSH_ASKPASS is set like this and DISPLAY refers to a working display, then a simple graphical prompt will appear asking for your passphrase:

ssh-askpass prompting for a passphrase

This program may need to be installed separately. Under Debian-derived systems, its package name is ssh-askpass.

All child processes and subshells of the login shell will inherit the agent’s variables, since they were exported with export:

tom@local:~$ screen
tom@local:~$ tmux bash
tom@local:~$ bash
tom@local:~$ ssh remote
Welcome to, running GNU/Linux!

We thus have to type our passphrase only once per login session, and can connect to all of the servers to which our keys confer access … very convenient!

GnuPG Agents

Just like ssh-agent(1), there exists an agent for managing GnuPG keys too, called gpg-agent(1). Its behavior is very similar. On Debian-derived systems, it can be installed as the gnupg-agent package. You should also install a pinentry program; as we’re focussing on learning the nuts and bolts on the command line here, we’ll use pinentry-curses(1) for a console-based passphrase prompt:

# apt-get install gnupg-agent pinentry-curses


We’ll start the agent using the same eval $(...) trick we learned with ssh-agent:

$ eval "$(gpg-agent --daemon)"

We can verify that the agent is running in the background with the given PID, and that we have a new environment variable:

$ pgrep gpg-agent
$ env | grep ^GPG

We’ll also set GPG_TTY, which will help the pinentry program know on which terminal to draw its passphrase request screen:

$ export GPG_TTY=$(tty)
$ echo $GPG_TTY

Finally, to prod gpg(1) into actually using the agent, we need to add a line to ~/.gnupg/gpg.conf. You can create this file if it doesn’t exist.



With this done, if we try to do anything requiring our private key, we should be prompted for a passphrase not directly on the command line, but by our PIN entry program:

$ gpg --armor --sign message1.txt

│ You need a passphrase to unlock the secret key for user: │
│ "Thomas Ryder (tyrmored, tejr) <>"    │
│ 4096-bit RSA key, ID 25926609, created 2013-03-12        │
│ (main key ID 77BB8872)                                   │
│                                                          │
│                                                          │
│ Passphrase ***__________________________________________ │
│                                                          │
│       <OK>                                 <Cancel>      │

When we enter the passphrase, our operation is performed:

$ ls message1*

Afterwards, if we perform another option requiring the private key, we see that we are not prompted:

$ gpg --armor --sign message2.txt
$ ls message2*

The agent has thus cached the private key for us, making it much easier to perform a series of operations with it. The default timeout is 10 minutes, but you can change this with the default-cache-ttl and max-cache-ttl settings in ~/.gnupg/gpg-agent.conf. For example, to retain any private key for one hour after its last use and a maximum of two hours from its first use, we could write:

default-cache-ttl 3600
max-cache-ttl 7200

Changing these values will require prompting the agent to reload:

$ gpg-connect-agent <<<RELOADAGENT

Permanent setup

Just like ssh-agent(1), an ideal place for gpg-agent(1)‘s startup lines is in a login shell setup script like ~/.bash_profile:

eval "$(gpg-agent --daemon)"

The agent will be started, and all of its environment variables will be set and inherited by all subshells, just as with ssh-agent.

If you’re using the console PIN entry tool, you should also add this to end of your interactive shell startup script. This should be ~/.bashrc for Bash on Linux; you may need to put it in ~/.bashrc on Mac OS X.

export GPG_TTY=$(tty)


To manage both ssh-agent(1) and gpg-agent(1) effectively, a tool called keychain(1) is available. It provides a simple way to start both agents with one command, including loading keys at startup, and also prevents running either agent twice, picking up on agents started elsewhere on the system. Because desktop environments are often configured to start one or both agents for users, it makes sense to re-use them where possible, at which keychain(1) excels.

On Debian-derived systems, the program is available in the keychain package:

# apt-get install keychain

With keychain installed, we can start both agents with just one command in ~/.bash_profile:

eval "$(keychain --eval)"

We can optionally include the filenames of SSH keys in ~/.ssh or the hex IDs of GnuPG keys as arguments to prompt loading the private key (including requesting the passphrase) at startup:

eval "$(keychain --eval id_rsa 0x77BB8872)"

If this program is available to you, then I highly recommend this; managing agents and environments can be fiddly, and keychain(1) does all the hard work for you in this regard so you don’t have to worry about whether an agent is available to you in your particular context. Check out the project’s homepage for more information about the tool.

Shortcut for adding SSH keys

If you’ve dabbled with SSH much, for example by following the excellent tutorial a few years ago, you’ll know about adding keys to allow passwordless login (or, if you prefer, a passphrase) using public key authentication. Specifically, you copy the public key ~/.ssh/ or ~/.ssh/ off the machine from which you wish to connect into the /.ssh/authorized_keys file on the target machine. That will allow you to open an SSH session with the machine from the user account on the local machine to the one on the remote machine, without having to type in a password.

tom@conan:~$ scp ~/.ssh/ crom:.ssh/conan.pubkey
tom@conan:~$ ssh crom
tom@crom:~$ cd .ssh
tom@crom:~$ cat .ssh/conan.pubkey >>~/.ssh/authorized_keys

However, there’s a nice shortcut that I didn’t know about when I first learned how to do this, which has since been added to that tutorial too — specifically, the ssh-copy-id tool, which is available in most modern OpenSSH distributions and combines this all into one less error-prone step. If you have it available to you, it’s definitely a much better way to add authorized keys onto a remote machine.

tom@conan:~$ ssh-copy-id crom

Incidentally, this isn’t just good for convenience or for automated processes; strong security policies for publically accessible servers might disallow logging in via passwords completely, as usernames and passwords can be guessed. It’s a lot harder to guess an entire SSH key, so forcing this login method will reduce your risk of script kiddies or automated attacks brute-forcing your OpenSSH server to zero. You can arrange this by setting ChallengeResponseAuthentication to no in your sshd_config, but if that’s a remote server, be careful not to lock yourself out!