From daa183d9fd62c726bfcc02d0e72c4749dce6ab17 Mon Sep 17 00:00:00 2001 From: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Date: Tue, 21 Sep 2021 11:30:58 +0430 Subject: [PATCH] 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 --- ...st-sshkey-password-userdata-configdrive.in | 508 ++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 setup/bindir/cloud-set-guest-sshkey-password-userdata-configdrive.in diff --git a/setup/bindir/cloud-set-guest-sshkey-password-userdata-configdrive.in b/setup/bindir/cloud-set-guest-sshkey-password-userdata-configdrive.in new file mode 100644 index 00000000000..5ac67957d47 --- /dev/null +++ b/setup/bindir/cloud-set-guest-sshkey-password-userdata-configdrive.in @@ -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