Universal sshkey and password manager script (#4890)

* Add sshkey and password manager script that works with userdata server and configdrive

* fix duplicated sshkey insertion

* Add Alpine support

* Force curl to fail on server errors

* Keep user defined ssh-keys only
This commit is contained in:
aleskxyz 2021-09-21 11:30:58 +04:30 committed by GitHub
parent 34bd92259a
commit daa183d9fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -0,0 +1,508 @@
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
username=root
userDataServerPort=8080
configDriveLabel=config-2
function findPrimaryNetwork(){
outputLog "Detecting primary network"
if command -v ip &> /dev/null
then
primaryNet=$(ip -o -4 route show to default | awk '{print $5}')
elif command -v netstat &> /dev/null
then
primaryNet=$(netstat -r4 | grep default | awk '{print $(NF)}')
elif command -v route &> /dev/null
then
primaryNet=$(route -4 2> /dev/null | grep default | awk '{print $(NF)}')
if [ -z "$primaryNet" ]
then
primaryNet=$(route get default 2> /dev/null | grep interface | tr -d ' ' | awk '{split($0,a,":"); print a[2]}')
fi
fi
if [ -z "$primaryNet" ]
then
outputLog "Could not find primary network"
return 1
fi
echo "$primaryNet"
return 0
}
function findUserDataServer(){
primaryNet=$1
outputLog "Trying to find userdata server"
if [ -z "$primaryNet" ]
then
outputLog "Unable to determine the userdata server, falling back to data-server"
echo "data-server"
return 0
fi
if command -v netplan &> /dev/null
then
outputLog "Operating System is using netplan"
userDataServer=$(netplan ip leases "$primaryNet" | grep SERVER_ADDRESS | awk '{split($0,a,"="); print a[2]}')
if [ -n "$userDataServer" ]
then
outputLog "Found userdata server IP $userDataServer in netplan config"
echo "$userDataServer"
return 0
fi
fi
if command -v nmcli &> /dev/null
then
outputLog "Operating System is using NetworkManager"
userDataServer=$(nmcli -t connection show "$(nmcli -t -f UUID,DEVICE connection | grep "$primaryNet" | awk '{split($0,a,":"); print a[1]}')" | grep next_server | tr -d ' ' |awk '{split($0,a,"="); print a[2]}')
if [ -n "$userDataServer" ]
then
outputLog "Found userdata server IP $userDataServer in NetworkManager config"
echo "$userDataServer"
return 0
fi
fi
if command -v wicked &> /dev/null
then
outputLog "Operating System is using wicked"
userDataServer=$(grep SERVERID /run/wicked/leaseinfo."$primaryNet"* | tr -d "'" | awk '{split($0,a,"="); print a[2]}')
if [ -n "$userDataServer" ]
then
outputLog "Found userdata server IP $userDataServer in wicked config"
echo "$userDataServer"
return 0
fi
fi
if command -v udhcpc &> /dev/null
then
outputLog "Operating System is using udhcpc"
userDataServer=$(< /run/dhcp-server-ip."$primaryNet")
if [ -n "$userDataServer" ]
then
outputLog "Found userdata server IP $userDataServer in udhcpc"
echo "$userDataServer"
return 0
fi
fi
outputLog "Searching for DHCP server in lease files"
primaryLease=$(
dhcpFolders="/var/lib/dhclient/* /var/lib/dhcp3/* /var/lib/dhcp/* /var/lib/NetworkManager/* /var/db/dhclient*"
for files in $dhcpFolders
do
if [ -e "$files" ]
then
< "$files" tr -d '\n' | sed 's/ //g ; s/lease {//g ; s/}/\n/g' | grep 'option routers'
fi
done
)
serverList=$(
IFS=$'\n'
for line in $(echo -e "$primaryLease")
do
splitLine=$(echo "$line" | sed -e 's/;/\n/g')
if date -j &> /dev/null
then
timestamp=$(date -j -f "%Y/%m/%d %H:%M:%S" "$(echo "$splitLine" | grep 'expire' | sed -r 's/.*expire [0-9]+ (.*)/\1/')" +"%s")
else
timestamp=$(date -d "$(echo "$splitLine" | grep 'expire' | sed -e 's/.*expire [0-9]\+ \(.*\)/\1/')" +"%s")
fi
interface=$(echo "$splitLine" | grep 'interface' | sed -e 's/.*interface "\(.*\)"/\1/')
server=$(echo "$splitLine" | grep 'dhcp-server-identifier' | sed -e 's/.*dhcp-server-identifier \(.*\)/\1/')
echo "$timestamp","$interface","$server"
done
)
userDataServer=$(echo "$serverList" | grep "$primaryNet" | sort -n | tail -1 | awk '{split($0,a,","); print a[3]}')
if [ -n "$userDataServer" ]
then
outputLog "Userdata server found: $userDataServer"
echo "$userDataServer"
return 0
fi
outputLog "Unable to determine the userdata server, falling back to data-server"
echo "data-server"
return 0
}
function getPasswordFromUserDataServer(){
userDataServer=$1
userDataServerPort=$2
outputLog "Sending request to userdata server at $userDataServer to get the password"
if ! response=$(curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: send_my_password" http://"$userDataServer":"$userDataServerPort")
then
outputLog "Failed to send request to userdata server at $userDataServer"
return 4
fi
outputLog "Got response from userdata server at $userDataServer"
response=$(echo "$response" | tr -d '\r')
case $response in
"")
outputLog "Userdata server at $userDataServer did not have any password for the VM"
return 2
;;
"bad_request")
outputLog "VM sent an invalid request to userdata server at $userDataServer"
return 3
;;
"saved_password")
outputLog "VM has already saved a password from the userdata server at $userDataServer"
return 1
;;
*)
outputLog "VM got a valid password from server at $userDataServer"
echo "$response"
return 0
esac
}
function findHomeDirectory(){
username=$1
getent passwd "$username"|awk -F ":" '{print $6}'
}
function setPassword(){
username=$1
homeDir=$2
password=$3
if command -v md5sum &> /dev/null
then
newMd5=$(echo "$password" | md5sum | awk '{print $1}')
elif command -v md5 &> /dev/null
then
newMd5=$(echo "$password" | md5)
else
newMd5='N/A'
fi
if [ $newMd5 != 'N/A' ]
then
if [ -f "$homeDir"/.password.md5 ]
then
oldMd5=$(cat "$homeDir"/.password.md5)
fi
if [ "$newMd5" == "$oldMd5" ]
then
outputLog "There is no update of VM password"
return 0
fi
else
outputLog "Cannot determine change of password"
fi
outputLog "Changing password for user $username"
if command -v chpasswd &> /dev/null
then
echo "$username":"$password" | chpasswd
elif command -v usermod &> /dev/null && command -v mkpasswd &> /dev/null
then
usermod -p "$(mkpasswd -m SHA-512 "$password")" "$username"
elif command -v pw &> /dev/null
then
echo "$password" | pw mod user "$username" -h 0
else
outputLog "Failed to change password for user $username"
return 1
fi
outputLog "Successfully changed password for user $username"
if [ $newMd5 != 'N/A' ]
then
echo "$newMd5" > "$homeDir"/.password.md5
chmod 600 "$homeDir"/.password.md5
chown "$username": "$homeDir"/.password.md5
fi
return 0
}
function sendAckToUserDataServer(){
userDataServer=$1
userDataServerPort=$2
outputLog "Sending acknowledgment to userdata server at $userDataServer"
if ! curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: saved_password" "$userDataServer":"$userDataServerPort" &> /dev/null
then
outputLog "Failed to sent acknowledgment to userdata server at $userDataServer"
return 1
fi
outputLog "Successfully sent acknowledgment to userdata server at $userDataServer"
return 0
}
function getPublicKeyFromUserDataServer(){
userDataServer=$1
outputLog "Sending request to userdata server at $userDataServer to get public key"
if ! reponse=$(curl --fail --silent --connect-timeout 20 --retry 3 http://"$userDataServer"/latest/public-keys)
then
outputLog "Failed to get public key from userdata server"
return 2
fi
outputLog "Got response from userdata server at $userDataServer"
if [ -z "$reponse" ]
then
outputLog "Did not receive any public keys from userdata server"
return 1
fi
outputLog "Successfully get public key from userdata server"
echo "$reponse"
return 0
}
function setPublicKey(){
username=$1
homeDir=$2
publicKey=$3
outputLog "Applying public key for $username"
sshDir=$homeDir/.ssh
authorizedKeysFile=$sshDir/authorized_keys
if [ ! -d "$sshDir" ]
then
outputLog ".ssh directory for $username not found, creating .ssh directory"
mkdir "$sshDir"
fi
if [ ! -f "$authorizedKeysFile" ]
then
outputLog "authorized_keys file for $username not found, creating authorized_keys file"
touch "$authorizedKeysFile"
fi
if grep "$(echo "$publicKey" | awk '{print $2}')" "$authorizedKeysFile" > /dev/null
then
outputLog "No need to update authorized_keys file"
return 0
fi
outputLog "Writing public key in authorized_keys file"
sed -i "/ cloudstack@apache.org$/d" "$authorizedKeysFile"
echo "$publicKey cloudstack@apache.org" >> "$authorizedKeysFile"
chmod 600 "$authorizedKeysFile"
chmod 700 "$sshDir"
chown -R "$username": "$sshDir"
which restorecon &> /dev/null && restorecon -R -v "$sshDir"
return 0
}
function findConfigDrive(){
configDriveLabel=$1
outputLog "Searching for ConfigDrive"
if [ -e /dev/disk/by-label/"$configDriveLabel" ]
then
outputLog "ConfigDrive found at /dev/disk/by-label/$configDriveLabel"
echo "/dev/disk/by-label/$configDriveLabel"
return 0
fi
if [ -e /dev/iso9660/"$configDriveLabel" ]
then
outputLog "ConfigDrive found at /dev/iso9660/$configDriveLabel"
echo "/dev/iso9660/$configDriveLabel"
return 0
fi
blockDevice=$(blkid -t LABEL="$configDriveLabel" /dev/hd? /dev/sd? /dev/xvd? /dev/vd? /dev/sr? -o device 2> /dev/null)
if [ -n "$blockDevice" ]
then
outputLog "ConfigDrive found at $blockDevice"
echo "$blockDevice"
return 0
fi
outputLog "ConfigDrive not found"
return 1
}
function mountConfigDrive(){
disk=$1
outputLog "Mounting ConfigDrive"
mountDir=$(mktemp -d)
if [ ! -e "$mountDir" ]
then
mkdir "$mountDir"
chmod 700 "$mountDir"
fi
mounted=0
if [ $mounted == 0 ] && mount -r "$disk" "$mountDir" &> /dev/null
then
mounted=1
fi
if [ $mounted == 0 ] && mount -r -t cd9660 "$disk" "$mountDir" &> /dev/null
then
mounted=1
fi
if [ $mounted == 0 ] && mount -r -t iso9660 "$disk" "$mountDir" &> /dev/null
then
mounted=1
fi
if [ $mounted == 1 ]
then
outputLog "$disk successfully mounted on $mountDir"
echo "$mountDir"
return 0
fi
outputLog "Failed mounting $disk on $mountDir"
rm -rf "$mountDir"
return 1
}
function unmountConfigDrive(){
mountDir=$1
outputLog "Unmounting ConfigDrive"
if ! umount "$mountDir"
then
outputLog "Failed unmounting $mountDir"
return 1
fi
rm -rf "$mountDir"
outputLog "Successfully unmount $mountDir"
return 0
}
function getPasswordFromConfigDrive(){
mountDir=$1
passwordFile=$mountDir/cloudstack/password/vm_password.txt
if [ ! -f "$passwordFile" ]
then
outputLog "Password file not found in ConfigDrivee"
return 3
fi
outputLog "Password file found in ConfigDrive"
content=$(< "$passwordFile" tr -d '\r')
case $content in
"")
outputLog "ConfigDrive did not have any password for the VM"
return 2
;;
"saved_password")
outputLog "VM has already saved a password"
return 1
;;
*)
outputLog "VM got a valid password"
echo "$content"
return 0
esac
}
function getPublicKeyFromConfigDrive() {
mountDir=$1
publicKeyFile=$mountDir/cloudstack/metadata/public-keys.txt
if [ ! -f "$publicKeyFile" ]
then
outputLog "Public key file not found in ConfigDrive"
return 2
fi
content=$(< "$publicKeyFile" tr -d '\r')
if [ -z "$content" ]
then
outputLog "Did not receive any public keys"
return 1
fi
echo "$content"
outputLog "Public key successfully received."
return 0
}
function outputLog() {
stderr=1
logger=1
message=$1
if [ $stderr == 1 ]
then
echo "Cloud Password Manager: $message" 1>&2
fi
if [ $logger == 1 ]
then
logger -t "Cloud Password Manager" "$message"
fi
}
publicKeyReceived=0
passwordReceived=0
dataSource=''
if disk=$(findConfigDrive "$configDriveLabel")
then
if mountDir=$(mountConfigDrive "$disk")
then
dataSource='ConfigDrive'
if publicKey=$(getPublicKeyFromConfigDrive "$mountDir")
then
publicKeyReceived=1
fi
if password=$(getPasswordFromConfigDrive "$mountDir")
then
passwordReceived=1
fi
unmountConfigDrive "$mountDir"
fi
fi
if [ $publicKeyReceived == 0 ] || [ $passwordReceived == 0 ]
then
primaryNet=$(findPrimaryNetwork)
userDataServer=$(findUserDataServer "$primaryNet")
if [ $publicKeyReceived == 0 ]
then
if publicKey=$(getPublicKeyFromUserDataServer "$userDataServer")
then
dataSource='UserDataServer'
publicKeyReceived=1
fi
fi
if [ $passwordReceived == 0 ]
then
if password=$(getPasswordFromUserDataServer "$userDataServer" "$userDataServerPort")
then
dataSource='UserDataServer'
passwordReceived=1
fi
fi
fi
homeDir=$(findHomeDirectory "$username")
if [ $passwordReceived == 1 ]
then
setPassword "$username" "$homeDir" "$password"
if [ $dataSource == 'UserDataServer' ]
then
sendAckToUserDataServer "$userDataServer" "$userDataServerPort"
fi
fi
if [ $publicKeyReceived == 1 ]
then
setPublicKey "$username" "$homeDir" "$publicKey"
fi