├── requirements.txt ├── README.md ├── openstack_cli_otp.env ├── cscs-keygen.py └── cscs-keygen.sh /requirements.txt: -------------------------------------------------------------------------------- 1 | progress==1.6 2 | requests==2.25.1 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mfa-cscs-access 2 | 3 | 4 | The repository contains a simple Python script [cscs-keygen.py] and a shell script [cscs-keygen.sh] which can be used as a command line tool for fetching public and private keys signed by CSCS'S CA after authenticating using MFA. You can then use those keys to ssh to CSCS'login nodes. 5 | 6 | For using the python script, these are the steps: 7 | 8 | ```sh 9 | git clone https://github.com/eth-cscs/sshservice-cli.git 10 | cd sshservice-cli 11 | pip install virtualenv # (if you don't already have virtualenv installed) 12 | virtualenv venv # to create your new environment (called 'venv' here) 13 | source venv/bin/activate # to enter the virtual environment 14 | pip install -r requirements.txt # to install the requirements in the current environment 15 | python cscs-keygen.py 16 | ``` 17 | 18 | For using the shell script, these are the steps: 19 | ```bash 20 | git clone git@github.com:eth-cscs/sshservice-cli.git 21 | cd sshservice-cli 22 | bash cscs-keygen.sh 23 | ``` 24 | -------------------------------------------------------------------------------- /openstack_cli_otp.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # This script sets the environment properly so that a user can use the OpenStack CLI with OTP enabled 5 | 6 | client_id="castor" 7 | client_secret="c6cc606a-5ae4-4e3e-8a19-753ad265f521" 8 | 9 | 10 | #Read Inputs 11 | read -p "Username : " username 12 | read -s -p "Password: " password 13 | echo 14 | read -s -p "Enter OTP: " otp 15 | echo 16 | 17 | 18 | 19 | ACCESS_TOKEN=$(curl -s -d "client_id=$client_id" -d "client_secret=$client_secret" -d "username=$username" -d "password=$password" -d "grant_type=password" -d "totp=$otp" https://auth.cscs.ch/auth/realms/cscs/protocol/openid-connect/token | cut -d \" -f 4) 20 | 21 | # Prepare filter parameter 22 | if [ "$1" == "" ]; then 23 | PRJ_FILTER="." 24 | else 25 | PRJ_FILTER=" $1$" 26 | fi 27 | 28 | # Prepare environment 29 | for key in $( set | awk '{FS="="} /^OS_/ {print $1}' ); do unset $key ; done 30 | export OS_USERNAME=$username 31 | export OS_PASSWORD=$password 32 | export OS_IDENTITY_API_VERSION=3 33 | export OS_AUTH_URL=https://castor.cscs.ch:13000/v3 34 | export OS_IDENTITY_PROVIDER=cscskc 35 | export OS_PROTOCOL=openid 36 | export OS_INTERFACE=public 37 | export OS_CLIENT_ID=$client_id 38 | export OS_CLIENT_SECRET=$client_secret #set to anything if keycloak client is public 39 | export OS_DISCOVERY_ENDPOINT=https://auth.cscs.ch/auth/realms/cscs/.well-known/openid-configuration 40 | export OS_ACCESS_TOKEN=$ACCESS_TOKEN 41 | 42 | #Getting the unscoped token 43 | echo "[openstack --os-auth-type v3oidcaccesstoken token issue]" 44 | UNSCOPED_TOKEN="$(openstack --os-auth-type v3oidcaccesstoken token issue --format value --column id)" 45 | 46 | 47 | export OS_AUTH_TYPE=token 48 | export OS_TOKEN=$UNSCOPED_TOKEN 49 | unset OS_PASSWORD 50 | 51 | # Getting the user ID with python directly (no other way, plus serves as an example!!) 52 | echo -n " * Logged in user ID $OS_USERNAME: " 53 | python <. 17 | # 18 | # AUTHORS Massimo Benini 19 | 20 | import getpass 21 | import requests 22 | import os 23 | import sys 24 | import time 25 | import re 26 | import json 27 | from progress.bar import IncrementalBar 28 | 29 | #Variables: 30 | api_get_keys = 'https://sshservice.cscs.ch/api/v1/auth/ssh-keys/signed-key' 31 | 32 | #Methods: 33 | def get_user_credentials(): 34 | user = input("Username: ") 35 | pwd = getpass.getpass() 36 | otp = getpass.getpass("Enter OTP (6-digit code):") 37 | if not (re.match('^\d{6}$', otp)): 38 | sys.exit("Error: OTP must be a 6-digit code.") 39 | return user, pwd, otp 40 | 41 | 42 | def get_keys(username, password, otp): 43 | headers = {'Content-Type': 'application/json', 'Accept':'application/json'} 44 | data = { 45 | "username": username, 46 | "password": password, 47 | "otp": otp 48 | } 49 | try: 50 | resp = requests.post(api_get_keys, data=json.dumps(data), headers=headers, verify=True) 51 | resp.raise_for_status() 52 | except requests.exceptions.RequestException as e: 53 | try: 54 | d_payload = e.response.json() 55 | except: 56 | raise SystemExit(e) 57 | if "payload" in d_payload and "message" in d_payload["payload"]: 58 | print("Error: "+d_payload["payload"]["message"]) 59 | raise SystemExit(e) 60 | else: 61 | public_key = resp.json()['public'] 62 | if not public_key: 63 | sys.exit("Error: Unable to fetch public key.") 64 | private_key = resp.json()['private'] 65 | if not private_key: 66 | sys.exit("Error: Unable to fetch private key.") 67 | return public_key, private_key 68 | 69 | def save_keys(public,private): 70 | if not public or not private: 71 | sys.exit("Error: invalid keys.") 72 | try: 73 | with open(os.path.expanduser("~")+'/.ssh/cscs-key-cert.pub', 'w') as file: 74 | file.write(public) 75 | except IOError as er: 76 | sys.exit('Error: writing public key failed.', er) 77 | try: 78 | with open(os.path.expanduser("~")+'/.ssh/cscs-key', 'w') as file: 79 | file.write(private) 80 | except IOError as er: 81 | sys.exit('Error: writing private key failed.', er) 82 | try: 83 | os.chmod(os.path.expanduser("~")+'/.ssh/cscs-key-cert.pub', 0o644) 84 | except Exception as ex: 85 | sys.exit('Error: cannot change permissions of the public key.', ex) 86 | try: 87 | os.chmod(os.path.expanduser("~")+'/.ssh/cscs-key', 0o600) 88 | except Exception as ex: 89 | sys.exit('Error: cannot change permissions of the private key.', ex) 90 | 91 | def set_passphrase(): 92 | user_input = input('Do you want to add a passphrase to your key? [y/n] (Default y) \n') 93 | 94 | yes_choices = ['yes', 'y'] 95 | no_choices = ['no', 'n'] 96 | 97 | if user_input.lower() in no_choices: 98 | passphrase = False 99 | else: 100 | passphrase = True 101 | cmd = 'ssh-keygen -f ~/.ssh/cscs-key -p' 102 | while (os.system(cmd) != 0): 103 | print("Please set the same passphrase twice...") 104 | return passphrase 105 | 106 | 107 | def main(): 108 | user, pwd, otp = get_user_credentials() 109 | bar = IncrementalBar('Retrieving signed SSH keys:', max = 3) 110 | public, private = get_keys(user, pwd, otp) 111 | bar.next() 112 | time.sleep(1) 113 | bar.next() 114 | time.sleep(1) 115 | save_keys(public, private) 116 | bar.next() 117 | time.sleep(1) 118 | bar.finish() 119 | if (set_passphrase()): 120 | substrg = ", using the passphrase you have set:" 121 | else: 122 | substrg = ":" 123 | message = """ 124 | 125 | Usage: 126 | 127 | 1. Add the key to the SSH agent"""+substrg+""" 128 | ssh-add -t 1d ~/.ssh/cscs-key 129 | 130 | 2. Connect to the login node using CSCS keys: 131 | ssh -A your_usernamen@ 132 | 133 | Note - if the key is not added to the SSH agent as mentioned in the step-1 above then use the command: 134 | ssh -i ~/.ssh/cscs-key 135 | 136 | """ 137 | print(message) 138 | 139 | if __name__ == "__main__": 140 | main() 141 | -------------------------------------------------------------------------------- /cscs-keygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script sets the environment properly so that a user can access CSCS 4 | # login nodes via ssh. 5 | 6 | # Copyright (C) 2023, ETH Zuerich, Switzerland 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, version 3 of the License. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | # AUTHORS Massimo Benini 21 | 22 | 23 | function ProgressBar { 24 | # Process data 25 | let _progress=(${1}*100/${2}*100)/100 26 | let _done=(${_progress}*4)/10 27 | let _left=40-$_done 28 | # Build progressbar string lengths 29 | _fill=$(printf "%${_done}s") 30 | _empty=$(printf "%${_left}s") 31 | 32 | # 1.2 Build progressbar strings and print the ProgressBar line 33 | # 1.2.1 Output example: 34 | # 1.2.1.1 Progress : [########################################] 100% 35 | printf "\rSetting the environment : [${_fill// /#}${_empty// /-}] ${_progress}%%" 36 | } 37 | 38 | #Variables 39 | _start=1 40 | #This accounts as the "totalState" variable for the ProgressBar function 41 | _end=100 42 | 43 | #Params 44 | MFA_KEYS_URL="https://sshservice.cscs.ch/api/v1/auth/ssh-keys/signed-key" 45 | 46 | #Detect OS 47 | OS="$(uname)" 48 | case "${OS}" in 49 | 'Linux') 50 | OS='Linux' 51 | ;; 52 | 'FreeBSD') 53 | OS='FreeBSD' 54 | ;; 55 | 'WindowsNT') 56 | OS='Windows' 57 | ;; 58 | 'Darwin') 59 | OS='Mac' 60 | ;; 61 | *) ;; 62 | esac 63 | 64 | #OS validation 65 | if [ "${OS}" != "Mac" ] && [ "${OS}" != "Linux" ]; then 66 | echo "This script works only on Mac-OS or Linux. Abording." 67 | exit 1 68 | fi 69 | 70 | #Read Inputs 71 | read -p "Username : " USERNAME 72 | read -s -p "Password: " PASSWORD 73 | echo 74 | read -s -p "Enter OTP (6-digit code): " OTP 75 | echo 76 | 77 | #Validate inputs 78 | if ! [[ "${USERNAME}" =~ ^[[:lower:]_][[:lower:][:digit:]_-]{2,15}$ ]]; then 79 | echo "Username is not valid." 80 | exit 1 81 | fi 82 | 83 | if [ -z "${PASSWORD}" ]; then 84 | echo "Password is empty." 85 | exit 1 86 | fi 87 | 88 | if ! [[ "${OTP}" =~ ^[[:digit:]]{6} ]]; then 89 | echo "OTP is not valid, OTP must contains only six digits." 90 | exit 1 91 | fi 92 | 93 | ProgressBar 25 "${_end}" 94 | echo " Authenticating to the SSH key service..." 95 | 96 | HEADERS=(-H "Content-Type: application/json" -H "accept: application/json") 97 | KEYS=$(curl -s -S --ssl-reqd \ 98 | "${HEADERS[@]}" \ 99 | -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\", \"otp\": \"$OTP\"}" \ 100 | "$MFA_KEYS_URL") 101 | 102 | if [ $? != 0 ]; then 103 | exit 1 104 | fi 105 | 106 | ProgressBar 50 "${_end}" 107 | echo " Retrieving the SSH keys..." 108 | 109 | DICT_KEY=$(echo ${KEYS} | cut -d \" -f 2) 110 | if [ "${DICT_KEY}" == "payload" ]; then 111 | MESSAGE=$(echo ${KEYS} | cut -d \" -f 6) 112 | ! [ -z "${MESSAGE}" ] && echo "${MESSAGE}" 113 | echo "Error fetching the SSH keys. Aborting." 114 | exit 1 115 | fi 116 | 117 | PUBLIC=$(echo ${KEYS} | cut -d \" -f 4) 118 | PRIVATE=$(echo ${KEYS} | cut -d \" -f 8) 119 | 120 | #Check if keys are empty: 121 | if [ -z "${PUBLIC}" ] || [ -z "${PRIVATE}" ]; then 122 | echo "Error fetching the SSH keys. Aborting." 123 | exit 1 124 | fi 125 | 126 | ProgressBar 75 "${_end}" 127 | echo " Setting up the SSH keys into your home folder..." 128 | 129 | #Check ~/.ssh folder and store the keys 130 | echo ${PUBLIC} | awk '{gsub(/\\n/,"\n")}1' > ~/.ssh/cscs-key-cert.pub || exit 1 131 | echo ${PRIVATE} | awk '{gsub(/\\n/,"\n")}1' > ~/.ssh/cscs-key || exit 1 132 | 133 | #Setting permissions: 134 | chmod 644 ~/.ssh/cscs-key-cert.pub || exit 1 135 | chmod 600 ~/.ssh/cscs-key || exit 1 136 | 137 | #Format the keys: 138 | if [ "${OS}" = "Mac" ] 139 | then 140 | sed -i '' -e '$ d' ~/.ssh/cscs-key-cert.pub || exit 1 141 | sed -i '' -e '$ d' ~/.ssh/cscs-key || exit 1 142 | else [ "${OS}" = "Linux" ] 143 | sed '$d' ~/.ssh/cscs-key-cert.pub || exit 1 144 | sed '$d' ~/.ssh/cscs-key || exit 1 145 | fi 146 | 147 | ProgressBar 100 "${_end}" 148 | echo " Completed." 149 | 150 | exit_code_passphrase=1 151 | read -n 1 -p "Do you want to add a passphrase to your key? [y/n] (Default y) " reply; 152 | if [ "$reply" != "" ]; 153 | then echo; 154 | fi 155 | if [ "$reply" = "${reply#[Nn]}" ]; then 156 | while [ $exit_code_passphrase != 0 ]; do 157 | ssh-keygen -f ~/.ssh/cscs-key -p 158 | exit_code_passphrase=$? 159 | done 160 | fi 161 | 162 | if (( $exit_code_passphrase == 0 )); 163 | then 164 | SUBSTRING=", using the passphrase you have set:"; 165 | else 166 | SUBSTRING=":"; 167 | fi 168 | 169 | cat << EOF 170 | 171 | Usage: 172 | 173 | 1. Add the key to the SSH agent${SUBSTRING} 174 | ssh-add -t 1d ~/.ssh/cscs-key 175 | 176 | 2. Connect to the login node using CSCS keys: 177 | ssh -A your_username@ 178 | 179 | Note - if the key not is added to the SSH agent as mentioned in the step-1 above then use the command: 180 | ssh -i ~/.ssh/cscs-key 181 | 182 | EOF 183 | 184 | 185 | 186 | --------------------------------------------------------------------------------