Greetings, earthlings.

This is a blog post explaining how to run Emacs, GPG Agent, and SSH Agent as user-level daemons, mostly using systemd. I do so on Arch Linux, but in principle, this setup should work fine on any Linux distribution that uses systemd.

Prerequisites

This post assumes an “I run Linux on my computer” level of tech-savviness. The portions of this post that pertain to Emacs also assume that you are an Emacs user. If you don’t know what any of those words mean, or if this post employs concepts that you don’t understand but would like to, please feel free to get in touch with me and I’ll be happy to write a follow-up post explaining whatever you’d like to know about.

Background

I rely on public key authentication for my day to day. SSH is the primary means by which my 11 computers + 1 smartphone communicate with each other, and it is also the means by which I authenticate to GitHub. In addition, I make routine use of GPG, which, despite criticisms that have emerged in recent years, is still the only mature, general-purpose encryption utility that allows you to encrypt any data you want.

If you decide to create a private key, you should certainly also password protect it; that way, if one of the bad guys manages to get their hands on it, your password will serve as an additional impediment. But sadly, although password protecting your private key is an effective way to annoy the bad guys, it can be an equally effective way to annoy yourself, particularly when it forces you to sit there typing a long, error prone password many times per day. Therefore, to avoid being my own worst enemy, I run an agent process in the background that remembers my private key password until some reasonable amount of time has passed.

A further wrinkle, which I have decided to address in this blog post, is that I run Emacs in server mode on all my machines, and that I would like my Emacs server to be aware of whether I recently typed my SSH or GPG passwords at all times. This was somewhat of a hassle for me to set up the first time, but let me tell you: once it is working, it is hard to live without. Emacs will transparently decrypt any file with the .gpg extension upon open, and transparently encrypt it upon save. Incredibly convenient.

Set up GPG Agent

Let’s start with the easy part, which you don’t even need systemd to do. Create a configuration file located at ~/.gnupg/gpg-agent.conf with the following contents:

default-cache-ttl 34560000
max-cache-ttl 34560000

I basically never want my GPG Agent to reset unless I reboot, since I reboot often enough, so I have set the expiry time to the above number, which has no reason for being the number that it is, other than that I want it be large. If you want it to forget your password sooner than that, just change the above two settings to whatever interval you prefer; the number is in seconds.

That’s it. Can you believe you’re done configuring gpg-agent already? I can’t. Here’s how it works, out of the box. You boot up. You go about your business. You decide to decrypt a GPG-encrypted file for the first time. Now the following happens:

  • the GPG Agent launches in the background, as a daemon that will stay alive pretty readily, including if you log out and log back in
  • the GPG Agent checks to see whether you recently typed your password in
  • if you have, it immediately decrypts the file
  • if not, it prompts you for your password, remembering it afterwards for the time interval you selected
  • you’re done

Brief aside about prompting you for your password. By default, gpg commits the unforgivable atrocity of popping up a graphical window for you to type your password in. We can’t have that. Alternatively, in a terminal-only envrionment, it will pop up a TUI window prompt for your password, which is fine if you happen to be seated at your computer running individual commands in your shell, but which will Completely Break Everything if run in other contexts. We can’t have that either.

To get gpg to ask you for your password in the only sensible way—via the terminal—make sure to pass it the command line option --pinentry-mode loopback:

$ gpg -d --pinentry-mode loopback file-to-be-decrypted.gpg

There are lots of different ways you may or may not be invoking GPG, so hopefully the above example is adaptable to your circumstances. One other such circumstance I’ll mention is Emacs, which lets you set its epg-pinentry-mode variable to 'loopback, in which case gpg will prompt you plaintext-ily for your password, as the good lord intended:

