Empty SSH agent before sleep

I use SSH keys to connect to the remote servers I manage. As those keys are password protected and it can be tedious to enter (long, complex) passwords multiple time per day, I setup an agent a long time ago to unlock keys once in the morning and then forget about them.

The only problem I realize recently was that the agent did nothing specific when I put my computer in sleep (going to lunch, answer a call, whatever). This means that all my keys are still loaded in memory and a well equiped attacker might access them. Or if a less equiped attacker discover my session password, he can also log in to any server.

The solution chose is to empty the SSH agent each time my computer goes to sleep. It means that after each resume I have to enter again the keys passwords, but this is a good compromise for security.

Systemd prerequisites

To react to sleep mode, a very simple solution on distributions using systemd, is to hook a service to `sleep.target'. However by default this target is only accessible from the `system' scope of systemd.

Here you might need a little parenthesis. You might already know that you can deal with two /scopes/ of systemd. The `system' one, which is the one you interract with more often to start system services like web servers, bluetooth devices… with the command `sudo systemctl start whatever.service'. There is also the `user' scope, which aims to run services at user level and do not need superuser priviledges. You might already use it to start some user level software like custom notification daemon, IDE or terminal daemon… with the command `systemctl --user start whatever.service'. System and user scopes are completely isolated, and actually run in two different processes.

In my case, as the SSH agent is running in user scope, I wanted to interract with the sleep mode in user scope too. By default this is not possible, but still very easily configurable. I took inspiration from this article from Daniel Garajau[1]. The idea is to add a single system service, which will be hooked to `sleep.target' in the system scope, and be responsible to trigger a new `sleep.target' in a given user scope.

So, first we need to add a new `sleep.target' into the user scoped systemd directory (in `~/.config/systemd/user/'). To do that, the most simple thing to do is to copy the system one (usually found in `/usr/lib/systemd/system/sleep.target').

Beware that the system target includes a `RefuseManualStart=yes' line, which would prevent the newly created user target to be externally called (what we will do later). So be sure to remove this line (or switch it to `no'). You should have the following file:

Now, it’s time to add a new system service file, dedicated to start this user scoped sleep target. Here is the file I use:

Contrary to the Daniel Garajau's version, I use a `Before' setting in the `[Unit]' section (instead of an `After'). During my tests, I see that triggering the user scoped target /after/ the system sleep target is reached leads to unexpected results (like the computer actually going to sleep in the middle of a long running sleep related process in user scope, crashing it). Thus I took the decision to trigger it /before/. It sounds also more logical to me to first run any user scoped action before letting the system do its stuff.

The interesting part is the `ExecStart' line, where we use a specially craft `systemctl --user' call. This command is used to start the new `sleep.target' we added before in user systemd folder. That’s why we need this new target to allow manual start. The last part is the `--machine' argument, which is used to help systemd to target the correct systemd user process. The magic comes from the `%i' placeholder, which will be replaced at runtime by the label used after the `@' in the enabled service name.

That’s all. You now have a working environment to be able to add user scoped service file, able to react to the sleep event of you computer.

Here we targetted the /sleep/ event, which is a shortcut for all possible sleep events. You can use the exact same logic to target only a specific event like /suspend/ or /hibernate/ if you wish.

[1] this article from Daniel Garajau (HTTPS)

SSH agent configuration

Now that we are able to react to sleep event, we just need to add a new user service file to ask `ssh-agent' to delete all known identities from the agent when the computer goes to sleep.

The only trick here is that systemd run as a completely isolated process, knowing nothing about your environment. So we have to do something to let `ssh-agent' knows the path of its internal socket to be able to remove the activated keys. To do that, we use the `Environment' setting of the `[Service]' section and set the `SSH_AUTH_SOCK' variable.

Here, I’m using `%t/ssh-agent.socket' value, which is the default for the agent provided by the OpenSSH package of my distribution. The `%t' placeholder will be replaced at runtime with the user scoped runtime directory `/run/user/<your user uid>' (e.g. `/run/user/1000').

The `SSH_AUTH_SOCK' value may differ if your distribution uses another path, or if you are using another agent. For example, modern Gnome-based desktop environment are using GCR as a backend, whose socket will be by default at `%t/gcr/ssh'. If you use GnuPG also to manage your SSH keys, it’s socket will be by default at `%t/gnupg/S.gpg-agent.ssh'.

That’s all. Now each time you’ll put your computer in sleep mode, all keys added to your SSH agent will be removed before, ensuring nobody would be able to use it in your back.

--

📅 lundi 2 juin 2025 à 21:14

📝 Étienne Pflieger with GNU/Emacs 30.1 (Org mode 9.7.18)

🏷️ Bidouille

🏷️ tutoriel

🏷️ configuration

🏷️ systemd

🏷️ Linux

🏷️ SSH

📜 Back to gemlog

🏡 Back to home

🚀 Propelled by fronde

Proxied content from gemini://alltext.umaneti.net/gemlog/empty-ssh-agent-before-sleep.gmi (external content)

Gemini request details:

Original URL
gemini://alltext.umaneti.net/gemlog/empty-ssh-agent-before-sleep.gmi
Status code
Success
Meta
text/gemini
Proxied by
kineto

Be advised that no attempt was made to verify the remote SSL certificate.