A lightweight SSH agent for time-bound, human-approved sudo.
Instead of typing your password for every sudo on a remote server, or
trusting a long-lived key forever, sudo (via pam_ssh_agent_auth) asks
this agent to sign a challenge. The agent signs only if:
- the key is still within its time-to-live (TTL), and
- you approve that specific request with a yes/no prompt.
Warning
Experimental. This is early software that hasn't been audited. It works end to end, but expect rough edges and breaking changes. Keep password authentication as a fallback (see Server setup). Linux only.
your machine server
──────────── ──────
ssh-add ─► control.sock ─┐ sudo
▼ └─ pam_ssh_agent_auth
sudo-agent: holds keys until │ "sign this challenge"
their TTL ends, asks "Allow? [y/N]" │
▲ │
└─ agent.sock ◄── forwarded ◄────┘
- Passphrase once. It's asked for once, when the key is loaded (by
ssh-add, or by the agent for--key). After that, each signature only needs ay. - TTL inside the agent. A key's TTL starts when it's loaded. Keys are
dropped (and zeroized) when their time is up. The clock keeps counting
while the machine is suspended.
ssh-add -lshows the time left after each key's comment, e.g.laptop (14m30s left), on a server you've forwarded the agent to as well as locally. - Every signature is approved, in a pinentry popup or at the agent's
terminal, whichever you answer first; the other one closes. The prompt
shows which local process is asking and which key it wants, so a request
you didn't start (for example from someone else with access to the
forwarded socket) is visible rather than silent. In the popup, click
Allow; Deny, Escape or closing it denies. At the terminal, approving
takes
yoryesplus Enter, and keys typed before the prompt appeared are discarded; anything else denies. With no answer anywhere in time, the request is denied. - Two sockets.
agent.sockis the one you forward: it can only list keys and ask for signatures.control.sockis for adding and removing keys (ssh-add) and is never forwarded; see Security notes. - One request at a time. Concurrent requests queue, and every approval and refusal is logged.
cargo install sudo-agentOr from a checkout: cargo build --release.
Start the agent on the machine that holds your key:
sudo-agent serve
# Agent listening on /run/user/1000/sudo-agent/agent.sock
# Control socket on /run/user/1000/sudo-agent/control.sock (for ssh-add; never forward it)
# No keys loaded yet; add one with: SSH_AUTH_SOCK='/run/user/1000/sudo-agent/control.sock' ssh-add <key>
# Approvals: a pinentry popup or this terminal, whichever answers first
# export SSH_AUTH_SOCK='/run/user/1000/sudo-agent/agent.sock'serve can be left out: sudo-agent [OPTIONS] does the same, as in 0.1.
With ssh-add through the control socket, while the agent runs:
SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/sudo-agent/control.sock ssh-add -t 4h ~/.ssh/id_ed25519-tsets the key's TTL, cut down to--max-ttlif it's longer (the agent logs it). Without-t(or with-t 0) the key gets--ttl: a key never stays until removed.- Adding a key that is already loaded replaces it and restarts its TTL; that's how you extend a session.
-llists keys with their time left,-dremoves one,-Dremoves all.-cis accepted (every signature is approved anyway).-h(destination restrictions) and other constraints are refused rather than silently ignored, as are certificates and smartcard keys.
Or load keys at startup, with the passphrase asked in the agent's terminal:
sudo-agent serve --key ~/.ssh/id_ed25519 (repeatable).
| Option | Default | |
|---|---|---|
--key <PATH> |
none | OpenSSH private key to load at startup; repeatable |
--ttl <DURATION> |
15m |
TTL for --key keys and for ssh-add without -t (90s, 15m, 8h) |
--max-ttl <DURATION> |
8h |
Longest TTL a key may have: a longer --ttl is an error, a longer ssh-add -t is cut down |
--approval-timeout <DURATION> |
2m |
How long the terminal prompt waits for an answer |
--pinentry <PROGRAM> |
pinentry |
Program for approval popups; see Approval popups |
--pinentry-timeout <DURATION> |
60s |
How long a popup waits for an answer |
--no-pinentry |
No popups: approve at the agent's terminal only | |
--agent-socket <PATH> |
$XDG_RUNTIME_DIR/sudo-agent/agent.sock |
The socket to forward. Also SUDO_AGENT_SOCKET; the flag wins. --socket still works |
--control-socket <PATH> |
$XDG_RUNTIME_DIR/sudo-agent/control.sock |
The socket for ssh-add; never forward it. Also SUDO_AGENT_CONTROL_SOCKET; the flag wins |
Without $XDG_RUNTIME_DIR, the sockets go to
~/.cache/sudo-agent/agent-<hostname>.sock and control-<hostname>.sock.
Only one agent runs per user: serve refuses to start while another agent
answers on either socket.
The agent runs in the foreground, and approval prompts appear in its terminal, and as popups when there's a display. Ctrl-C at a prompt denies that request; Ctrl-C otherwise (or twice within two seconds, or closing the terminal) stops the agent, drops the keys and removes the sockets. A prompt or popup is withdrawn if its client goes away (e.g. the SSH connection closes) or its key expires.
When DISPLAY or WAYLAND_DISPLAY is set, each request also opens a
pinentry popup (it comes
with GnuPG on most desktops), unless --no-pinentry is given. The startup
line Approvals: … says whether popups are used.
- The default
pinentryis the one GnuPG uses: on Fedora, openSUSE and Arch a script that picks a variant, on Debian and Ubuntu whichever variantupdate-alternativeschose.--pinentrynames another, e.g.--pinentry pinentry-qt(or on NixOS, where there is no default). - It needs a graphical variant (
pinentry-gnome3,-qt,-gtk). A terminal one (pinentry-curses,-tty) can't work for the agent: it fails without touching the agent's terminal, the failure is logged once, and the other prompts still work. - A popup that fails or times out doesn't deny the request while the terminal prompt is still waiting.
pinentry-gnome3holds the keyboard and mouse while it's open, so the terminal can't be reached until you answer the popup or it times out (--pinentry-timeout); the terminal prompt then still waits.- With neither a terminal nor a display, nothing can approve requests, so every one is refused; the startup line says so.
Keep your normal agent for the login and forward only sudo-agent's
agent.sock:
ssh -o ForwardAgent=/run/user/1000/sudo-agent/agent.sock serverOr set it per host in ~/.ssh/config. ForwardAgent doesn't expand
${XDG_RUNTIME_DIR}, so use the literal path:
Host server
ForwardAgent /run/user/1000/sudo-agent/agent.sock
ssh forwards an agent only if it has one of its own (SSH_AUTH_SOCK or
IdentityAgent). Without one, ForwardAgent=<path> silently does nothing.
Then sudo on the server makes the approval prompt appear on your machine:
Signature request from pid 402599, uid 1000: ssh server
key: laptop (SHA256:…), 12m30s left
Allow? [y/N] y
Approved (terminal) signature request from pid 402599, uid 1000: ssh server
The popup shows the same text, says when it closes by itself
(--pinentry-timeout), and has Allow and Deny buttons.
The prompt names the local ssh process that carries the forwarded
request. It can't see which command runs on the server.
Key types: Ed25519 is recommended and the one tested end to end.
ECDSA should work but is untested. RSA works only when
the requester asks for rsa-sha2-512 signatures, which older
pam_ssh_agent_auth versions may not do. RSA signing also uses the
rsa crate, which has an unfixed timing side-channel advisory
(RUSTSEC-2023-0071). Every signature waits for your approval, which
makes the timing hard to measure from a server, but prefer Ed25519.
Setup is manual on purpose: a broken PAM config can lock you out of root,
and distributions differ. sudo-agent never edits system configuration.
Keep a root shell open while you change any of this.
- Install
pam_ssh_agent_auth:libpam-ssh-agent-authon Debian/Ubuntu,pam_ssh_agent_authon openSUSE. Tested with 0.10.4 (openSUSE) and 0.10.3 (Debian). - Put your public key in a root-owned file that you can't write to, e.g.
/etc/security/authorized_keys. If the file is user-writable, anyone who gets your account can add a key and become root. - Let
sudokeep the agent socket (visudo):Defaults env_keep += "SSH_AUTH_SOCK" - Put the module in front of the password stack in
/etc/pam.d/sudo, assufficientso a refusal falls back to the password:auth sufficient pam_ssh_agent_auth.so file=/etc/security/authorized_keys
To check it: ssh-add -l on the server should list your key with its
time left, and sudo -k; sudo -v should prompt on your machine instead of
asking for a password.
- The approval prompt is a sanity check and a tripwire, not a vault. Root on the machine running the agent can read keys from its memory.
- Anyone with root on a server you're connected to can send requests
through the forwarded socket, but each one needs your approval, and you
see it happen. They can't add, remove or lock keys through it: the agent
refuses that on
agent.sockand logs the attempt. control.sockmust never be forwarded, and the agent makes sure a mistake there (say, a copy-pastedForwardAgentline) doesn't hand a server control over your keys. It accepts only your own user, and refuses connections that come throughssh: OpenSSH 8.9 and later announce those with asession-bind@openssh.commessage, and as a further tripwire the agent refuses any client whose executable is namedssh. Every refusal is logged.- The terminal prompt needs
yplus Enter and ignores type-ahead. A popup can be approved with a click or Enter, so a keystroke meant for another window could approve one that appears while you type. You still see the popup, and you expect one for everysudoyou run. - Peer details in the prompt come from the requesting process and are informational. They are escaped so they can't rewrite the prompt.
Planned, roughly in order:
- running as a background service (a systemd user unit);
- a
sudo-agent approvecommand for approving from any terminal; - later:
sudo-agent stop, lock/unlock, andsudo-agent status; - showing the forwarding chain (which host a request came through) in the prompt.
MIT