Skip to content

Instantly share code, notes, and snippets.

@thaJeztah
Forked from sankage/two_factor_ssh.md
Created December 24, 2015 21:02
Show Gist options
  • Save thaJeztah/cc609fda76c9c59cd66f to your computer and use it in GitHub Desktop.
Save thaJeztah/cc609fda76c9c59cd66f to your computer and use it in GitHub Desktop.
Two Factor Authentication for ssh

Simple Two-Factor SSH Authentication

Posted on September 23 2011 by Richard Taylor (@moomerman)

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 the rotp 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['SHELL']

The secret is in Kernel.exec ENV['SHELL'] which, upon successful validation, replaces the two_factor_ssh script process with the users 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}"
url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{data}"

puts "Your secret key is: #{secret}"
puts url

Running this produces:

Your secret key is: 4rr7kc47sc5a2fgt
https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/myserver?secret=4rr7kc47sc5a2fgt

We can scan the QR code directly into Google Authenticator and then update our authorized_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.

If you have any tips or ideas please leave them in the comments below and if you liked this post follow us on twitter so you'll get notified about next weeks follow-up post.

Comments

As noted alsewhere, ForceCommand is a nicer option, and can be done with something like this in sshd_config:

Match group yubikey
#       ForceCommand /usr/local/bin/yubikey.sh
       ForceCommand /usr/local/bin/mobileverification.sh

The commented out script is something I wrote to authenticate Yubikeys - see http://yubico.com/yubikey

And the mobileverification.sh sends a randomly generated 4 digit pin code to the phone number that user has saved in ~/.ssh/mobile_number and asks them to enter it.

Edit: in case anyone is interested, mobileverification.sh and yubikey.sh

Edit2: It's worth pointing out that both those scripts were written fairly quickly by me, for basic personal use. If you want to use them I would recommend going through them and making sure I didn't screw anything up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment