Thomas Touhey

Computer enthusiast.

OpenSSH Public Key Authentication

Recently, I have discovered the power of using keys to authenticate on SSH services such as simple ones (servers I help to administrate), git shells (such as for Github or any Gitlab or Gitolite instance), and other shells I call "action shells" such as the one for Archlinux's AUR. But to some, there is some magic to how, when you connect to the same user (usually git) as everyone else, only with your certificate, the distant shell manages to identify you. For example:

Hi cakeisalie5! You've successfully authenticated, but GitHub does not provide shell access.

So eventually, I got used to this, and wanted to set up my own for custom commands, such as updating a site (by pulling and re-building a Jekyll repository) with a local command looking like ssh adm@domain.org update domain.org. I've already made an interactive shell (see Make a shell service, like in the 80s!) before, but now, the subtility is that I'll have to authenticate the user using its public key, and take the command line from its original command line (such as update domain.org).

The "what SSH server will I use for this" question already has an answer, as there is a de facto standard for this: OpenSSH. It is free software, and it provides clients (ssh, scp, sftp), the server sshd and utilities such as sftp-server or ssh-agent. If you're using any GNU/Linux distribution, you'll be using OpenSSH as a client and a server.

So my first track for trying to identify the user was to look the environment variables OpenSSH transmitted to the shell, and there indeed was a few, such as SSH_ORIGINAL_COMMAND (which we will use!), but nothing related to the identified user or to the public key he used. After a few hours of searching, I finally found where the heart of the battle was: the authorized_keys file.

On a basic OpenSSH configuration, when you try to login to your account using a public key, OpenSSH will open your ~/.ssh/authorized_keys and lookup for your key. If it finds it, it logs you in, and otherwise, it will probably tell you something like Permission denied (publickey).. Well, this file can actually do more than that: for each public key, you can define a few things the user will be logged in with, such as various OpenSSH options... or the command! So a trick consists of setting the command with a different argument for each key in this file, for example:

command="/opt/ssh-update/shell first-key" ssh-rsa <key>
command="/opt/ssh-update/shell secnd-key" ssh-rsa <key>

Then, in your shell, just check the first argument to see who the user is! Don't forget to put this in your sshd configuration, usually /etc/ssh/sshd_config, in order to only enable key-based authentication (the user would be adm in my example):

Match User <user>
    PasswordAuthentication no

And don't forget to reload/restart sshd, using service sshd reload on Debian, or systemctl reload sshd with systemd.

Now, you should know there is another technique that can be more practicle. The first one works on all recent GNU/Linux distributions, is used by Gitlab, and "only" requires you to regenerate the ~/.ssh/authorized_keys on every public key update. The one that I'm about to show you relies on a mechanism that is only implemented and useful since OpenSSH 6.9, and some widely spread and still maintained distributions use an older version of OpenSSH, such as Debian Jessie (Debian's oldstable at the time I'm writing this post), that uses OpenSSH 6.8. It is used by Archlinux's AUR, but any project that aims at being usable on all widely spread maintained GNU/Linux distributions should rely on the first technique.

This technique relies on AuthorizedKeysCommand. This configuration option is there since 6.2, and allows you to generate an authorized_keys file each time an user is to log in. The thing is, this doesn't scale up, as this means thousands of lines have to be generated each time a user logs in, so relying on a file that is only generated when updated is really better. But in 6.9, a neat feature was implemented: you could use tokens in the AuthorizedKeysCommand! For example, you can set this option to /opt/ssh-update/auth "%t" "%k", and each time a user is to log in, the key type and base64-encoded key is sent to your custom authentication utility, which can then only produce zero or one line of authorized_keys for OpenSSH to read. This means you don't have to generate a static ~/.ssh/authorized_keys anymore, as a custom one will be generated at each connexion!

To configure this, we will just have to update the previous block:

Match User <user>
    PasswordAuthentication no
    AuthorizedKeysCommand /opt/ssh-update/auth "%t" "%k"
    AuthorizedKeysCommandUser <user>

Here's an example /opt/ssh-update/auth utility, coded in Python:

#!/usr/bin/env python3
import sys

cake_key = "..."
if sys.argv[1] != "ssh-rsa" or sys.argv[2] != cake_key:
    exit(1)
print('command="/opt/ssh-update/shell cake" ssh-rsa %s cake@thing'%cake_key)

And here's an example /opt/ssh-update/shell utility, coded in Python, too:

#!/usr/bin/env python3
import os, sys

print("Hello, %s!"%sys.argv[1])
if not 'SSH_ORIGINAL_COMMAND' in os.environ:
    print("No command?")
else:
    print("Your command was: %s"%os.environ['SSH_ORIGINAL_COMMAND'])

Of course, you can choose to do many things with this:

  • Make a utility that statically generates the ~/.ssh/authorized_keys from a database (if you're using the first technique);

  • Get the user name out of a key using a database (if you're using the second technique);

  • Implement the git SSH commands git-receive-pack and git-upload-pack;

  • The limit is your imagination!

Thomas Touhey

Computer enthusiast.