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.
Content
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:
1 2 3 4 5 6 7 8 9 |
#!/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).
1 2 3 |
# 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
--- 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:
1 2 3 4 5 |
#!/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:
1 2 |
AuthorizedKeysCommand /usr/local/sbin/ssh_authorized_keys AuthorizedKeysCommandUser nobody |
Now run your OpenSSH server and connect to it with the matching private key:
1 2 3 |
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:
1 |
sudo $(which sshd) -ddd |
Hi!
Great post – thank you! :)
Contact me please, I have a proposition for you.
Bests!
Hi Philipp,
Thank you for the tutorial.
I building a git server system same as Github with patch OpenSSH and looking for publish key in a Database.
So, my customer can connect to the server by:
ssh git@domain.com:username/repos.git
Instead:
ssh -o IdentityFile=user.key user@localhost id
??
Before connect, they added the key to the admin page.
And how to get a fingerprint key?
Thanks.
1. I would strongly suggest to use a newer version of OpenSSH, so you don’t have to patch it.
2. Here’s how you get the fingerprint from the public key: https://stackoverflow.com/questions/9607295/how-do-i-find-my-rsa-key-fingerprint