TouchID on the mac is really cool. It's awesome being able to use it for sudo,
but I thought it would be even more awesome if it could be used to authenticate
sudo remotely over ssh.
I've made this work using touch2sudo - https://github.com/prbinu/touch2sudo
which is a simple binary that when executed will show a touchID authentication
prompt and return 0 if the auth was successful and non-zero if not.
I've created a simple nginx vhost that exposes a python cgi script. When
executed this spawns touch2sudo and returns the status code to indicate if
authentication was successful or not.
I then create a persistent ssh connection to the remote server I want to use
this with, reverse forwarding the local nginx instance to the remote machine.
Then I wrote a simple PAM module which calls the endpoint in order to initiate
the touchID authentication.
## warning
Please be aware this is just a proof-of-concept, it's very rough around the
edges and will likely have security issues. Please don't use this for systems
you care about unless you know what you're doing. I am not liable for any issues
that may arise from following these steps.
Also note that when the nginx endpoint isn't listening on the remote machine,
any local user could throw up a tcp listener that just responds with "0" in
order to bypass sudo authentication. If you want to use this securely you'll
need to do a bit more work.
One possible way to mitigate this might be to use SSL on the nginx endpoint and
verify the fingerprint of the SSL cert in the touchid shell script wrapper.
## setup
1) Install fcgiwrap from macports
$ sudo port install fcgiwrap
2) Give your local user permission to execute /opt/local/bin/spawn-fcgi as root
without a password:
/etc/sudoers.d/fcgiwrap
--------------
admin ALL=(ALL) NOPASSWD: /opt/local/bin/spawn-fcgi
--------------
3) Create ~/Library/LaunchAgents/org.macports.fcgiwrap.plist
This has to be in your local user's LaunchAgents path as it needs to be able to
spawn the touchID gui popup on the user's desktop.
--------------
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
<plist version='1.0'>
<dict>
<key>Label</key><string>org.macports.fcgiwrap</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/sudo</string>
<string>/opt/local/bin/spawn-fcgi</string>
<string>-F</string>
<string>1</string>
<string>-P</string>
<string>/opt/local/var/run/fcgiwrap.pid</string>
<string>-s</string>
<string>/opt/local/var/run/fcgiwrap.socket</string>
<string>-U</string>
<string>nobody</string>
<string>-G</string>
<string>nobody</string>
<string>/opt/local/sbin/fcgiwrap</string>
</array>
<key>KeepAlive</key><true/>
</dict>
</plist>
--------------
4) Spawn fcgiwrap
$ launchctl load -w ~/Library/LaunchAgents/org.macports.fcgiwrap.plist
5) Install nginx from macports
/opt/local/etc/nginx/nginx.conf
--------------
worker\_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
include conf.d/*.conf;
include sites-enabled/*;
upstream fcgiwrap {
server unix:/opt/local/var/run/fcgiwrap.socket;
}
}
--------------
6) Create /opt/local/etc/nginx/fastcgi_params
--------------
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
--------------
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
7) Create the vhost config: /opt/local/etc/nginx/sites-enabled/auth.conf
--------------
server {
listen 61111 default_server;
root /var/www/auth/htdocs;
index index.py;
server_name auth;
rewrite ^/auth$ /auth.py;
location ~ \.py$ {
include /opt/local/etc/nginx/fastcgi_params;
fastcgi_param DOCUMENT_ROOT /var/www/auth/htdocs;
fastcgi_param SCRIPT_FILENAME /var/www/auth/htdocs$fastcgi_script_name;
fastcgi_pass fcgiwrap;
fastcgi_read_timeout 300s;
}
}
--------------
8) Start nginx
$ sudo launchctl load -w /Library/LaunchDaemons/org.macports.nginx.plist
9) Grab the touch2sudo binary from here:
https://github.com/prbinu/touch2sudo/releases/download/v0.1/touch2sudo-0.1.tgz
copy it to:
/usr/local/bin/touch2sudo
10) Create the python CGI wrap at /var/www/auth/htdocs/auth.py
--------------
#!/usr/bin/env python3
import os
import sys
KEY = 'YOUR_KEY_HERE'
if 'HTTP_AUTH' not in os.environ or os.environ['HTTP_AUTH'] != KEY:
sys.exit(0)
rc = os.system("/usr/bin/sudo -u admin /usr/local/bin/touch2sudo")
print("Content-type: text/plain\n")
print(str(rc))
--------------
Be sure to replace the key with a random string of your own choosing.
11) Test that the nginx auth endpoint works:
$ curl -s -H 'Auth: YOUR_KEY_HERE' http://localhost:61111/auth
This should show a touchID prompt, and the output from curl should be 0 if you
authenticate correctly with touchID. If this doesn't work, check the logs and
see why it isn't working before proceeding.
12) Create a new non-admin user on your mac to run the persistent ssh connection
as. For example you could call the user "sshuser". If you want to hide them from
the macOS login window you can execute:
$ sudo dscl . create /Users/sshuser IsHidden 0
If you want to remove them from the FileVault authentication page on startup,
run this:
$ sudo fdesetup remove -user sshuser
13) Create the same unprivileged user on your remote machine. I'll assume you
used the same username.
14) (Optional) prevent the user on the remote machine from spawning a shell.
This is for additional security. Add the below lines to your sshd config on the
remote machine and restart sshd.
--------------
# tail -n3 /etc/ssh/sshd_config
Match User sshuser
PermitTTY no
ForceCommand /bin/true
--------------
15) Become sshuser on your mac and generate an ssh key for them:
$ sudo -Hu sshuser bash
$ cd
$ ssh-keygen
16) Add the contents of /Users/sshuser/.ssh/id_rsa.pub on your mac to
/home/sshuser/.ssh/authorized_keys on the remote machine
17) Verify that the key authentication is working from sshuser on the mac to the
remote machine.
$ sudo -Hu mbssh bash
$ cd
$ ssh sshuser@REMOTE_MACHINE
PTY allocation request failed on channel 0
Connection to REMOTE_MACHINE closed.
If you see "PTY allocation request failed" this means it's working.
18) Create a launchd config to launch and maintain the persistent ssh
connection at: /Library/LaunchDaemons/com.sshuser.ssh.plist
--------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.sshuser.ssh.plist</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-NT</string>
<string>-o ServerAliveInterval=60</string>
<string>-o ExitOnForwardFailure=yes</string>
<string>-i</string>
<string>/Users/sshuser/.ssh/id_rsa</string>
<string>-R 61111:localhost:61111</string>
<string>sshuser@REMOTE_MACHINE</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>UserName</key>
<string>sshuser</string>
</dict>
</plist>
--------------
19) Start the launch service:
$ sudo launchctl load -w /Library/LaunchDaemons/com.sshuser.ssh.plist
20) Test that the touchID prompt comes up when you hit the nginx endpoint from
the remote server:
$ ssh REMOTE_SERVER
$ curl -sH 'Auth: YOUR_KEY_HERE' http://localhost:61111/auth
You should get a touchID prompt on your mac. If not, investigate and resolve the
issue before proceeding.
21) Create a simple bash wrapper to invoke the touchid authentication:
/usr/local/bin/touchid
--------------
#!/bin/bash
/bin/netstat -nat |grep ':61111' 1>/dev/null
if [ $? -eq 0 ] ; then
r=`/usr/bin/curl -s -H 'Auth: YOUR_KEY_HERE' http://localhost:61111/auth`
if [ "$r" == "0" ] ; then
exit 0
else
exit 1
fi
fi
exit 1
--------------
This wrapper will first check that the :61111 socket is listening, if it isn't
there it will return a non-zero exit code allowing sudo to fall back to password
authentication.
Test that this works before proceeding.
22) Clone my fork of simple-pam: https://github.com/m4rkw/simple-pam
$ git clone https://github.com/m4rkw/simple-pam
23) Compile and install the pam module
$ sudo apt install libpam-dev
$ cd simple-pam
$ gcc -fPIC -fno-stack-protector -c src/mypam.c
$ sudo ld -x --shared -o /lib/security/mypam.so mypam.o
24) Add the auth sufficient line to the top of your /etc/pam.d/sudo file after
the bangline:
--------------
#%PAM-1.0
auth sufficient mypam.so
--------------
At this point it should work. You should be able to ssh to your remote machine,
type "sudo bash" and authenticate the sudo escalation with touchID.
How cool is that? :)