diff --git a/README.md b/README.md index c6b2c5a..4dd2a6b 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ Type the following command in the terminal: - $ curl -O 'https://raw.githubusercontent.com/authy/authy-ssh/master/authy-ssh' + $ curl -O 'https://raw.githubusercontent.com/DigitalDJ/authy-ssh/master/authy-ssh' $ sudo bash authy-ssh install /usr/local/bin Then enable two-factor for your user: - $ sudo /usr/local/bin/authy-ssh enable `whoami` + $ sudo /usr/local/bin/authy-ssh enable `whoami` [grace-period] Test everything is working: @@ -45,7 +45,7 @@ Restart your SSH server (look below if you are not on Ubuntu). Type the following command in the terminal: - $ curl 'https://raw.githubusercontent.com/authy/authy-ssh/master/authy-ssh' -o authy-ssh + $ curl 'https://raw.githubusercontent.com/DigitalDJ/authy-ssh/master/authy-ssh' -o authy-ssh $ bash authy-ssh install ~/.authy-ssh/ @@ -64,7 +64,7 @@ To enable users type the following command and fill the form: If you want to do it in one line just type: - $ sudo authy-ssh enable + $ sudo authy-ssh enable [grace-period] ## How it works @@ -79,11 +79,17 @@ Here's an example: [root@ip-10-2-113-233 ~]# cat /usr/local/bin/authy-ssh.conf banner=Good job! You've securely logged in with Authy. + load_default_banner=enable api_key=05c783f2db87b73b198f11fe45dd8bfb - user=root:1 - user=daniel:1 + user=root:1:-1 + user=daniel:1:300 In this case it means user root and daniel have two-factor enabled and that 1 is their `authy_id`. If a user is not in this list, `authy-ssh` will automatically let him in. +The user daniel has an optional `grace-period` of 300 seconds, allowing them to open a new session within 5 minutes of the last successful login without requiring two-factor authentication. +On the other hand, the root user uses the default `grace-period` of -1, requiring all sessions to use two-factor authentication, regardless of recent successful logins. + +The `load_default_banner` option will show the operating system's default SSH banner when a successful login occurs. This checks to see if a MOTD is set in /etc/pam.d/sshd or /etc/motd. +Setting this to disable will suppress the default sshd MOTD. ## Using two-factor auth with automated deployment tools. @@ -144,8 +150,8 @@ and then for each person add their ssh key using the following command: you should end up with an authorized_keys file that looks like: - command="/usr/local/bin/authy-ssh login 13386" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGRJbWu+WLVXYVADY3iQPE1kA7CIOSqHmskPM8qIAzKzq+1eRdmPwDZNmAvIQnN/0N7317Rt1bmTRLBwhl6vfSgL6677vUwsevPo27tIxdja67ELTh55xVLcJ3O8x2qkZsySgkLP/n+w3MUwLe1ht31AZOAsV7J7imhWipDijiysNgvHyeSWsHqExaL1blPOYJVHcqPbKY4SxFRq/MWeyPf/Sm24MFSKEaY6u0kNx8MLJ1X9X/YxmY9rdvzsZdQ7Z/PYhYt2Ja/0mzfYx2leeP2JQBsVfZZzAoFEPpw6mSP9kJREGe2tXvS9cRenhz/+V0+mvSJKG0f0Zzh428pTzN - command="/usr/local/bin/authy-ssh login 20" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAyvj2d0rSDukDT04mK7njUxtXffUrOnDCm2Bqub0zN7LQS733nBHp89aMuBI5ENjw1SQ2qXhLxvK1Xhr0pQr+dOWNn3emQjQuiA+YL39yp2RLLpflerJ3KAVY09CHYLFxdKj/DJgXsH+LMAPe2uVmWCP2xAV5ZcLnz3CdS2SX/EVlbNrftesZx9uAbmwKPLY1pmW7q/75AhJRow8VTP7zM/VS7jEHkj03g51BZGB8tMI3G8RDVEDtu2jVwZiq+8BaNCyjYVlsLfu6uGhnXeeUS3swu/atlt+pxy+QTf/HGvrJR58tER+foqheWtV3LqXN4oLckzqTVkDDmnNJlmrpYQ== + command="/usr/local/bin/authy-ssh login 13386 -1" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGRJbWu+WLVXYVADY3iQPE1kA7CIOSqHmskPM8qIAzKzq+1eRdmPwDZNmAvIQnN/0N7317Rt1bmTRLBwhl6vfSgL6677vUwsevPo27tIxdja67ELTh55xVLcJ3O8x2qkZsySgkLP/n+w3MUwLe1ht31AZOAsV7J7imhWipDijiysNgvHyeSWsHqExaL1blPOYJVHcqPbKY4SxFRq/MWeyPf/Sm24MFSKEaY6u0kNx8MLJ1X9X/YxmY9rdvzsZdQ7Z/PYhYt2Ja/0mzfYx2leeP2JQBsVfZZzAoFEPpw6mSP9kJREGe2tXvS9cRenhz/+V0+mvSJKG0f0Zzh428pTzN + command="/usr/local/bin/authy-ssh login 20 300" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAyvj2d0rSDukDT04mK7njUxtXffUrOnDCm2Bqub0zN7LQS733nBHp89aMuBI5ENjw1SQ2qXhLxvK1Xhr0pQr+dOWNn3emQjQuiA+YL39yp2RLLpflerJ3KAVY09CHYLFxdKj/DJgXsH+LMAPe2uVmWCP2xAV5ZcLnz3CdS2SX/EVlbNrftesZx9uAbmwKPLY1pmW7q/75AhJRow8VTP7zM/VS7jEHkj03g51BZGB8tMI3G8RDVEDtu2jVwZiq+8BaNCyjYVlsLfu6uGhnXeeUS3swu/atlt+pxy+QTf/HGvrJR58tER+foqheWtV3LqXN4oLckzqTVkDDmnNJlmrpYQ== The previous command will ask you the user ssh public key, cellphone and email. @@ -159,7 +165,7 @@ To uninstall type: ## Running Unit Tests -Fork and clone the git repository https://github.com/authy/authy-ssh.git +Fork and clone the git repository https://github.com/DigitalDJ/authy-ssh.git $ cd tests $ rake test diff --git a/authy-ssh b/authy-ssh index a6a18b7..02f3b16 100755 --- a/authy-ssh +++ b/authy-ssh @@ -1,10 +1,12 @@ #!/usr/bin/env bash -VERSION="1.7" +VERSION="1.7.1" AUTHY_URL="https://api.authy.com" APP_ROOT=`dirname $0` CONFIG_FILE="$APP_ROOT/authy-ssh.conf" -UPSTREAM_URL="https://raw.githubusercontent.com/authy/authy-ssh/master/authy-ssh" +LAST_LOGIN_FOLDER="$HOME/.authy-ssh/" +LAST_LOGIN_FILE="$LAST_LOGIN_FOLDER/last-login" +UPSTREAM_URL="https://raw.githubusercontent.com/DigitalDJ/authy-ssh/master/authy-ssh" READ_TIMEOUT=60 MIN_API_KEY_SIZE=12 @@ -56,7 +58,7 @@ function escape_input() { } function escape_number() { - sed 's/[^0-9]*//g' <<< $* + sed 's/^-?[0-9]*//g' <<< $* } function os_version() { @@ -73,6 +75,21 @@ function read_number() { echo "$(escape_number $number)" } +function get_date() { + echo "$(date +%s)" +} + +function update_last_login() { + mkdir -p "$LAST_LOGIN_FOLDER" + chmod 755 "$LAST_LOGIN_FOLDER" + echo "$USER|$(get_date)|$(get_ssh_client_ip)" > $LAST_LOGIN_FILE + chmod 600 $LAST_LOGIN_FILE +} + +function get_ssh_client_ip() { + echo "$(echo $SSH_CLIENT | awk '{ print $1}')" +} + function require_root() { debug "Checking if user is root" find_sshd_config @@ -109,8 +126,17 @@ function find_sshd_config() { elif [[ -f /etc/ssh/sshd_config ]] then SSHD_CONFIG="/etc/ssh/sshd_config" - else - red "Cannot find sshd_config in your server. Authy SSH will be enabled when you add the ForceCommand to it" + fi +} + +function find_pamd_sshd() { + debug "Trying to find pam sshd file" + if [[ -f /etc/pam.d/sshd ]] + then + PAM_SSHD_CONFIG="/etc/pam.d/sshd" + elif [[ -f /etc/pam.conf ]] + then + PAM_SSHD_CONFIG="/etc/pam.conf" fi } @@ -131,6 +157,8 @@ function add_force_command() { red " MAKE SURE YOU DO NOT MOVE/REMOVE ${authy_ssh_command} BEFORE UNINSTALLING AUTHY SSH" sleep 5 + else + red "Cannot find sshd_config in your server. Authy SSH will be enabled when you add the ForceCommand to it" fi } @@ -206,9 +234,31 @@ function install_authy() { return $FAIL ;; esac + + echo "Show default MOTD / Banner on successful login: " + echo "" + echo " 1. Show the default sshd MOTD / Banner on login" + echo " 2. Suppress the default sshd MOTD / Banner on login" + echo "" + echo -n "type 1 or 2 to select the option: " + read load_default_banner + + case $load_default_banner in + 1) + load_default_banner="enable" + ;; + 2) + load_default_banner="disable" + ;; + *) + red "you have entered an invalid option" + return $FAIL + ;; + esac yellow "Generating initial config on ${config_file}..." echo "banner=Good job! You've securely logged in with Authy." > "${config_file}" + echo "load_default_banner=${load_default_banner}" >> "${config_file}" echo "api_key=${authy_api_key}" >> "${config_file}" echo "default_verify_action=${default_verify_action}" >> "${config_file}" else @@ -224,16 +274,20 @@ function install_authy() { if [[ $SUDO_USER ]] then - green " sudo ${dest} enable $SUDO_USER " - green " Example: sudo $0 enable $SUDO_USER myuser@example.com 1 401-390-9987" + green " sudo ${dest} enable $SUDO_USER [grace-period]" + green " Example: sudo $0 enable $SUDO_USER myuser@example.com 1 401-390-9987 -1" else - green " sudo ${dest} enable $USER " - green " Example: sudo $0 enable $USER myuser@example.com 1 401-390-9987" + green " sudo ${dest} enable $USER [grace-period]" + green " Example: sudo $0 enable $USER myuser@example.com 1 401-390-9987 -1" fi echo "" echo "To enable two-factor authentication on user account type: " echo "" - green " sudo ${dest} enable " + green " sudo ${dest} enable [grace-period]" + echo "" + echo "Where grace-period, optionally, specify the number of seconds two-factor authentication will not be" + echo " required for creating a new session, after a successful login, from the same user and IP address." + echo " Using the value of -1 will always require two-factor authentication and is the default if not specified." echo "" echo "To uninstall Authy SSH type:" echo "" @@ -302,6 +356,46 @@ function check_api_key() { return $OK } +function check_grace_period() { + # check if we have a last login file + if [[ -f "$LAST_LOGIN_FILE" ]] + then + # read in the last_login file, don't use colons: ipv6! + IFS="|"; declare -a last_login=($(head -n 1 "$LAST_LOGIN_FILE")) + # if the user in the file is current user and the IP matches + if [[ ${last_login[0]} == $USER && ${last_login[2]} == $(get_ssh_client_ip) ]] + then + grace_period="$(escape_number "$1")" + + # if grace_period not sent in by key + if [ -z $grace_period ] + then + # read in the user config + for user in `read_config user` + do + IFS=":"; declare -a authy_user=($user) + # for this user, check if it has a grace period + if [[ ${authy_user[0]} == $USER && ${#authy_user[@]} -gt 2 && $(escape_number ${authy_user[2]}) -gt -1 ]] + then + grace_period="$(escape_number ${authy_user[2]})" + fi + done + fi + + # once we have grace period + if [ ! -z $grace_period ] + then + # if last login time plus grace period is less than current time + if [[ $(($(escape_number ${last_login[1]}) + $grace_period)) -ge $(escape_number $(get_date)) ]] + then + return $OK + fi + fi + fi + fi + return $FAIL +} + # Usage: $(read_config banner) function read_config() { key="$1" @@ -357,8 +451,20 @@ function protect_user() { then exit $FAIL fi + + echo -n "Enter your desired grace-period (in seconds) [-1]: " + read grace_period + + if [[ $grace_period == "" ]] + then + grace_period=-1 + elif ! [[ $grace_period =~ ^(-1|[0-9]+)$ ]] + then + yellow "grace-period is invalid" + exit $FAIL + fi - echo "command=\"$COMMAND login $authy_user_id\" $ssh_key" >> "${auth_keys}" + echo "command=\"$COMMAND login $authy_user_id $grace_period\" $ssh_key" >> "${auth_keys}" if [[ $(whoami) == "root" ]] then chown "$local_user" "$auth_keys" @@ -366,7 +472,7 @@ function protect_user() { green "The user is now protected with authy" } -# usage: register_user "local_user" "" "" "" +# usage: register_user "local_user" "" "" "" "" function register_user() { register_user_on_authy $* @@ -375,9 +481,19 @@ function register_user() { exit $FAIL fi - if [[ $authy_user_id ]] + grace_period="$(escape_number $5)" + if [ -z $grace_period ] then - echo "user=$local_user:$authy_user_id" >> $CONFIG_FILE + grace_period=-1 + elif ! [[ $grace_period =~ ^(-1|[0-9]+)$ ]] + then + yellow "grace-period is invalid" + exit $FAIL + fi + + if [[ $authy_user_id && $grace_period ]] + then + echo "user=$local_user:$authy_user_id:$grace_period" >> $CONFIG_FILE green "User was registered" else red "Cannot register user: $response" @@ -483,6 +599,31 @@ function run_shell() { elif [ $SHELL ] # when user runs: ssh server then debug "running shell: $SHELL" + load_default_banner=$(read_config load_default_banner) + if [[ $? == $OK && "$load_default_banner" == "enable" ]]; + then + find_pamd_sshd + # if we found pam + if [[ -r $PAM_SSHD_CONFIG ]] + then + debug "Found $PAM_SSHD_CONFIG" + # pam says load motd from specific file for sshd + # find an uncommented line for session or sshd containing pam_motd and motd variable + BANNER="`sed -n 's/^\(session\|sshd\).*pam_motd.*motd=\(\S*\)\s*.*$/\2/p' /etc/pam.d/sshd`" + if [[ ! -z $BANNER && -r "/var/$BANNER" ]] + then + debug "Showing /var/$BANNER" + cat "/var/$BANNER" + fi + fi + + # if motd exists, print that out too + if [[ -r "/etc/motd" ]] + then + debug "Showing /etc/motd" + cat "/etc/motd" + fi + fi exec -l $SHELL fi @@ -569,6 +710,8 @@ function login() { fi fi + update_last_login + if [[ $mode != "test" ]] then run_shell @@ -591,7 +734,7 @@ function request_sms() { response=`curl --connect-timeout 10 -A "${useragent}" "${url}" 2>/dev/null` debug "[request sms] url: $url | response: $response | curl exit stats: $?" - if [[ $response == *success*sent* ]] + if [[ $response == *success*"was sent"* ]] then green "SMS message was sent" elif [[ $response == *not*enabled* ]] @@ -606,6 +749,7 @@ function request_sms() { function ask_token_and_log_user_in() { mode="$(escape_input $1)" authy_id="$(escape_number $2)" + grace_period="$(escape_number $3)" if [[ $authy_id == "" ]] then @@ -625,12 +769,21 @@ function ask_token_and_log_user_in() { run_shell fi + check_grace_period $grace_period + if [[ $? == $OK ]] + then + debug "Grace period allowed" + update_last_login + run_shell + fi + times=3 if [[ $AUTHY_TOKEN ]] # env var then times=1 fi + unset IFS for i in `$SEQ 1 $times` do authy_token="$(escape_number $AUTHY_TOKEN)" @@ -709,14 +862,14 @@ case $1 in check_config_file "writable" AUTHY_API_KEY="$(read_config api_key)" check_api_key - register_user "$2" "$3" "$4" "$5" + register_user "$2" "$3" "$4" "$5" "$6" ;; login) check_config_file check_dependencies AUTHY_API_KEY="$(read_config api_key)" check_api_key - ask_token_and_log_user_in "login" "$2" + ask_token_and_log_user_in "login" "$2" "$3" ;; protect) check_config_file @@ -757,9 +910,19 @@ Available commands: enable receives a list of arguments needed to register a user. usage: - sudo $0 enable [local-user] [email] [numeric country code] [cellphone] + sudo $0 enable [local-user] [email] [numeric country code] [cellphone] + + Where grace-period, optionally, specifies the number of seconds two-factor authentication will not be + required for creating a new session, after a successful login, from the same user and IP address. + Using the value of -1 will always require two-factor authentication and is the default if not specified. + + Always require two-factor authentication: + + Example: sudo $0 enable myuser myuser@example.com 1 401-390-9987 -1 + + Allow 300 seconds after a successful login before requiring two-factor authentication again: - Example: sudo $0 enable myuser myuser@example.com 1 401-390-9987 + Example: sudo $0 enable myuser myuser@example.com 1 401-390-9987 300 protect installs authy-ssh for the given user diff --git a/authy-ssh.sha1sum b/authy-ssh.sha1sum index 6bcaa87..d357919 100644 --- a/authy-ssh.sha1sum +++ b/authy-ssh.sha1sum @@ -1 +1 @@ -c4f3c851c2ed1546a9849318f14f5c18e7077edf authy-ssh +1a07ab1f753ad5c091023646c4ab48f37ab65ae5 authy-ssh diff --git a/tests/test_enable.rb b/tests/test_enable.rb index 7e93aac..1dc45a7 100644 --- a/tests/test_enable.rb +++ b/tests/test_enable.rb @@ -80,3 +80,31 @@ red " [FAILED]" end end + +authy_ssh("enable #{ENV["USER"]} 'test;;;@authy.com' '1|}+)(&%' '111;|};111#-/:1111' aaa", {}, true) do |stdin, stdout| + + if read_until(stdout, /Do you want to enable this user/i) + stdin.puts "y" + end + + if read_until(stdout, /grace-time is invalid/i) + print "Setting an invalid grace-time: aaa" + green " [OK]" + else + red " [FAILED]" + end +end + +authy_ssh("enable #{ENV["USER"]} test@authy.com 0 111-111-1111 -2", {}, true) do |stdin, stdout| + + if read_until(stdout, /Do you want to enable this user/i) + stdin.puts "y" + end + + if read_until(stdout, /grace-time is invalid/i) + print "Setting an invalid grace-time: -2" + green " [OK]" + else + red " [FAILED]" + end +end diff --git a/tests/test_install.rb b/tests/test_install.rb index ca14838..d416dc9 100644 --- a/tests/test_install.rb +++ b/tests/test_install.rb @@ -56,11 +56,16 @@ print "Select the option 1: Disable two factor authentication until api.authy.com is back" stdin.puts "1" end + + if read_until(stdout, /type 1 or 2 to select the option/i) + print "Select the option 1: show default MOTD" + stdin.puts "1" + end if read_until(stdout, /Restart the SSH server to apply changes/i) config_path = File.expand_path("../authy-ssh.conf", __FILE__) config = File.read(config_path) - if config =~ /default_verify_action=disable/ + if config =~ /default_verify_action=disable/ and config =~ /load_default_banner=enable/ green " [OK]" else yellow " Option not configured" @@ -80,11 +85,16 @@ print "Select the option 2: Don't allow logins until api.authy.com is back" stdin.puts "2" end + + if read_until(stdout, /type 1 or 2 to select the option/i) + print "Select the option 2: suppress default MOTD" + stdin.puts "2" + end if read_until(stdout, /Restart the SSH server to apply changes/i) config_path = File.expand_path("../authy-ssh.conf", __FILE__) config = File.read(config_path) - if config =~ /default_verify_action=enforce/ + if config =~ /default_verify_action=enforce/ and config =~ /load_default_banner=disable/ green " [OK]" else yellow " Option not configured" @@ -112,3 +122,25 @@ end system("sudo rm authy-ssh") end + +authy_ssh("install .", {}, true) do |stdin, stdout| + if read_until(stdout, /Enter the Authy API key/i) + stdin.puts "0cd08abec2e9b9641e40e9470a7fc336" + end + + if read_until(stdout, /type 1 or 2 to select the option/i) + stdin.puts "2" + end + + if read_until(stdout, /type 1 or 2 to select the option/i) + print "Select an invalid option: 9" + stdin.puts "9" + end + + if read_until(stdout, /you have entered an invalid option/i) + green " [OK]" + else + red " [FAILED]" + end + system("sudo rm authy-ssh") +end diff --git a/tests/test_protect.rb b/tests/test_protect.rb index 2bcd6bb..d83755f 100644 --- a/tests/test_protect.rb +++ b/tests/test_protect.rb @@ -88,3 +88,36 @@ red " [FAILED]" end end + +authy_ssh("protect #{ENV["USER"]}") do |stdin, stdout| + if read_until(stdout, /Enter your public ssh key/i) + stdin.puts "#{SSH_KEY}" + end + + if read_until(stdout, /Your country code/i) + stdin.puts "1" + end + + if read_until(stdout, /Your cellphone/i) + stdin.puts "1234567" + end + + if read_until(stdout, /Your email/i) + stdin.puts "test@authy.com" + end + + if read_until(stdout, /Do you want to enable this user/i) + stdin.puts "y" + end + + if read_until(stdout, /Enter your desired grace-period/i) + print "Setting an invalid grace-time" + stdin.puts "aaa" + end + + if read_until(stdout, /grace-period is invalid/i) + green " [OK]" + else + red " [FAILED]" + end +end