OpenSSH 'AuthorizedKeysCommand' with 'fingerprint' argument (Patch for OpenSSH 6.6p1)

Contents

Many of us developers or system administrators use OpenSSH’s public key authentication (aka password-less login) on a daily basis. The mechanism works based on public key cryptography: By adding a RSA/DSA public key to the authorized_keys file, the user with the matching private key can login without a password. The mechanism works great for a couple of hundred, thousands and even 100k thousand users (tested, login takes ~2sec).

But what if there are more keypairs, say, a million users, or a more flexible approach is desired? Maybe with an LDAP or a database backend? Think of GitHub and how they do their ssh git@github.com ... login! This blog post shows you how to do that by patching OpenSSH’s AuthorizedKeysCommand option to support an additional fingerprint argument.

1. AuthorizedKeysCommand: Not quite a solution (just yet)

At first sight, the AuthorizedKeysCommand option in the /etc/ssh/sshd_config config file is the right choice. Every time someone tries to log in, it executes a command and returns a dynamic authorized_keys file.

Example: If the option is set to AuthorizedKeysCommand /my/command.sh and a user tries to log in as ssh git@myserver.com .., OpenSSH will run /my/command.sh git and parse whatever the command outputs as authorized_keys file.

The command.sh script could query a database or an LDAP backend, so this is definitely dynamic! However, it does not fix the million user problem, because if the user ‘git’ could be logged in to by a million users, the dynamically created authorized_keys file would still need a million entries.

To make an efficient keypair lookup, we need the key fingerprint as an additional argument for the AuthorizedKeysCommand. So instead of doing /my/command.sh git, we want the command to be run like this: /my/command.sh git 64:a1:03:7c:1d:b6:7e:b0:0f:fd:76:7e:f0:ca:4f:20.

Using the user (here: git) and the fingerprint (here: 64:a1:03:7c:1d:b6:7e:b0:0f:fd:76:7e:f0:ca:4f:20), we can query the database or the LDAP and only return one result (instead of a million!).

Imagine something like this:

#!/bin/bash

user=$1
fingerprint=$2
public_key=$(get_public_key "$user" "$fingerprint") # get_public_key could call an LDAP/database!

if [ -n "$public_key" ]; then
        echo "$public_key"
fi

2.1. Solution 1: Using OpenSSH 6.9

If you are reading this at a point at which OpenSSH 6.9 has been released and is available for you to use, check out the changes in the AuthorizedKeyCommand option. As of today (May 2015), a patch is being reviewed (see bug #2081) that greatly enhances this command, and also may contain the fingerprint as an argument (here: %f).

Potential /etc/ssh/sshd_config if patch gets merged

# This does NOT work for versions before OpenSSH 6.9!
# And it may not work after. This is based on the current patch for this feature!
AuthorizedKeysCommand /my/command.sh %u %k %t %f

2.2. Solution 2: Patching OpenSSH

If above mentioned patch has not been merged, we need to patch OpenSSH ourselves (check out the source in this GitHub Gist). That actually sounds harder than it is. If your system is based on Debian/Ubuntu, for instance, you can get the source of the OpenSSH version you are currently using via apt-source, and then rebuild a Debian package after patching the source via dpkg-source and dpkg-buildpackage.

Here’s how that works:

Retrieving the OpenSSH source (on Ubuntu/Debian)

# Get the source
apt-get source openssh-server

# Retrieve and apply the patch
wget https://gist.githubusercontent.com/binwiederhier/40e76f8c51055173dd81/raw/06e10128b3cd09ad42f833a04db0e9425d9ed3a3/auth2-pubkey.c.patch
patch -p1 < auth2-pubkey.c.patch

# Rebuild
sudo apt-get install libwrap0-dev libpam-dev libgtk2.0-dev libedit-dev libselinux1-dev libck-connector-dev dh-autoreconf dh-systemd libssl-dev libkrb5-dev

cd openssh-6.6p1/
dpkg-source --commit
 Enter desired patch name: auth2-pubkey.c.patch

 # Description: Add fingerprint argument to AuthorizedKeysCommand
 #  This patch makes OpenSSH pass the public key fingerprint to the 
 #  AuthorizedKeysCommand, thereby allowing an efficient lookup.

dpkg-buildpackage -us -uc

# Install the package
dpkg -i ../openssh-server_*.deb

2.2.1. What’s in the patch?

The patch is really simple. As mentioned above, it merely runs the authorized_keys command with an additional ‘fingerprint’ argument – nothing more. Not counting the log output, it’s a 4 line fix.

OpenSSH 6.6p1 patch for enhanced AuthorizedKeysCommand

--- openssh.ORIG/openssh-6.6p1/auth2-pubkey.c	2015-04-16 01:02:52.000000000 +0100
+++ openssh/openssh-6.6p1/auth2-pubkey.c	2015-04-16 01:03:50.304751623 +0100
@@ -512,7 +512,7 @@
 	struct stat st;
 	int status, devnull, p[2], i;
 	pid_t pid;
-	char *username, errmsg[512];
+	char *username, *fp, errmsg[512];
 
 	if (options.authorized_keys_command == NULL ||
 	    options.authorized_keys_command[0] != '/')
@@ -552,8 +552,10 @@
 		goto out;
 	}
 
