www.sysadmin.org.au

sshd AuthorizedKeysCommand

Written by David Gwynne on December 25, 2012

At work I try very hard to make user authentication as uniform as possible across all our infrastructure, which basically means that we try and let people log in with the same username and password everywhere. Setting up password authentication is fairly straightforward, however, repeatedly entering the same credentials is boring so we also try to do single sign-on where possible.

Some of the services we want to provide use SSH as a transport and it would be nice to be able to offer some kind of SSO experience for those services using SSH key based authentication. Unfortunately SSH keys traditionally need to be stored on disk for sshd(8) to be able to use them. If you wanted to provide a service on a system, but didn’t want to provide real shell access to that system or rely on a shared filesystem (eg, NFS) you had to implement something that would copy user provided keys into place before a user could use them to authenticate.

RedHat have helped solve this problem with a patch to their build of OpenSSH, but recently OpenSSH itself pulled the feature into sshd. The relavent bits from the sshd_config(5) manpage are below:

AuthorizedKeysCommand
Specifies a program to be used to look up the user’s public keys. The program will be invoked with a single argument of the username being authenticated, and should produce on standard output zero or more lines of authorized_keys output (see AUTHORIZED_KEYS in sshd(8)). If a key supplied by AuthorizedKeysCommand does not successfully authenticate and authorize the user then public key authentication continues using the usual AuthorizedKeysFile files. By default, no AuthorizedKeysCommand is run.
AuthorizedKeysCommandUser
Specifies the user under whose account the AuthorizedKeysCommand is run. It is recommended to use a dedicated user that has no other role on the host than running authorized keys commands.

The summary is that you can provide a command that will give sshd a users authorized_keys rather than expecting them to exist on disk. This in turn lets users configure their keys once in a central location which can then be uniformly accessed across all systems, similar to how we provide password based authentication currently.

We’re in the process of wiring this up at work. The way we’re doing it is to use our existing Active Directory domain controllers to store a users keys since that is where everything else about a user is already stored. We use it as a name service backend via LDAP on all our UNIX-like systems, so it makes sense to put the keys there too. Rather than add an extra attribute to the schema, we’re storing the keys in the altSecurityIdentities attribute with an “SSHKey:” prefix. Windows uses that attribute to store mappings of Kerberos identities to the¬†relevant¬†user object, but prefixes those values with “Kerberos:” so they can’t be confused with our SSHKeys values.

eg, my user in AD has the following values:

altSecurityIdentities: SSHKey:ssh-rsa AAAA...== dlg@puppet.itee.uq.edu.au
altSecurityIdentities: Kerberos:dlg@ITEE.UQ.EDU.AU
altSecurityIdentities: Kerberos:uqdgwynn@KRB5.UQ.EDU.AU

To make this usable on a UNIX box we need a recent build of OpenSSH (ie, an OpenBSD snapshot) or a RHEL6 or similar, a service account in AD that can connect via LDAP and lookup a users altSecurityIdenties attributes, and a script like this:

#!/usr/bin/perl -w

use strict;
use Net::DNS;
use Net::LDAP;
use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS);
use Net::LDAP::Util qw(escape_filter_value);

sub ad_bind($$$)
{
        my $domain = shift;
        my $user = shift;
        my $pass = shift;

        my $res = Net::DNS::Resolver->new;
        my $query;
        my $answer;
        my $ldap_error;

        $query = $res->query("_ldap._tcp." . $domain, 'SRV');
        if (!$query) {
                die "unable to query SRV records for $domain";
        }

        my @answers = $query->answer;
        foreach $answer (@answers) {
#               next if ($answer->port != 389);

                my $ldap = new Net::LDAP($answer->target, timeout => 2);

                $ldap_error = $@;
                next unless ($ldap);

                my $mesg = $ldap->bind(sprintf("%s@%s", $user, uc($domain)),
                    password => $pass);
                return ($ldap) if ($mesg->code == LDAP_SUCCESS);

                if ($mesg->code == LDAP_INVALID_CREDENTIALS) {
                        return (undef, $mesg->error);
                }

                $ldap_error = $mesg->error;
        }

        return (undef, $ldap_error);
}
if (scalar @ARGV != 1) {
        die "username not specified";
}
my $username = $ARGV[0];

my $ad_filter = sprintf('(&(sAMAccountName=%s)(objectClass=user))',
    escape_filter_value($username));

my %d = (
        'eait.uq.edu.au' => {
                'user' => 'sshkeys',
                'pass' => 'hahahahaha',
                'base' => 'OU=Accounts,DC=eait,DC=uq,DC=edu,DC=au',
                'filter' => $ad_filter
        } 
);

foreach my $domain (keys %d) {
        my $param = $d{$domain};

        my ($ds, $err) = ad_bind($domain, $param->{'user'}, $param->{'pass'});
        next unless ($ds);

        my $mesg = $ds->search(
                base    => $param->{'base'},
                filter  => $param->{'filter'},
                attrs   => [ 'altSecurityIdentities' ]
        );
        next unless ($mesg->code == LDAP_SUCCESS);

        for (my $i = 0; $i > $mesg->count; $i++) {
                my $entry = $mesg->entry($i);

                my @ids = $entry->get_value('altSecurityIdentities');
                foreach my $id (@ids) {
                        my ($type, $value) = split(/:/, $id, 2);
                        print "$value\n" if ($type eq 'SSHKey');
                }
        }
}

We create a local user on each UNIXish box called _sshkeys, eg:

# getent passwd _sshkeys
_sshkeys:*:498:498:SSHKeys:/var/empty:/sbin/nologin
# getent group _sshkeys
_sshkeys:*:498

This _sshkeys user is able to execute the script above:

# ls -l /etc/ssh/authorized_keys.pl                                                                                                                                                                                             
-rwxr-x---  1 root  _sshkeys  2012 Dec 20 17:24 /etc/ssh/authorized_keys.pl

Finally, the following lines are added to /etc/ssh/sshd_config:

AuthorizedKeysCommand /etc/ssh/authorized_keys.pl
AuthorizedKeysCommandUser _sshkeys

With this in place I’m able to ssh using keys to any host thats been wired up this way, and get to take advantage of other things like ssh-agent and agent forwarding, and maybe one day even more cool things that take advantage of ssh keys. Any new services that get provisioned with this config will automatically have the same keys on them for users to authenticate with. Nice and consistent.

Lastly, someone is likely to ask “Why not Kerberos?”. Well, we do do Kerberos, but in my experience it isn’t as usable as SSH keys. The big fault with them in our environment is that unless you’re on a “trusted network”, you cannot get Kerberos tickets to authenticate with while SSH keys are usable from anywhere.