How to use touchID for sudo remotely over ssh

18 Apr 2020 14:25 | macOS | security | touchID

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 -
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:


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"
"" >;
<plist version='1.0'>

4) Spawn fcgiwrap

$ launchctl load -w ~/Library/LaunchAgents/org.macports.fcgiwrap.plist

5) Install nginx from macports


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;

  server_name auth;

  rewrite ^/auth$ /;

  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:

copy it to:


10) Create the python CGI wrap at /var/www/auth/htdocs/

#!/usr/bin/env python3

import os
import sys


if 'HTTP_AUTH' not in os.environ or os.environ['HTTP_AUTH'] != KEY:

rc = os.system("/usr/bin/sudo -u admin /usr/local/bin/touch2sudo")

print("Content-type: text/plain\n")

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/ 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" "">;
<plist version="1.0">
      <string>-o ServerAliveInterval=60</string>
      <string>-o ExitOnForwardFailure=yes</string>
      <string>-R 61111:localhost:61111</string>

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:

$ 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:


/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
    exit 1
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

Test that this works before proceeding.

22) Clone my fork of simple-pam:

$ git clone

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.o

24) Add the auth sufficient line to the top of your /etc/pam.d/sudo file after
the bangline:


auth sufficient

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? :)