Having a local, secure and passwordless sudo is easy, especially with apples built in biometrics support. But what if most of your sudo prompts are on a remote machine you ssh into? Well, we can set this up too.
Prerequisites
- We have a set of target hosts we SSH into and escalate our privileges using sudo, which requires a password
- You have some kind of secure password manager on your local machine. I’ll be using 1password on a mac, but the solution is more general than that.
The problem
Our initial setup looks like this:
- We SSH into a remote machine
- Call
sudo whatever
- We need to type a password
And our target architecture is the following:
- We SSH into a remote machine
- Call
sudo --askpass whatever
- Behind the scenes, your password manager is queried for password, and you grant access using your regular method, and it gets delivered to sudo
I don’t want to spoil too much, but our solution will include defining SUDO_ASKPASS
, an askpass script, ssh tunnel between two separate sockets, a script to talk to the password manager and a sprinkle of some configuration on top. Let’s get to it.
Creating a script to print the password on stdout
This is the simples part. Just use whatever scripting or compiled language to fetch a specific password from your vault and dump it to standard output. I use 1password, so I’ll make use of 1Password CLI utility called op
:
#!/usr/bin/env bash
main() {
declare account="$1" uuid="$2" field="${3:-password}"
/opt/homebrew/bin/op read --account "$account" "op://Employee/$uuid/$field"
}
main "$@"
Since the script will not be executed from my login shell, I passed the full path to the op binary (as found by command -v op
), because my $PATH
settings will not be present in that context.
I have my password saved in a work account, so my default vault is named Employee
. I got the item uuid from 1Password app. You can learn more about secret reference syntax to adjust to your context.
Let’s confirm that it works. After I saved this to ~/bin/op-sudo.sh
and adding necessary permisions chmod a+x op-sudo.sh
:
Everything works fine. The password manager asks me for confirmation first, and the password is printed on stdout.
Setting up a LaunchAgent (inetd) to listen on a socket
This next part will create a local socket, that will map to the script we just created. On linux machines there was this thing called inetd
which opened a port for us, listened for connections, and mapped the script’s stdio to the socket. Today, I guess systemd can do that, and here’s a person describing how to set that up.
I am using a macOs machine instead, so I will create a LaunchAgent instead. LaunchAgents use a very similar concept, but they are configured via XML instead. Fill in some blanks in the following config and save it in ~/Library/LaunchAgents
with a plist
extension. I used the name pl.narzekasz.op-sudo.plist
.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<false/>
<key>Label</key>
<string>pl.narzekasz.op-sudo.35s524did7b74qggqeoqbetpee</string>
<key>ProgramArguments</key>
<array>
<string>/Users/puck/bin/op-sudo.sh</string>
<string>WonderNetwork</string>
<string>35s524did7b74qggqeoqbetpee</string>
</array>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>InitGroups</key>
<true/>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockPathMode</key>
<integer>384</integer>
<key>SockPathName</key>
<string>/Users/puck/.op-sudo/wondernetwork.sock</string>
<key>SockType</key>
<string>stream</string>
</dict>
</dict>
</dict>
</plist>
There are some key placeholders there you need to pay attention to:
- I used a pl.narzekasz.pl.op-sudo.{id} Label, because I anticipate I might have more such configurations to serve different passwords. This does not matter that much.
The ProgramArguments is an array representing the command invokation, so it contains the full path to the script as well as its arguments (1password account and item uuid). There’s nothing stopping you to call
op
directly here instead of having an intermediate script:<string>/opt/homebrew/bin/op</string> <string>--account</string> <string>WonderNetwork</string> <string>read</string> <string>op://Employee/{place-your-id-here}/{field}</string>
And at the bottom there’s
SocketPathName
defining a path to the unix domain socket that will be used to listen for incomming connections. Make sure to create the directory first, or the agent won’t start:mkdir /Users/puck/.op-sudo
Finally, we need to launch the agent:
launchctl load ~/Library/LaunchAgents/pl.narzekasz.op-sudo.plist
And I will use socat
to confirm that I can get the password using the specified socket:
Creating a tunnel to the socket
Now, we need to somehow transfer that socket to the remote host. Fortunately, we can simply use ssh remote forwarding for that. I will quickly show how to do that by opening a TCP port of target machine, which is less secure, because every user on that system will be able to access it. That’s not a huge issue, since the script asks for confirmation each time, but it’s better if we can avoid that.
Using a TCP port
The way we connect would be:
ssh -R 7825:/Users/puck/.op-sudo/wondernetwork.sock target-host
We’re good to go.
Using a Unix Domain socket
Let’s do that properly and permanently instead. Add the remote forwarding instruction to your /config
for target host or group. And here’s the part where you can set up multiple launch agents, serving different passwords, listening on different sockets, forwarded to different remote machines.
Host narzekasz.pl
RemoteForward /home/puck/.ssh/op-sudo.sock /Users/puck/.op-sudo/wondernetwork.com
We can confirm that this still works:
There is one catch. The remote socket won’t be cleaned up by the sshd
after you disconnect, so a dead socket file will be left in the filesystem. And next time you try to connect: sshd will bail, because the target path already exists. This behaviour can be overriden using the StreamLocalBindUnlink
option, unfortunately, this needs to be set in the sshd_config
of the target host, which might be out of reach in many situations.
Final piece of the puzzle: sudo-askpass
Finally, we need to tie this together with sudo on the remote host. We can use the SUDO_ASKPASS
environment variable for this. First, let’s save a script that reads from our forwarded unix domain socket. You can use either netcat (nc
) or socat
for that, whatever is more convenient for you:
#!/usr/bin/env bash
has-command() {
command -v "$1" >/dev/null 2>&1
}
main() {
SOCKET="$(realpath ~/.ssh/op-sudo.sock)"
if has-command nc; then
echo | nc -U "$SOCKET"
elif has-command socat; then
socat stdio "UNIX-CONNECT:$SOCKET"
else
echo "Type the passwords your own self" >&2
return 1
fi
}
main "$@"
Save it to wherever, say ~/bin/sudo-askpass
(remember to chmod a+x ~/bin/sudo-askpass
) and configure it in your ,
or similar:
export SUDO_ASKPASS=~/bin/sudo-askpass
Enjoy! Remember to run your sudo with --askpass
option!
Please leave likes and comments below to let me know if you enjoyed this instruction or have anything to improve