(setq epg-pinentry-mode 'loopback)

If, like me, you get a special thrill out of using GPG with Emacs, then I recommend sticking that in your Emacs init file.

Set up SSH Agent

SSH Agent is going to require a little more work than GPG Agent did, but we aren’t going let that harsh our vibes. My first recommendation is to add the following configuration option to your ~/.ssh/config:

AddKeysToAgent yes

This option means that the first time you type your password in, your public key gets added to the SSH Agent for the standard amount of time, and as long as the application you’re running is in touch with the SSH Agent, no password retyping will be necessary. Your SSH Agent should remain aware of your password until it restarts, which normally means until reboot.

Next, start and enable the SSH Agent using systemd:

$ systemctl --user start ssh-agent
$ systemctl --user enable ssh-agent

This will set systemd to start your SSH Agent automatically upon login. I know what you’re thinking: surely we want to run the ssh-agent as a system-wide systemd service. But astonishingly, running ssh-agent as a user-level serice works perfectly. It will persist when you log out and log back in and is accessible from multiple distinct login sessions. Thanx, systemd!

Unfortunately, we aren’t quite done yet. SSH Agent still isn’t going to be able to convey its ineffable private key insight to any of your running apps, unless the app in question knows how to find the SSH Agent’s socket. For that to happen, every application you’d like to stay in touch with the agent needs to be run in an environment with the SSH_AUTH_SOCK variable set, and that the value of that environment variable needs to be the full path to your SSH Agent socket.

Conveniently, when you log in, systemd puts the location of that socket for you in an environment variable:

$ echo $XDG_RUNTIME_DIR
/run/user/1234

The path to the location of the socket on your machine will look like that, except that the 1234 part at the end will be your UID, which is the numeric identifier that UNIX refers to your user account by. On Arch Linux, the socket is called ssh-agent.socket, making the full path to the socket the following:

/run/user/1234/ssh-agent.socket

On some Linux distributions, the name of the socket might be something different, like openssh_agent. If you aren’t sure, do an ls $XDG_RUNTIME_DIR and look for a filename in there that mentions something to do with the ssh agent.

To get the majority of the applications you run to be aware of the agent, you just need to set the SSH_AUTH_SOCK environment in your login shell’s config. In my case, that login shell is zsh:

$ echo $SHELL
/usr/bin/zsh

Assuming your login shell is also zsh and the name of your SSH Agent socket is ssh-agent.socket, add this to your ~/.zshrc:

export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"

If you use a different login shell, you probably know how to do the equivalent of the above in that shell’s config, in which case you should do that instead. Because zsh is my login shell, and beacuse I start my graphical session from my login shell, my graphical session will inherit the SSH_AUTH_SOCK envrionment variable, meaning that any application I launch using a launcher or from within a terminal will also inherit it.

Set up Emacs

Are we done yet? You aren’t done if you want to run Emacs the way I do: i.e. always as a server, launched by systemd. As with ssh-agent, I like to run Emacs as a user-level service. You can set systemd up to start your Emacs server automatically, when you log in, with these commnads:

$ systemctl --user start emacs
$ systemctl --user enable emacs

Because Emacs (in this setup) is launched by systemd, Emacs will not necessarily learn what it needs to learn about the SSH Agent socket simply by inheriting the SSH_AUTH_SOCK variable from your login shell. Depending on the rest of your setup, you may need to be proactive about telling it where the SSH Agent socket is.

User service environment

I recently discovered a new method for being proactive, so let’s start with that. systemd will consult the directory ~/.config/environment.d before starting any user-level service. If it finds a file there called *.conf, it will add any environment variable settings in finds in that file to the environment of any user-level service it starts up. The *.conf files are sourced in the alphabetic/numerical order that you’d expect. So, assuming your UID is 1234, create a file called ~/.config/environment.d/emacs.conf and add this to it:

SSH_AUTH_SOCK=/run/user/1234/ssh-agent.socket

I warn you that these environment.d files are a total drag to refresh. As far as I know, the only failsafe way to get systemd to use the most recent edits to these *.conf files when launching new services is to reboot. Oh, well. At least I don’t anticipate having to update it very often.

An alternate option: use a custom unit file

I have no issue with the user service environment option, because I don’t run that many user-level systemd services and I don’t see any disadvantage to putting them all in touch with my running SSH Agent.

If you would rather micromanage which of your user-level systemd services know the location of the SSH Agent socket on a per-service basis, another option is to edit the default unit file that comes with the Emacs package. Should you decide to go that route, you can start by running systemctl cat to peek at the default unit file:

$ systemctl --user cat emacs
# /usr/lib/systemd/user/emacs.service
[unit]
Description=Emacs text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

[Service]
Type=notify
ExecStart=/usr/bin/emacs --fg-daemon

# Emacs will exit with status 15 after having received SIGTERM, which
# is the default "KillSignal" value systemd uses to stop services.
SuccessExitStatus=15

# The location of the SSH auth socket varies by distribution, and some
# set it from PAM, so don't override by default.
# Environment=SSH_AUTH_SOCK=%t/keyring/ssh
Restart=on-failure

[Install]
WantedBy=default.target

To put SSH_AUTH_SOCK into Emacs’ environment when the server starts up, you add one line to the [Service] section, which uses a special %t shortcut for finding your SSH Agent socket. The default unit file already has it written up in a comment for you, so all you have to do is uncomment it and change it to Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket.

systemd rudely expects you to edit unit files using the edit subcommand, which will open an $EDITOR window in your terminal and invite you to contribute the new line, commit message-style. The edit subcommand writes your change to a separate file, which systemd knows how to include in/overlay over the real unit file when it loads it:

$ systemctl --user edit emacs

It is woefully incosiderate of systemd, IMO, to assume that everyone will want to make edits this way. You can do it the way systemd expects, if you want. Alternatively, if you find systemd’s assumption as cringe as I do, you can create this file:

# /usr/lib/systemd/user/emacs.service
[Unit]
Description=Emacs text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

[Service]
Type=notify
ExecStart=/usr/bin/emacs --fg-daemon

# Emacs will exit with status 15 after having received SIGTERM, which
# is the default "KillSignal" value systemd uses to stop services.
SuccessExitStatus=15

# The location of the SSH auth socket varies by distribution, and some
# set it from PAM, so don't override by default.
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
Restart=on-failure

[Install]
WantedBy=default.target

If that file is called new-emacs-service, then you can install it with this command, in which you forfeit the power of making incremental edits but gain the power of being scriptable:

$ systemctl --user edit --full --stdin emacs < new-emacs-service

Suck on that, $EDITOR!

Set up Cron

Since we’ve come this far, I might as well mention that some of my cron jobs also require awareness of the SSH Agent, because they involve git pull-s and such. cron doesn’t allow you to execute arbitrary shell code in your crontab, but it does allow you to add hardcoded values to your shell environment. So if your jobs need to be brought into alignment with the agent, you can make that happen by adding the same line to the top of your crontab:

SSH_AUTH_SOCK=/run/user/1234/ssh-agent.socket

Without that, any of your cron jobs that require SSH key authentication will stall out waiting for your SSH key password.

Conclusion

The result of all this is a pretty sweet setup, from my point of view. The SSH Agent and the Emacs server launch upon login, but are not meaningfully tied to your login session—they persist when you log out and log back in, and can be accessed from multiple simultaneous login sessions. The Emacs server can be killed and started up again at will, but systemd establishes the connection with the SSH Agent afresh each time via the SSH_AUTH_SOCK environment variable. The SSH Agent prompts the user for a password the first time they try to use their SSH keys post-reboot, then remembers the password until the next reboot. And the GPG Agent does all that too, with no special config necessary.

Nothing left to do now but kick back and enjoy yourself.

Matt Teichman