Simple Two-Factor SSH Authentication

23 09 2011

In a two-part post I’m going to show you some tricks you can do with SSH logins. This post covers setting up two-factor SSH authentication with the Google Authenticator app.

I was recently getting some servers in shape so I can pass the Payment Card Industry standards questionnaire and one requirement was two-factor authentication access to the server. I queried whether SSH key + passphrase was acceptable but didn’t get a clear answer so I figured I’d explore setting up another authentication factor myself, plus it piqued my interest.

After a bit of research I found it was possible using a PAM module but it doesn’t work along with SSH key authentication (only password authentication) and I only use SSH key logins for my servers.

The magic

I wanted to find the simplest method of implementing this so I started looking at what we can do with SSH itself. There is an option in the authorized_keys file that allows you to run a command when a user authorizes with a particular key eg.

command="/usr/bin/my_script" ssh-dsa AAA...zzz me@example.com

The command="..." part invokes a different command upon key authentication and runs the /usr/bin/my_script instead. Now we’ve got a starting point to work on the Google Authenticator logic.

Simple implementation

I’ve chosen ruby to implement this simple example but in theory you could use anything you want. This is a naive implementation but it will prove the concept. You’re going to need therotp library as well for this to work gem install rotp.

We put the following in /usr/bin/two_factor_ssh

#!/usr/bin/env ruby
require 'rubygems'
require 'rotp'
# we'll pass in a secret to this script from the authorized_keys file
abort unless secret = ARGV[0]
# prompt the user for their validation code
STDERR.write "Enter the validation code: "
until validation_code = STDIN.gets.strip
  sleep 1
end
# check the validation code is correct
abort "Invalid" unless validation_code == ROTP::TOTP.new(secret).now.to_s
# user has validated so we'll give them their shell
Kernel.exec ENV['SSH_ORIGINAL_COMMAND'] || ENV['SHELL']

The secret is in Kernel.exec which, upon successful validation, replaces thetwo_factor_ssh script process with the original command the user was attempting or their default shell so it is a completely seamless experience from that point on.

Generating the secret

We need to generate a secret token that is shared between the Google Authenticator app and the server.

Here’s a little script that will spit out a new token and a link to a QR code that can be scanned into the Google Authenticator application.

#!/usr/bin/env ruby
require 'rubygems'
require 'rotp'
secret = ROTP::Base32.random_base32
data = "otpauth://totp/#{`hostname -s`.strip}?secret=#{secret}"
puts "Your secret key is: #{secret}"
puts url

Running this produces:

We can scan the QR code directly into Google Authenticator and then update ourauthorized_keys file as follows:

command="/usr/bin/two_factor_ssh 4rr7kc47sc5a2fgt" ssh-dsa AAA...zzz me@example.com

That should do it!

Testing it out

[richard@mbp ~]$ ssh moocode@myserver
Enter the validation code: wrong
Invalid
Connection to myserver closed.
[richard@mbp ~]$
[richard@mbp ~]$ ssh moocode@myserver
Enter the validation code: 410353
moocode@myserver:~$

Great, that seems to work as expected.

Wrapping up

I’ve got a slightly more involved example that adds in support for ‘remember me’ by IP address for a fixed period of time so you don’t have to reach for the phone on every single login from the same IP.

The extended example also does some primitive logging but I’d like to add in a better auditing system (another PCI compliance requirement) as this would allow us to know which key is used to log into the server and whether they validated.

We should also probably have a fallback mechanism (a master key or 5 one-time codes like Google does) so we don’t inadvertently lock ourselves out of the server.

Article: moocode.com

Advertisements




RedBook – Daily Logging App

12 03 2008

Redbook allows you to log your daily activities during the day and comes with a great reporting function.

Read More 





Rush = Shell::New(’Ruby’, ‘Shell’)

22 02 2008

rush is a replacement for the unix shell (bash, zsh, etc) which uses pure Ruby syntax. Grep through files, find and kill processes, copy files – everything you do in the shell, now in Ruby.

More Info





Configuring Ruby Rails for Apache on SUSE Linux Enterprise Server

20 12 2007

I was browsing the net for a decent way to configure Apache and Rails and came across this article. I hope all you Ruby developers enjoy this…

Read Article





Create in Ruby Weblog in under a minute!

19 11 2007

 

 

 





Slow Posts

14 11 2007

Sorry about the slow posts, I’ve been developing Novell-Help.com in Ruby on Rails and its only me at this point. I’ll return in a few days.

Thanks