├── 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 |
--------------------------------------------------------------------------------