In many high security (top secret) or ultra paranoid environments multi factor authentication is required. In order to log into a machine you need to provide many proofs of your identity. An authentication factor is a piece of information and process used to authenticate or verify the identity of a person requesting access under security constraints. For example, two-factor authentication (TFA) is a system wherein two different factors are used in conjunction to authenticate. Using more than one factor (single factor or SFA) is sometimes called strong authentication. However, strength is always bound to secrecy under which the factors are kept and protected against any third party challenge. In order to increase security one can increase the amount of independent authentication methods used.
Multi factor authentication (MFA) is comprised of three(3) or more of the following:
OpenSSH can help you better secure remote access by adding another authentication layer to the system by using the ForceCommand directive and the ssh_gatekeeper script. By definition true multi-factor authentication requires the use of solutions from two or more of the three categories of factors. Using multiple solutions from the same category would not constitute multi-factor authentication
To practice good security using multi-factor (very strong) authentication through OpenSSH you should at least:
The Gatekeeper script will require the user to enter the correct string before being allowed a shell and access to the system. This adds another layer of authentication as the string can be constantly changing. If someone steals your ssh key, especially if it is pass phrase-less, or gets a hold of your password they will still need to deduce what the gatekeeper expects as an answer. Lets take a look.
The gatekeeper will run after the user provides a valid password or pre-authorized ssh key for a known username on the machine. Then, the sshd daemon will run the ssh_gatekeeper.pl script using ForceCommand. Once authenticated the user will be awarded any shell listed in /etc/passwd under their username including bash, sh, ksh, csh or tcsh. The user will not be able to break out of the script and must provide the correct answer to receive shell access according to the script's question. If the script receives anything other than the correct response the connection is closed.
The ssh_gatekeeper authentication script currently provides two independent methods you can choose to authenticate with. You can use Google Authenticator or a custom string you design yourself. Both options are full documented inside the script itself.
Google Authenticator works with a tablet, phone or script and provides a six(6) digit numerical PIN to enter to pass the gatekeeper script. The PIN changes every 30 seconds and is based on the current time and your own secret key. We use the Time-based One-time Password (TOTP) implementation which is supported by Android, IOS and Blackberry apps.
Custom String is simply a string you design yourself and a good choice if you do not wish to use the Google Authentication option. The string can include the date so it changes making it more difficult to guess. You can put anything into the variable like a string, an equation or put a logic if-then tree in to make it even more difficult to know. We highly suggest using some logic that changes periodically, but the client can remotely deduce. The example in the script uses a string based on the current minute and day.
With regards to Google Authentication: Understand that there is NO communication made between your machines and Google or the ssh_gatekeeper script and your phone/tablet. The script uses your secret key and the current date to calculate the six(6) digit pin. Independently, the phone or tablet using the Google Authenticator app does the exact same calculation. If both the ssh script and the phone app provide the same numerical string, then the gatekeeper script grants a shell.
First, edit the ssh server's /etc/ssh/sshd_config and add the ForceCommand directive pointing to the full path you will be putting the ssh_gatekeeper.pl script at. The location does not matter as log as all users who will be logging onto the system can execute it. ForceCommand will make sshd execute this script when any client tries to login to the machine. Make sure you restart the sshd server so the changes take effect.
ForceCommand /tools/ssh_gatekeeper.pl
Next, if you are going to use the Google Authenticator option you will need add a few additional perl modules on the sshd server box. The modules are used to do the calculation of the current time and your Base32 encoded secret key into the six(6) digit pin for verification. All of the modules are easily installed through Ubuntu's apt-get or FreeBSD's ports or package system.
Ubuntu apt-get install libtime-local-perl libmime-base32-perl libsys-syslog-perl libdigest-hmac-perl FreeBSD ports /usr/ports/devel/p5-Time-Local /usr/ports/converters/p5-MIME-Base32 /usr/ports/security/p5-Digest-HMAC
Google Authenticator requires that you generate a 16 digit Base32 key to be used on your phone or tablet and also in our script. We wrote a simple perl script to generate random secret keys. The script will loop a random amount of extra key generations to increase entropy on the system and then print out the 16 digit key on the final loop. Copy the following script and make it executable. We called the script google_auth_secret_generator.pl , but you can call it anything you like. Just run the script with no arguments to generate a random key. Run it as many times as you want. When you are happy with a string keep it safe. You will be entering this string into the Google Authenticator phone app and into the ssh_gatekeeper script.
#!/usr/bin/perl -T # Calomel.org -- google_auth_secret_generator.pl # # Generate 16 digit Base32 secret key for Google Authenticator. Additional # system entropy is created by running extra key generations. use strict; use warnings; use MIME::Base32 qw( RFC ); # while loop counter and seed variable. my $count = 0; my $seed = ""; # run a simple while loop to generate system entropy. Additional entropy might # help since rand() is not the best random number collector, but good enough # for this task. At the end of the run send the $seed to be base32 encoded. while ($count <= int(rand(100000)) + int(rand(10000)) * int(rand(1000))) { # Randomly generate a 10 character seed string my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(! @ $ % ^ & *) ); $seed = join("", @chars[ map { rand @chars } ( 1 .. 10 ) ]); $count++; } # encode the 10 character sudo random string into a base32 16 character string # for use in the Google Authenticator app. my $encoded = MIME::Base32::encode($seed); # print the code print "\nNumber of iterations for entropy: $count\n"; print "Google Authenticator Secret Key : $encoded\n\n"; ######### EOF #########
Go to the application store on your mobile device and install the free Google Authenticator (android) or Google Authenticator (apple IOS) app. The app is available for free on android, apple IOS and blackberry. Once installed, enter the secret code string you generated using the "google_auth_secret_generator.pl" script above into the app. You should see the app generating a new six(6) digit numerical PIN every 30 seconds or so.
If you do not have a mobile device you are welcome to use our "google_auth_pin.pl" perl script found later on this page.
Now that openssh (sshd) is configured using the ForceCommand and you have generated a Google Authenticator secret key, copy the following script to a location where all users will be able to execute it. We placed the script in /tools/ssh_gatekeeper.pl and set the permissions to "chmod 755" with the owner as root (chown root). Notice this is the same script location we used for the sshd ForceCommand directive in the first step. When the ForceCommand directive runs the script it will execute as the user attempting log in.
NOTE: we heavily commented every option and method in the script. Please take a few minutes to read through the script and see what additional changes you may want to make. For example, your Google Authenticator secret key needs to be placed in the script for the $secretkey variable or in a file in the user's home directory. The script's comments explain in detail.
#!/usr/bin/perl -T # Calomel.org # ssh_gatekeeper.pl # version 0.18 # # ssh_gatekeeper is used to add a second authentication method to interactive # ssh log in. The script is called by sshd_config's ForceCommand directive # after the user provides the correct password, ssh key or ssh key with pass # phrase. The user is required to enter the PIN from Google Authenticator or a # custom string _before_ being awarded a valid shell on the system. Any other # attempt to break out of the script or give an incorrect answer will kill this # processes and sever the ssh connection. Full logging of all connections are # sent to syslogd. use strict; use warnings; use POSIX qw(strftime); use Sys::Syslog qw( :DEFAULT setlogsock); # catch all attempts to exit the script and force an exit. $SIG{'INT' } = 'abort_exit'; $SIG{'HUP' } = 'abort_exit'; $SIG{'ABRT'} = 'abort_exit'; $SIG{'QUIT'} = 'abort_exit'; $SIG{'TRAP'} = 'abort_exit'; $SIG{'STOP'} = 'abort_exit'; # User options ################################################################### # Option: What greeting string do you want to display to the user when they # attempt to log in ? "\n" are new line characters. my $greeting = "\n\n Intelligence is the ability to adapt to change.\n\n\n"; # Option: Which authentication method do you want to use? The options are # "google" for google authenticator and "custom" for a string you make yourself # in the custom_string subroutine found below. #my $authentication = "custom"; my $authentication = "google"; # Option: if you are using Google Authenticator you have two options. You can # read a file in the user's home directory for the secret key or, if you are # the only user using this script, you can just put your secret key here. If # this script is being used for more then one user put the string "multi-user" # for the $secretkey and the script will read the file # $home/.google_authenticator . The .google_authenticator file should # contain a single line with only the secret key and the permissions should be # no more then 400 for security. If multi-user is chosen and the # .google_authenticator file does not exist the script will exit and drop the # connection. If you are the only user of this script and you want to put your # secret key here then no external file will be read. #my $secretkey="multi-user"; my $secretkey="JJUEEUDOIJAFCZCC"; # Option: Do you want to append any characters to the end of your password? # This option is for the truly paranoid and makes the password a lot harder to # crack. For example, Google Authenticator has 6 numerical digits. You can # append more characters to the end in case someone gets hold of your secret # key. Then the attacker would still need to know your appended string. If the # Google Authenticator code was 123456 and we added "0.0,8-" our true password # would be "1234560.0,8-" . Again, if the GA code is "456789" the password # would be "4567890.0,8-" . You can add any of the following safe characters in # any combination: a-z A-Z 0-9 . , - + = # : #my $additional_pass_chars = "0.0,8-"; my $additional_pass_chars = ""; # Option: Exactly how many characters are in your password? Set the minimum and # maximum amount of characters allowed in the password. Remember, if you added # extra characters to the $additional_pass_chars option you need to add that # number to the total. For example, Google Authenticator has 6 digits. So we # would enter a minimum of at least 6. If we added six(6) characters to # $additional_pass_chars then the total would be 12 (6+6=12) and we would need # to make sure the length maximum was at least 12. We recommend using a 12 # character password or greater. my $pasword_length_min = 6; my $pasword_length_max = 24; # Option: Some protocols can not use two factor authentication because they # need a clean shell environment. rsync, scp and sftp (sshfs uses sftp) are # part of this exception. Also, if you try to pass a command with ssh then the # extra command will be denied. For example, if you "ssh user@machine ls" to do # a non-interactive ls on the remote machine you will be denied unless you add # "ls" as a clean command. WARNING: you can also do an "ls -la" and even "ls # -la;rm -rf *" since ls is still the primary command. Be careful about what # you allow. If you do not use a protocol, remove it. If you wanted to be # sneaky you could make an executable script in the $PATH called something like # "AA" on the server that does nothing. Then add "AA" to our clean protocols # list. You could then do an ssh user@machine "AA;ls -al". This would execute # AA, which does nothing, and then ls -la. The difference is no one would ever # pass your system the AA command since it is not a common program. Sort of a # non-interactive command pass-through with AA pass the password. You must # enter the protocol as a string the same way the sshd server will execute the # command. This means rsync is just "rsync. sftp is actually the full path to # the sftp-server binary. #my @clean_protocols = ("rsync","/usr/lib/openssh/sftp-server","scp","ls", "AA"); my @clean_protocols = ("rsync","/usr/libexec/sftp-server"); ################################################################### # declare global variables, redefine PATH for safety and open syslog logging # socket. my $code="invalid_code"; my $code_attempt="invalid_code_attempt"; $ENV{PATH} = "/bin:/usr/bin:/usr/local/bin"; setlogsock('unix'); # collect and untaint environment values. It is very important we scrub the # environmental variables so a bad user can not send illegal commands into our # script. If you run the script and it logs abort_exit in /var/log/messages # output then the script could have exited on one of the following lines. Each # line only allow certain characters from each environmental variable. Illegal # characters will make the script abort_exit. my $ssh_orig = ""; my $username = "$1" if ($ENV{LOGNAME} =~ m/^([a-zA-Z0-9\_\-]+)$/ or &abort_exit); my $ssh_conn = "$1" if ($ENV{SSH_CONNECTION} =~ m/^([0-9\. ]+)$/ or &abort_exit); my $usershell = "$1" if ($ENV{SHELL} =~ m/^([a-zA-Z\/]+)$/ or &abort_exit); my $home = "$1" if ($ENV{HOME} =~ m/^([a-zA-Z0-9\_\-\/]+)$/ or &abort_exit); if (defined($ENV{SSH_ORIGINAL_COMMAND})) { $ssh_orig = "$1" if ($ENV{SSH_ORIGINAL_COMMAND} =~ m/^([a-zA-Z0-9\_\/\~\-\. ]+)$/ or &abort_exit); } # SSH standard interactive access with two factor authentication. The following # function is for interactive SSH when the user just want to log into the # machine. You can choose the authentication method and the sub routines will # be called farther below in the script. if ( ! defined $ENV{SSH_ORIGINAL_COMMAND} ) { &user_login_attempt; &google_authenticator if ( $authentication eq "google"); &custom_string if ( $authentication eq "custom"); &validate_input; } # @clean_protocols access like rsync, scp, sftp, and sshfs. If the user passes # a command to ssh (like ssh user@machine ls) or uses another protocol through # SSH this function will be run. We carefully separate the command shell of the # user and allow any of our pre-defined @clean_protocols. if ( defined $ENV{SSH_ORIGINAL_COMMAND} ) { my @scp_argv = split /[ \t]+/, $ssh_orig; foreach (@clean_protocols) { if ( $scp_argv[0] eq "$_") { # Sanitize rsync so a sneaky user can not use rsync as a tunnel to execute # system commands. rsync is for coping files only. if ( ( "rsync" eq "$_") && ($ssh_orig =~ /[\&\(\{\;\<\`\|]/ ) ) { &logger("DENIED","dirty rsync: $ssh_orig"); &clean_exit; } # Log the connection and allow the @clean_protocols to run in a shell &logger("ACCEPT","clean_protocol: $_"); system("$usershell", "-c", "$ssh_orig"); &clean_exit; } } # deny the connection if the previous checks have failed &logger("DENIED","UN_clean_protocol: $ssh_orig"); &clean_exit; } sub user_login_attempt { local $SIG{ ALRM } = sub { &clean_exit; }; # The user has this many seconds to complete the authentication process # or we kill the connection. No lollygagging. alarm 60; # The personal greeting when a user attempts to authenticate print $greeting; # WARNING: This is the only time we ask the remote user for input. STDIN is # completely untrusted and must be validate and checked. Use stty to not # echo the password from the user and accept their input. system("/bin/stty", "-echo"); chomp($code_attempt = <STDIN>) or &abort_exit; # The password we will accept is between $pasword_length_min and # $pasword_length_max length and may only consist of alphanumeric, comma, # dash, plus, equal, pound, colon and period characters. All other input # aborts the script and never awards a shell. If the input is validated the # script considers the string untainted. my $inputlength = length($code_attempt); if ( $inputlength < $pasword_length_min || $inputlength > $pasword_length_max ) { &logger("DENIED","bad pass length: $inputlength characters"); &clean_exit; } elsif ( $code_attempt !~ m/^([a-zA-Z0-9\,\-\+\=\#\:\.]+)$/ ) { &logger("DENIED","invalid password characters"); &clean_exit; } else { # Accept the user's now untainted input. $code_attempt = "$1" } alarm 0; } sub google_authenticator { # Delay module loading until the user sends valid input. There is no need # to load modules if the user fails the input sanity checks. You will need # these three modules installed on the sshd server machine in order to use # the Google Authenticator feature. They are all available through FreeBSD # ports or Ubuntu's apt-get. eval " use Time::Local; use MIME::Base32; use Digest::HMAC_SHA1 qw(hmac_sha1); "; # For multi-user systems the private 16 digit secret key is read from the # file in the user's home directory. Do not share this the key with anyone # and make sure the permissions on the file are no more then 400. This key # is the exact same string you put into the Google Authenticator phone or # tablet app. If you defined your secret key in the options above this # statement is not run. if ( $secretkey eq "multi-user" ) { $secretkey = do { local $/ = undef; open my $file, "<", "$home/.google_authenticator" or &abort_exit; <$file>; }; } # Calculate the valid PIN using the secret key and current date. The result # in $code will match the 6 digit pin from the Google Authenticator phone # app. my $tm = int(time/30); $secretkey=MIME::Base32::decode_rfc3548($secretkey); my $b=pack("q>",$tm); my $hm=hmac_sha1($b,$secretkey); my $offset=ord(substr($hm,length($hm)-1,1)) & 0x0F; my $truncatedHash=substr($hm,$offset,4); $code=unpack("L>",$truncatedHash); $code=$code & 0x7FFFFFFF; $code%=1000000; $code = join( "", "0", $code) if (length($code) == 5); # Take the google PIN number of 6 digits and add the # additional_pass_chars option if we defined it in the options section. $code = join( "", $code, $additional_pass_chars); } sub custom_string { # Instead of using Google Authenticator, define some random string which # could even include the time or date to make the code more random and # difficult to guess. For example, lets make the password string # "m,56Wed--" when the Date and time are "Wed May 1 14:56:22 EDT 2033". # Use the local time of the machine to collect the minute and day using # "%M%a" which is "56Wed". Then use the join function to add "m," to the # date string "56Wed" and the final "--" string. This way you have a # password which changes every minute and you can deduce on the fly using # any moderately accurate time source. my $date_string = strftime "%M%a", localtime; $code = join( "", "m,", $date_string, "--", $additional_pass_chars); } # Validate the user input and, if correct, reward a shell sub validate_input { if ( $code eq $code_attempt ) { # turn command line echo back on so the logged in user can see what they are typing. system("/bin/stty", "echo"); # log the successful shell being awarded &logger("ACCEPT","interactive ssh"); # Award a shell to the user, using their preferred shell, and exit the # script when they log out of the machine. If this script is killed for # any reason also kill the user's shell. setpgrp $$, 0; system("$usershell", "--login"); END {kill 15, -$$} &clean_exit; } else { # log the failed attempt &logger("DENIED","bad password"); # DEBUG: log the user attempted password and the actual password. #&logger("DENIED","input: $code_attempt valid: $code"); &clean_exit; } } # global logger to send data to syslogd. We accept two(2) arguments. First is # the message about ACCEPT or DENIED access. The Second argument is extra # information for the end of the log line like "bad password". sub logger { openlog($0,'pid','$username'); syslog('info', "$_[0] $username from $ssh_conn $_[1]"); closelog; } # If the user or script exits normally the script will pass an exit code of # zero(0). sub clean_exit { &logger("EXIT ","clean exit"); exit(0); } # This is the final catch all exit routine and passes an exit code of one(1). sub abort_exit { &logger("ABORT ","caught bad script exit: $ssh_orig"); exit(1); } &abort_exit; ######### EOF #########
Ssh to the server machine as a valid user. You should first have to enter your password or use your ssh key. The greeting message will print from the ssh_gatekeeper script. If you are using the Google Authenticator method enter the six(6) digit PIN from the tablet or phone Google Authenticator (android for example) app. If the PIN code you entered, provided by the phone app, is the same as the gatekeeper script calculated your user will be awarded its shell. If not, the connection will be severed immediately. All login attempts are logged to syslogd and you can find them in /var/log/messages on the server machine.
Not a problem. If you like the idea of a time based key and still want to use this access type from another machine we have made a perl script to generate the six(6) digit PIN from your secret key and current time on the command line. This function is the same method used in the ssh_gatekeeper.pl script, but cleaned up for client side only use.
When you want to log into the remote machine just run this script on the client machine and it will print the pin. Please make sure you always keep your secret key secure by only allowing your user to execute the script. Also, make sure the time is always accurate on all of your machines by using ntp.
#!/usr/bin/perl -T # Calomel.org google_auth_pin.pl # # The google_auth_pin.pl script will print out the six(6) digit pin using the # current date and your Google Authenticator secret key. This is the same # calculation done by the phone app. use strict; use warnings; use POSIX qw(strftime); use Sys::Syslog qw( :DEFAULT setlogsock); use Time::Local; use MIME::Base32; use Digest::HMAC_SHA1 qw(hmac_sha1); # declare global variable my $code = ""; # the secret key of the machine you are connecting to my $secretkey="JJUEEUDOIJAFCZCC"; # Calculate the valid PIN using the secret key and current date. The result in # $code will match the 6 digit pin from the Google Authenticator phone app. my $tm = int(time/30); $secretkey=MIME::Base32::decode_rfc3548($secretkey); my $b=pack("q>",$tm); my $hm=hmac_sha1($b,$secretkey); my $offset=ord(substr($hm,length($hm)-1,1)) & 0x0F; my $truncatedHash=substr($hm,$offset,4); $code=unpack("L>",$truncatedHash); $code=$code & 0x7FFFFFFF; $code%=1000000; $code = join( "", "0", $code) if (length($code) == 5); # print out the six(6) digit pin to the command line. print "\nGoogle Authenticator PIN: $code\n\n"; ######### EOF #########
Another solution is the Google Authenticator project which includes the implementation of one-time passcode generators for several mobile platforms, as well as a pluggable authentication module (PAM). One-time passcodes are generated using open standards developed by the Initiative for Open Authentication (OATH) (which is unrelated to OAuth). The PAM module can add a two-factor authentication step to any PAM-enabled application and can provide system wide two-factor authentication.