root-ubuntu: run one ssh-agent per user under systemd - #35
Merged
Conversation
Boxes this script provisions had no agent at all, so every ssh and every tool that leans on one (diskpush, git over ssh, rsync to another host) either prompted for a passphrase per connection or failed outright. A systemd user service rather than a line in .zshrc: the shell-snippet version starts a new agent per shell, so every tmux pane and every reconnect gets its own, a key added in one is invisible to the next, and the dead ones accumulate until reboot. The unit lives in /etc/systemd/user enabled --global, so accounts created later pick it up with no re-run, and anyone who wants none of it can `systemctl --user mask ssh-agent`. Two pieces beyond the unit. /etc/profile.d/ssh-agent.sh points login shells at the socket, because the unit's own Environment= only reaches services systemd starts, not an sshd login shell; it is POSIX so Debian's zsh reads it under `emulate sh` too, and it refuses to overwrite a live inherited SSH_AUTH_SOCK so `ssh -A` forwarding still wins. And linger, without which the user manager exits with the last session and takes the agent (and any detached tmux) with it. No key is ever loaded here -- a passphrase prompt has no place in an unattended root run. Verified on this box against real systemd and a real ssh-agent: unit starts and answers ssh-add, ExecStartPre recovers from a stale socket file, the snippet resolves the socket under both dash and zsh, keeps an inherited live agent, and replaces a dead one. Re-running install_ssh_agent writes nothing the second time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhwDgBphuy2UKenqMKkezQ
ThreatCrush Security Scan16 finding(s) HIGH/CRITICAL: 5 | MEDIUM: 2 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 30, 2026
Eight commits since 0.15.0, one of them a new command, so the minor moves. - sysupdate: apt lists, apt packages and snaps in one command, reachable from the pit as /update (#37) - root-ubuntu.sh: a 2G swapfile on a box with no swap at all, plus vm.swappiness=10 (#36) - root-ubuntu.sh: one ssh-agent per user under systemd (#35) - root-ubuntu.sh: lm-sensors, smartmontools and the rest of the hardware inventory set, with sensor detection on bare metal - companions: diskpush, and installers that are not npm (#34) - porkbun: Porkbun's own pre-flight, structured refusals, and the fact that registration spends prepaid credit rather than a card (#32, #33) Claude-Session: https://claude.ai/code/session_013TerE4nvNU3jvS51nRR6Pd Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Boxes provisioned by
root-ubuntu.shhad no ssh-agent, so anything leaning on one — git over ssh, rsync to a third host, diskpush — prompted for a passphrase every connection or just failed.What it adds
/etc/systemd/user/ssh-agent.service, enabled--global. One agent per user on$XDG_RUNTIME_DIR/ssh-agent.socket./etc/profile.d/ssh-agent.sh— points login shells at that socket.refresh_user(and root separately).Why a unit and not a line in
.zshrcThe shell-snippet version starts a new agent per shell: every tmux pane and every reconnect gets its own, a key added in one is invisible to the next, and the dead ones pile up until reboot. systemd gives exactly one and restarts it if it dies.
/etc/systemd/user+--globalrather than seeding each~/.config/systemd/user: one file to update, and accounts created later pick it up without a re-run. It stays a default rather than a policy —systemctl --user mask ssh-agentoutranks the global enable.Two details worth reviewing
The profile snippet is not redundant with the unit.
Environment=SSH_AUTH_SOCK=only reaches processes systemd starts; an sshd login shell is not one, so the shell has to be told separately. It is POSIX because Debian sources/etc/profile.d/*.shfrom zsh too, underemulate sh.It sets
SSH_AUTH_SOCKonly when there is not already a working agent — an inherited one is usually forwarded (ssh -A), and overwriting it would swap the keys you brought with you for the ones on the box. A set-but-dead socket (reattached tmux) is replaced.Linger is load-bearing, not a nicety. Without it the user manager exits with the last session and takes the agent with it, so a key added in one ssh session is gone by the next — most of the point of an agent. It also keeps detached tmux alive after logout.
No key is ever loaded. Every key worth having is passphrased and an unattended root run is the last thing that should be asking.
ssh-addon first login, orAddKeysToAgentin your own~/.ssh/config.Verified
On a real box, against real systemd and a real
ssh-agent— not just by reading the generated files:ssh-add -lanswersThe agent has no identities.ExecStartPrerecovers from a stale socket filedashzsh -c "emulate sh"systemd-analyze verifyinstall_ssh_agentrun writes nothingOne thing I did not change
write_if_changedreports success when itsinstallfails — it does not check the exit status, so a failed write still returns "changed" and the caller prints anote. I hit this when/etc/profile.dwas missing in a test sandbox. I worked around it inside the new function (install -dfirst) rather than touching a helper with ~15 call sites, but it is worth a separate look — during provisioning a silent write failure is exactly the kind that bites later.🤖 Generated with Claude Code
https://claude.ai/code/session_01EhwDgBphuy2UKenqMKkezQ