-	debug3("Running AuthorizedKeysCommand: \"%s %s\" as \"%s\"",
-	    options.authorized_keys_command, user_pw->pw_name, pw->pw_name);
+	fp = key_fingerprint(key, SSH_FP_MD5, SSH_FP_HEX);
+
+	debug3("Running AuthorizedKeysCommand: \"%s %s %s\" as \"%s\"",
+	    options.authorized_keys_command, user_pw->pw_name, fp, pw->pw_name);
 
 	/*
 	 * Don't want to call this in the child, where it can fatal() and
@@ -602,7 +604,7 @@
 		}
 
 		execl(options.authorized_keys_command,
-		    options.authorized_keys_command, user_pw->pw_name, NULL);
+		    options.authorized_keys_command, user_pw->pw_name, fp, NULL);
 
 		error("AuthorizedKeysCommand %s exec failed: %s",
 		    options.authorized_keys_command, strerror(errno));
@@ -611,6 +613,7 @@
 		break;
 	}
 
+	free(fp);
 	temporarily_use_uid(pw);
 
 	close(p[1]);

2.2.2. Testing the patch

First create a dummy authorized_keys script and save it at /usr/local/sbin/ssh_authorized_keys (don’t forget to chmod +x it!) – something like this:

Dummy authorized_keys command with fixed lookup

#!/bin/bash

if [ "$1" == "user" -a "$2" == "64:a1:03:7c:1d:b6:7e:b0:0f:fd:76:7e:f0:ca:4f:20" ]; then
        echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDk15Ms4gJVpr8NbvHPPdAinLD6rWwGtJl4r1UokjXd6qQ2SKtR3xtCoIerhSE+KkYEF1mMoExIwx31d0WuQOJIkuoyJFhLhPAZBgVY6xj8t33Xlvnj1NMmUy+YuG/M8wTjbH1ooTQfg63BQibLzAhR7vbxm8j4UX+w8V5QV+VQtNG4sbxg5H6Szn2OP9s8HYfmmutOgygwKIymn8PapTPFlFqy1pj7iPs9XSSaNbS8FI/FU9yX0OWPpXD7S73JXZulLVTUMZGpNzaI5NQJAPUq7cRHNZjx0dOWQZ5OH5QrW2kJVzDAA2+oq+pv4TGTk1gPrDC6tnGyjkOWdElMRRN pheckel@platop"
fi

Then edit your /etc/ssh/sshd_config file and add the AuthorizedKeysCommand directive:

/etc/ssh/sshd_config

AuthorizedKeysCommand /usr/local/sbin/ssh_authorized_keys
AuthorizedKeysCommandUser nobody

Now run your OpenSSH server and connect to it with the matching private key:

Get matching private and connect via SSH

wget https://gist.githubusercontent.com/binwiederhier/40e76f8c51055173dd81/raw/56070bed38bedcead404eb07955206345d2eac7a/user.key
sudo chmod 600 user.key
ssh -o IdentityFile=user.key user@localhost id

To get a more extensive logging, start SSHd like this:

Start SSHd with lots of logging

sudo $(which sshd) -ddd