The bleeding-edge OpenSSH supports using an AuthorizedKeysCommand statement in sshd_config to get the authorized_keys file for a user. This lets you store your authorized_keys files in LDAP, but avoids linking OpenSSH against OpenLDAP. (You could actually use any data store for your back end, but LDAP is both the most popular and what I have.)
Your AuthorizedKeysCommands script should take one argument, and return a series of authorized keys, one per line. CentOS has a script, which I previously mentioned, and one of my readers was kind enough to put together an OpenBSD port for it.
But there’s some problems with the CentOS approach, from a cross-platform perspective. While it’s fine on CentOS, porting and installing it on other platforms is more difficult than it needs to be. I’d really like something that has very few dependencies, is easy to install, and is easily portable across platforms.
When setting up a couple of Ubuntu boxes I came across an Ubuntu AuthorizedKeysCommand script. His approach is much simpler than the CentOS approach, but his script didn’t work for me out of the box, and it wouldn’t work on other operating systems without modification. But it inspired me to write my own script, one that works across all of my operating systems, using only the tools I already have on all of my servers. My results follow. But before I offer my script, I feel obliged to warn you.
The bad news is, I am not a programmer. I have been told by multiple independent parties that my code makes the Baby Jesus cry.
The really bad with this is that you need an ldapsearch command that returns your public keys, and this command is hard-coded with this script. The ideal script would read the system ldap config, but you’d probably have to hard-code a path to ldap.conf anyway.
The appalling news is, I wrote it in Perl. Why? Well, the evidence indicates that I am insane. But every server in my environment runs Perl, and it’s been ten years since I used enough sed/awk to make this work. A proper authentication credentials script would not use Perl.
I’m not suggesting that you take my code. I’m hoping to motivate one of my programmer readers, whose code only makes the Baby Jesus grimace a little, to do better. I mean, I’ve set a pretty low bar here.
#!/usr/bin/perl
#takes one argument, the username to get the keys for
die unless $ARGV[0];
open (LDAP, "/usr/bin/ldapsearch -L -xZb\"dc=michaelwlucas,dc=com\" '(&(objectClass=posixAccount)(uid=$ARGV[0]))' sshPublicKey |") || die "ldapsearch failed $!\n";
while (<LDAP>) {
next if /^#|^version|^dn\:|^\s*$/;
s/\n//;
s/\://g;
s/sshPublicKey/\n/;
s/^ //;
print;
}
print "\n";
One catch with this is that you’re processing the raw output of ldapsearch. An acceptable ldapsearch will return keys that look something like this:
...
sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAw9zmtbk8bT+7OVpVvzuYYQqRhF8j
...blah, blah, blah...
ilXzBw== rsa-key-20110114
Initially, my LDAP searches returned some keys that looked like this:
...
sshPublicKey:: c3NoLWRzcyANCkFBQUFCM056YUMxa2MzTUFBQUNCQVBhZmNhemdmUHI1T29DdFp
...blah, blah, blah...
y5sb2NhbA==
This key doesn’t start with ssh-rsa or ssh-dss, and it doesn’t even look like a SSH key. But if you look at the key in a graphical tool like phpLDAPAdmin, it has the leading ssh-rsa. In my case, the key had an extra carriage return at the end of the key, which meant that ldapsearch had to base64 encode it. Get rid of extra whitespace. The LPK patch doesn’t notice that space, but a simple external script does.
Of course, if someone wanted to write an AuthorizedKeysCommand that was portable, handled that encoding, and didn’t suck as bad as mine, it wouldn’t hurt my feelings at all. Really.
AuthorizedKeysCommand will only ever be called for users that exist, but I’d still recommend rejecting $ARGV[0]s that contain any characters that could be interpreted by the shell or by LDAP itself. E.g.
$ARGV[0] =~ /[^a-zA-Z0-9._-|/ && die “bad characters in username”
Thank you, sir! That’s basic code security, I should know that. But I warned you all not to trust my code…
This is a little cleaner, but hasn’t been tested. I don’t have an OpenLDAP setup anywhere to test, so take it with a grain of salt. This includes the suggested modification by djm (though I corrected the ending of his regular expression check to use a closing brace rather than a pipe character. If this was not an error, I apologize, but it looked like a typo to me.) Perl has a core module for handling Base64, so I included it for the white space keys. Hopefully this helps…
#!/usr/bin/perl
#takes one argument, the username to get the keys for it
#Core perl module may be needed to decode keys with extra newlines
use MIME::Base64 ();
#set up our LDAP stuff
$DOMAIN = “dc=michaelwlucas,dc=com”
#Check the argument exists and is sane
die unless $ARGV[0];
$ARGV[0] =~ /[^a-zA-Z0-9._-]/ && die “bad characters in username”
open (LDAP, “/usr/bin/ldapsearch -L -xZb\”$DOMAIN\” ‘(&(objectClass=posixAccount)(uid=$ARGV[0]))’ sshPublicKey |”) || die “ldapsearch failed $!\n”;
while () {
next if /^#|^version|^dn\:|^\s*$/;
s/\n//;
s/\://g;
s/sshPublicKey/\n/;
s/^ //;
if ( $_ =~ /^ssh.*/ ) {
print;
} else {
print MIME::Base64::decode($_);
}
}
print “\n”;
Stefan,
Thanks, but the Base64 chunk never trips. I’ve played with a bit, and it appears that the key always matches ^ssh.*. Weird. Tried on OpenBSD and Ubuntu. If you really want to play with this, I could send you a text file of ldapsearch output to play with.
That could work. I’m not sure why it would trip on ^ssh.* when it’s Base64 encoded, because the line doesn’t include that when it is based on the description you gave 🙁 I’d be more than happy to use a file to test this, though and try to figure out why it’s tripping.
Stefan, files are on the way. I tried to figure out why ^ssh.* would trip the regex because, well, it shouldn’t. Thanks!
I’ve played with the script as written and sent you back the updated script in a tarball. The file is coming from my work address, so if it doesn’t arrive, let me know, and I’ll forward it from the address you have already.
Another issue I just thought of. The script may need to be modified a bit further to handle any keys stored with options that come before the “ssh-” portion of the key. For example:
command=”do my thing here”
from=”only the server I trust here”
It might be better to change it to check for a base64 hash and decode that, then default to printing the key as is if it doesn’t need to be decoded. Just a thought I had… 🙁
I run OpenSSH – LPK which Im sure you are familiar with. Because it simply requires libldap and a compile I run my own repository with it.
May not be the best idea in your situation, but is simply a matter of ./configure && make && make install.
I’ve had to merge upstream manually though.
Just a thought if you want to have it run on an ancient version of obsd like myself.