|
#!/usr/bin/env bash |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @file ssh_copy.sh |
|
# @brief Secure SSH Key Copy Tool with Safety Checks & Automation |
|
# |
|
# @author Lee C. Bussy (@lbussy) |
|
# @date February 1, 2024 |
|
# @copyright Copyright (c) 2024 Lee C. Bussy |
|
# |
|
# @license MIT License |
|
# |
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
|
# of this software and associated documentation files (the "Software"), to deal |
|
# in the Software without restriction, including without limitation the rights |
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
# copies of the Software, and to permit persons to whom the Software is |
|
# furnished to do so, subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be included in |
|
# all copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
# ----------------------------------------------------------------------------- |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Shell options for script safety: |
|
# - `set -e` : Exit immediately if any command fails. |
|
# - `set -u` : Treat unset variables as an error. |
|
# - `set -o pipefail` : Ensure all pipeline commands must succeed. |
|
# ----------------------------------------------------------------------------- |
|
set -e |
|
set -u |
|
set -o pipefail |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @file ssh_copy.sh |
|
# @brief Global variables used throughout the SSH Key Copy Tool. |
|
# |
|
# @details These variables store critical SSH configuration details, including |
|
# paths, selected user credentials, and key file locations. They are |
|
# utilized across multiple functions for seamless SSH key management. |
|
# |
|
# @global SSH_CONFIG Path to the user's SSH configuration file (~/.ssh/config). |
|
# @global SSH_KEY_DIR Directory containing SSH keys (~/.ssh/). |
|
# @global SSH_ALIAS The SSH alias selected by the user (from ~/.ssh/config). |
|
# @global SSH_USER The username used for SSH authentication. |
|
# @global SSH_KEY The selected SSH private key file for authentication. |
|
# ----------------------------------------------------------------------------- |
|
SSH_CONFIG="$HOME/.ssh/config" |
|
SSH_KEY_DIR="$HOME/.ssh" |
|
SSH_ALIAS="" |
|
SSH_USER="" |
|
SSH_KEY="" |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Ensures that the SSH configuration file exists and is readable. |
|
# |
|
# @details This function checks whether the SSH config file (`~/.ssh/config`) |
|
# exists. If missing, it prompts the user to create it. If the user |
|
# declines, the script exits. The function also verifies that the |
|
# config file is readable and applies proper permissions. |
|
# |
|
# @global SSH_CONFIG Path to the SSH configuration file (`~/.ssh/config`). |
|
# @global HOME User's home directory, used for creating `~/.ssh/`. |
|
# |
|
# @throws Exits if the user declines to create the config file. |
|
# @throws Exits if the config file exists but is not readable. |
|
# |
|
# @example |
|
# check_for_config |
|
# # Ensures SSH config exists before proceeding. |
|
# ----------------------------------------------------------------------------- |
|
check_for_config() { |
|
if [[ ! -f "$SSH_CONFIG" ]]; then |
|
printf "π¨ SSH config file (%s) does not exist.\n" "$SSH_CONFIG" |
|
read -rp "Would you like to create it? (Y/n): " create_config |
|
create_config="${create_config:-Y}" |
|
|
|
if [[ "$create_config" =~ ^[Yy]$ ]]; then |
|
mkdir -p "$HOME/.ssh" |
|
touch "$SSH_CONFIG" |
|
chmod 600 "$SSH_CONFIG" |
|
printf "β
Created SSH config file at %s.\n" "$SSH_CONFIG" |
|
else |
|
printf "β SSH config is required. Exiting.\n" >&2 |
|
exit 1 |
|
fi |
|
fi |
|
|
|
if [[ ! -r "$SSH_CONFIG" ]]; then |
|
printf "β Error: Cannot read SSH config file at %s.\n" "$SSH_CONFIG" >&2 |
|
exit 1 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Verifies and ensures required SSH prerequisites are installed. |
|
# |
|
# @details This function checks for the presence of essential SSH-related |
|
# commands and ensures that the ~/.ssh directory and SSH configuration |
|
# file exist with proper permissions. If any required command is |
|
# missing, the function prints an error message and exits. |
|
# |
|
# @global SSH_CONFIG The path to the SSH configuration file. |
|
# |
|
# @return Exits with an error if a required command is missing. |
|
# |
|
# @throws Exits with status 1 if any required SSH tool is not installed. |
|
# |
|
# @example |
|
# check_prerequisites |
|
# # Ensures SSH tools and configuration exist before proceeding. |
|
# ----------------------------------------------------------------------------- |
|
check_prerequisites() { |
|
local required_cmds=("ssh" "ssh-keygen" "ssh-copy-id" "awk" "grep" "ping") |
|
|
|
for cmd in "${required_cmds[@]}"; do |
|
if ! command -v "$cmd" &>/dev/null; then |
|
echo "β Error: Required command '$cmd' is missing. Install it first. Exiting." |
|
exit 1 |
|
fi |
|
done |
|
|
|
mkdir -p "$HOME/.ssh" |
|
chmod 700 "$HOME/.ssh" |
|
|
|
if [[ ! -f "$SSH_CONFIG" ]]; then |
|
echo "Creating SSH config file at $SSH_CONFIG." |
|
touch "$SSH_CONFIG" |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Handles exit on empty input. |
|
# @details This function checks if the provided input is empty (or only |
|
# contains whitespace). If so, it prints a message and exits |
|
# the script gracefully. |
|
# |
|
# @param $1 The input string to check. |
|
# |
|
# @return This function does not return a value. It exits the script if |
|
# the input is empty. |
|
# |
|
# @example |
|
# exit_if_empty "$user_input" |
|
# ----------------------------------------------------------------------------- |
|
exit_if_empty() { |
|
local input="${1:-}" |
|
if [[ -z "${input// }" ]]; then # Trim whitespace before checking |
|
printf "πͺ No selection made. Exiting.\n" >&2 |
|
exit 0 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Tests if a hostname is valid and reachable using `ping`. |
|
# |
|
# @details Works on Windows (Git Bash, WSL, Cygwin), macOS, and Linux. |
|
# Uses the correct `ping` flags: |
|
# - Windows: `-n 1 -w 2000` (1 ping, 2s timeout) |
|
# - macOS: `-c 1 -t 2` (1 ping, 2s timeout) |
|
# - Linux: `-c 1 -W 2` (1 ping, 2s timeout) |
|
# |
|
# @param $1 Hostname to test. |
|
# @return 0 if the hostname is reachable, 1 otherwise. |
|
# ----------------------------------------------------------------------------- |
|
test_hostname() { |
|
local hostname="$1" |
|
|
|
printf "π Checking hostname: %s\n" "$hostname" |
|
|
|
case "$(uname)" in |
|
MINGW*|CYGWIN*|MSYS*) |
|
# Windows (Git Bash, Cygwin, WSL) |
|
ping -n 1 -w 2000 "$hostname" &>/dev/null && return 0 |
|
;; |
|
Darwin) |
|
# macOS (`-t 2` for 2s timeout) |
|
ping -c 1 -t 2 "$hostname" &>/dev/null && return 0 |
|
;; |
|
Linux) |
|
# Linux (`-W 2` for 2s timeout) |
|
ping -c 1 -W 2 "$hostname" &>/dev/null && return 0 |
|
;; |
|
*) |
|
printf "β οΈ Unsupported OS: %s\n" "$(uname)" >&2 |
|
return 1 |
|
;; |
|
esac |
|
|
|
return 1 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Prompts the user to select an SSH host or add a new one. |
|
# @details This function retrieves a list of available SSH hosts from the |
|
# user's SSH config file (`~/.ssh/config`), sorts them alphabetically, |
|
# and presents a selection menu. If no hosts are found, it prompts |
|
# the user to add a new one. It also handles `.local` mDNS resolution |
|
# and validates manually entered hostnames. |
|
# |
|
# @global SSH_CONFIG Path to the user's SSH configuration file. |
|
# @global SSH_ALIAS Stores the selected SSH alias. |
|
# @global HOSTNAME Stores the manually entered hostname (if applicable). |
|
# @global SSH_PORT Stores the SSH port number for a new host entry. |
|
# |
|
# @throws Exits if no selection is made or an invalid hostname is entered. |
|
# |
|
# @example |
|
# select_host |
|
# ----------------------------------------------------------------------------- |
|
select_host() { |
|
SSH_ALIAS="" |
|
HOSTNAME="" |
|
|
|
mapfile -t HOSTS < <(grep -E "^Host " "$SSH_CONFIG" | awk '{print $2}' | grep -vE "[*?]" | sort) |
|
|
|
if [[ ${#HOSTS[@]} -eq 0 ]]; then |
|
printf "π¨ No existing SSH hosts found in %s. Adding a new host.\n" "$SSH_CONFIG" |
|
SSH_ALIAS="Other" |
|
else |
|
HOSTS+=("Other") |
|
printf "πΉ Select a host to copy the SSH key:\n" |
|
for ((i = 0; i < ${#HOSTS[@]}; i++)); do |
|
printf "%d) %s\n" "$((i + 1))" "${HOSTS[$i]}" |
|
done |
|
|
|
while true; do |
|
read -rp "π Choice: " choice |
|
exit_if_empty "$choice" |
|
|
|
if [[ "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -le "${#HOSTS[@]}" ]]; then |
|
SSH_ALIAS="${HOSTS[$((choice - 1))]}" |
|
break |
|
else |
|
printf "β Invalid selection. Please enter a number from the list.\n" >&2 |
|
fi |
|
done |
|
fi |
|
|
|
if [[ "$SSH_ALIAS" == "Other" ]]; then |
|
while true; do |
|
read -rp "π Enter the full hostname (FQDN or IP): " HOSTNAME |
|
exit_if_empty "$HOSTNAME" |
|
|
|
if test_hostname "$HOSTNAME"; then |
|
printf "β
Hostname '%s' is valid and reachable.\n" "$HOSTNAME" |
|
break |
|
else |
|
printf "β Error: Hostname '%s' is invalid or unreachable. Please try again.\n" "$HOSTNAME" >&2 |
|
fi |
|
done |
|
|
|
DEFAULT_HOST=$(echo "$HOSTNAME" | awk -F. '{print $1}') |
|
printf "π Enter SSH config alias for this host (default: %s): " "$DEFAULT_HOST" |
|
read -r SSH_ALIAS |
|
SSH_ALIAS="${SSH_ALIAS:-$DEFAULT_HOST}" |
|
|
|
read -rp "πͺ Enter SSH port (default: 22): " SSH_PORT |
|
SSH_PORT="${SSH_PORT:-22}" |
|
|
|
select_username |
|
add_host_to_ssh_config |
|
fi |
|
|
|
printf "β
Selected Host: %s\n" "$SSH_ALIAS" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Prompts the user to select an SSH username. |
|
# @details This function retrieves a previously selected or configured username |
|
# from `~/.ssh/config`, detects the current OS user, and offers a |
|
# selection menu. If no previous username is found, it defaults to `pi`. |
|
# The user can choose from detected usernames or enter a custom one. |
|
# |
|
# @global SSH_CONFIG Path to the user's SSH configuration file. |
|
# @global SSH_ALIAS Stores the selected SSH alias. |
|
# @global SSH_USER Stores the selected SSH username. |
|
# |
|
# @throws Exits if the user selects "Other" and does not provide a valid username. |
|
# |
|
# @example |
|
# select_username |
|
# ----------------------------------------------------------------------------- |
|
select_username() { |
|
if [[ -n "${SSH_USER:-}" ]]; then |
|
printf "β
Using previously selected username: %s\n" "$SSH_USER" |
|
return |
|
fi |
|
|
|
local detected_user |
|
detected_user=$(whoami) |
|
|
|
local found_user |
|
found_user=$(awk "/^Host $SSH_ALIAS\$/ {f=1} f && /^ *User / {print \$2; exit}" "$SSH_CONFIG") |
|
|
|
SSH_USER="${found_user:-pi}" |
|
|
|
local username_choices=() |
|
[[ "$SSH_USER" != "$detected_user" ]] && username_choices+=("$SSH_USER") |
|
username_choices+=("$detected_user") |
|
[[ "$detected_user" != "pi" && "$SSH_USER" != "pi" ]] && username_choices+=("pi") |
|
username_choices+=("Other") |
|
|
|
printf "πΉ Select a username:\n" |
|
for i in "${!username_choices[@]}"; do |
|
printf "%d) %s\n" "$((i + 1))" "${username_choices[$i]}" |
|
done |
|
|
|
while true; do |
|
read -rp "π Choice: " user_choice |
|
[[ -z "$user_choice" ]] && printf "β
Selected Username: %s\n" "$SSH_USER" && return |
|
|
|
if [[ "$user_choice" =~ ^[0-9]+$ ]] && ((user_choice >= 1 && user_choice <= ${#username_choices[@]})); then |
|
SSH_USER="${username_choices[$((user_choice - 1))]}" |
|
break |
|
else |
|
printf "β Invalid selection. Please enter a valid number.\n" >&2 |
|
fi |
|
done |
|
|
|
if [[ "$SSH_USER" == "Other" ]]; then |
|
read -rp "π€ Enter the username: " SSH_USER |
|
exit_if_empty "$SSH_USER" |
|
fi |
|
|
|
printf "β
Selected Username: %s\n" "$SSH_USER" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Generates a new SSH key with user-selected options. |
|
# @details This function prompts the user to select an SSH key type and filename, |
|
# then generates a new key with an optional custom comment. If a key |
|
# with the chosen name already exists, the user is asked whether to |
|
# overwrite it or keep the existing key. |
|
# |
|
# @global SSH_KEY_DIR Directory where SSH keys are stored. |
|
# @global SSH_USER Stores the selected SSH username. |
|
# @global SSH_KEY Stores the generated SSH key file path. |
|
# |
|
# @throws Exits if the user does not select a valid key type. |
|
# |
|
# @example |
|
# generate_ssh_key |
|
# ----------------------------------------------------------------------------- |
|
generate_ssh_key() { |
|
local key_types=("ED25519 (Recommended)" "RSA (Legacy, 4096-bit)" "ECDSA" "DSA") |
|
local key_args="" |
|
|
|
while true; do |
|
echo "Select an SSH key type (Press Enter to exit):" |
|
for ((i = 0; i < ${#key_types[@]}; i++)); do |
|
echo "$((i + 1))) ${key_types[$i]}" |
|
done |
|
|
|
read -rp "Choice: " choice |
|
[[ -z "$choice" ]] && echo "No selection made. Exiting." && exit 0 # β
Exit if Enter is pressed |
|
|
|
case "$choice" in |
|
1) KEY_TYPE="ed25519"; key_args=""; break ;; |
|
2) KEY_TYPE="rsa"; key_args="-b 4096"; break ;; |
|
3) KEY_TYPE="ecdsa"; key_args=""; break ;; |
|
4) KEY_TYPE="dsa"; key_args=""; break ;; |
|
*) echo "β Invalid selection. Please enter a valid number." ;; |
|
esac |
|
done |
|
|
|
# β
Prompt for filename but apply default if empty |
|
read -rp "Enter filename for the new key (default: id_$KEY_TYPE): " NEW_KEY_NAME |
|
[[ -z "$NEW_KEY_NAME" ]] && NEW_KEY_NAME="id_$KEY_TYPE" |
|
|
|
NEW_KEY_PATH="$SSH_KEY_DIR/$NEW_KEY_NAME" |
|
|
|
# β
If key exists, prompt before overwriting (default is "No") |
|
if [[ -f "$NEW_KEY_PATH" ]]; then |
|
read -rp "The key '$NEW_KEY_NAME' exists. Overwrite? (y/N): " OVERWRITE_KEY |
|
[[ -z "$OVERWRITE_KEY" ]] && OVERWRITE_KEY="n" # β
Default to "No" if empty |
|
[[ "$OVERWRITE_KEY" != "y" ]] && SSH_KEY="$NEW_KEY_PATH" && return |
|
rm -f "$NEW_KEY_PATH" "$NEW_KEY_PATH.pub" |
|
fi |
|
|
|
# β
Ensure SSH_USER is set correctly |
|
SSH_USER="${SSH_USER:-$(whoami)}" |
|
|
|
# β
Set the correct default key comment (Selected Username @ Current Machine) |
|
DEFAULT_COMMENT="$SSH_USER@$(hostname -f)" |
|
|
|
# β
Ask if the user wants to override the default comment |
|
read -rp "Use default key comment ($DEFAULT_COMMENT)? (Y/n): " COMMENT_OVERRIDE |
|
[[ -z "$COMMENT_OVERRIDE" ]] && COMMENT_OVERRIDE="y" # Default to "yes" |
|
|
|
if [[ "$COMMENT_OVERRIDE" =~ ^[Nn]$ ]]; then |
|
read -rp "Enter custom comment: " CUSTOM_COMMENT |
|
[[ -z "$CUSTOM_COMMENT" ]] && CUSTOM_COMMENT="$DEFAULT_COMMENT" # β
If empty, use default |
|
else |
|
CUSTOM_COMMENT="$DEFAULT_COMMENT" |
|
fi |
|
|
|
# β
Generate the key with the chosen comment and handle RSA correctly |
|
ssh-keygen -t "$KEY_TYPE" "$key_args" -C "$CUSTOM_COMMENT" -f "$NEW_KEY_PATH" |
|
SSH_KEY="$NEW_KEY_PATH" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Allows the user to select an existing SSH key or generate a new one. |
|
# @details This function lists available SSH private keys stored in `~/.ssh/`, |
|
# allowing the user to select one or generate a new key if none exist. |
|
# If only one key is found, it is automatically selected unless the |
|
# user chooses to override it. |
|
# |
|
# @global SSH_KEY_DIR Directory where SSH keys are stored. |
|
# @global SSH_KEY Stores the selected or newly generated SSH key file path. |
|
# |
|
# @throws Exits if no key is selected and the user does not explicitly choose |
|
# to generate a new one. |
|
# |
|
# @example |
|
# select_ssh_key |
|
# ----------------------------------------------------------------------------- |
|
select_ssh_key() { |
|
SSH_KEYS=() |
|
|
|
# β
Collect available private keys (skip .pub files) |
|
for key in "$SSH_KEY_DIR"/id_*; do |
|
[[ -f "$key" && ! "$key" =~ \.pub$ ]] || continue |
|
SSH_KEYS+=("$(basename "$key")") |
|
done |
|
|
|
# β
Determine default key |
|
local default_key="" |
|
if [[ ${#SSH_KEYS[@]} -eq 1 ]]; then |
|
default_key="${SSH_KEYS[0]}" |
|
elif [[ " ${SSH_KEYS[*]} " =~ " id_ed25519 " ]]; then |
|
default_key="id_ed25519" |
|
fi |
|
|
|
# β
Always provide "Generate New Key" as an option |
|
SSH_KEYS+=("Generate New Key") |
|
|
|
echo "Available SSH keys (Press Enter to select default: ${default_key:-Generate New Key}):" |
|
for i in "${!SSH_KEYS[@]}"; do |
|
echo "$((i + 1))) ${SSH_KEYS[$i]}" |
|
done |
|
|
|
while true; do |
|
read -rp "Choice: " choice |
|
|
|
# β
If Enter is pressed, use the default (if available) |
|
if [[ -z "$choice" ]]; then |
|
if [[ -n "$default_key" ]]; then |
|
SSH_KEY="$SSH_KEY_DIR/$default_key" |
|
echo "β
Defaulting to: $default_key" |
|
return |
|
else |
|
echo "π¨ No SSH keys found. Generating a new key " |
|
generate_ssh_key |
|
return |
|
fi |
|
fi |
|
|
|
# β
Validate input and allow "Generate New Key" |
|
if [[ "$choice" =~ ^[0-9]+$ ]] && ((choice >= 1 && choice <= ${#SSH_KEYS[@]})); then |
|
SSH_KEY="${SSH_KEYS[$((choice - 1))]}" |
|
break |
|
else |
|
echo "β Invalid selection. Please enter a valid number." |
|
fi |
|
done |
|
|
|
# β
Handle the selection |
|
case "$SSH_KEY" in |
|
"Generate New Key") generate_ssh_key ;; |
|
*) SSH_KEY="$SSH_KEY_DIR/$SSH_KEY" ;; |
|
esac |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Detects and resolves SSH host key conflicts. |
|
# @details This function checks for SSH host key conflicts in `~/.ssh/known_hosts` |
|
# and prompts the user to remove the old key if a mismatch is detected. |
|
# It ensures that the SSH key verification process does not fail due |
|
# to an outdated or incorrect host key. |
|
# |
|
# @param $1 The SSH alias to check (as defined in `~/.ssh/config`). |
|
# |
|
# @global SSH_CONFIG Path to the SSH configuration file. |
|
# @global HOME User's home directory, used for locating known_hosts. |
|
# |
|
# @throws Exits if the user chooses not to remove the conflicting host key. |
|
# |
|
# @example |
|
# handle_known_hosts_conflict "wspr5" |
|
# ----------------------------------------------------------------------------- |
|
handle_known_hosts_conflict() { |
|
local alias="$1" |
|
|
|
# β
Extract FQDN/IP from ~/.ssh/config |
|
local host |
|
host=$(awk -v alias="$alias" ' |
|
$1 == "Host" && $2 == alias { in_host = 1 } |
|
in_host && $1 == "HostName" { print $2; exit } |
|
' "$SSH_CONFIG") |
|
|
|
# β
If no HostName is found, default to the alias |
|
[[ -z "$host" ]] && host="$alias" |
|
|
|
echo "π Checking for SSH host key conflicts with '$host' " |
|
|
|
# β
Check if the host already exists in known_hosts |
|
if ! ssh-keygen -F "$host" &>/dev/null; then |
|
return 0 # β
No conflict detected, continue |
|
fi |
|
|
|
# β
Get the current known host key fingerprint |
|
local known_fingerprint |
|
known_fingerprint=$(ssh-keygen -lf <(ssh-keygen -F "$host" | tail -n1 | awk '{print $2}') 2>/dev/null | awk '{print $2}') |
|
|
|
# β
Get the actual current fingerprint from the remote server (FQDN/IP) |
|
local new_fingerprint |
|
new_fingerprint=$(ssh-keyscan -t ed25519 "$host" 2>/dev/null | ssh-keygen -lf - | awk '{print $2}') |
|
|
|
# β
Compare known fingerprint with the new one |
|
if [[ "$known_fingerprint" != "$new_fingerprint" ]]; then |
|
echo "" |
|
echo "π¨ WARNING: The SSH host key for '$host' has changed!" |
|
echo "This may be due to a server reinstall or a security risk." |
|
echo "" |
|
echo "To fix this, the old host key must be removed from your known_hosts file." |
|
read -rp "Do you want to remove the old host key and continue? (y/N): " response |
|
|
|
if [[ "$response" =~ ^[Yy]$ ]]; then |
|
ssh-keygen -R "$host" |
|
echo "β
Old host key removed." |
|
|
|
# β
Re-add the new key immediately to prevent further SSH failures |
|
ssh-keyscan -H "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null |
|
echo "β
New host key added to known_hosts." |
|
else |
|
echo "β SSH host key verification failed. Exiting for safety." |
|
exit 1 |
|
fi |
|
else |
|
echo "β
No host key conflict detected. Continuing " |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Copies an SSH public key to the remote host for authentication. |
|
# @details This function ensures a secure key-based SSH authentication setup |
|
# by copying the selected SSH public key to the remote server. |
|
# It also handles SSH host key conflicts, verifies key authentication, |
|
# and provides fallback methods if key-based authentication fails. |
|
# |
|
# @global SSH_USER The selected SSH username. |
|
# @global SSH_ALIAS The alias or hostname of the remote SSH server. |
|
# @global SSH_KEY The full path to the SSH private key file. |
|
# |
|
# @throws Exits if SSH key authentication and password authentication both fail. |
|
# |
|
# @example |
|
# copy_ssh_key |
|
# ----------------------------------------------------------------------------- |
|
copy_ssh_key() { |
|
printf "π Copying SSH key to: %s@%s\n" "$SSH_USER" "$SSH_ALIAS" |
|
set +e # Allow failure handling |
|
|
|
# β
Ensure no host key conflicts before proceeding |
|
handle_known_hosts_conflict "$SSH_ALIAS" |
|
|
|
ssh-copy-id -i "$SSH_KEY" "$SSH_USER@$SSH_ALIAS" &>/dev/null |
|
local ssh_copy_exit=$? |
|
|
|
if [[ $ssh_copy_exit -ne 0 ]]; then |
|
printf "β ERROR: SSH key copy failed. Checking alternative authentication methods.\n" |
|
fi |
|
|
|
printf "π Verifying SSH access to %s@%s\n" "$SSH_USER" "$SSH_ALIAS" |
|
ssh -o BatchMode=yes "$SSH_USER@$SSH_ALIAS" exit &>/dev/null |
|
local login_status=$? |
|
|
|
if [[ $login_status -eq 0 ]]; then |
|
set -e |
|
printf "β
SSH key successfully copied.\n" |
|
finalize_ssh_login_message |
|
return 0 |
|
fi |
|
|
|
printf "β ERROR: SSH login failed after copying the key.\n" |
|
printf "π Checking authentication methods.\n" |
|
|
|
local auth_methods |
|
auth_methods=$(ssh -o PreferredAuthentications=none "$SSH_USER@$SSH_ALIAS" 2>&1 | \ |
|
grep -o "Permission denied (.*)" | awk -F '[()]' '{print $2}') |
|
|
|
if [[ "$auth_methods" == "publickey" ]]; then |
|
printf "β ERROR: The remote host only allows public key authentication, and the provided key was rejected.\n" |
|
printf "βΉοΈ You must manually add a valid SSH key to %s@%s before you can connect.\n" "$SSH_USER" "$SSH_ALIAS" |
|
exit 1 |
|
fi |
|
|
|
if [[ "$auth_methods" == *"password"* ]]; then |
|
printf "π¨ SSH key authentication failed. Attempting password authentication.\n" |
|
ssh "$SSH_USER@$SSH_ALIAS" |
|
local retval=$? |
|
if [[ $retval -eq 0 ]]; then |
|
printf "β
Password authentication successful. Manually add an SSH key.\n" |
|
finalize_ssh_login_message |
|
exit 0 |
|
fi |
|
fi |
|
|
|
printf "β ERROR: Both SSH key and password authentication failed!\n" |
|
exit 1 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Displays the correct SSH login command for the user. |
|
# @details This function determines the appropriate SSH login command based on |
|
# whether the selected SSH alias has a corresponding user entry in |
|
# the SSH configuration file. If a `User` entry exists, it suggests |
|
# using `ssh <alias>`, otherwise, it provides the full login format |
|
# `ssh user@host`. |
|
# |
|
# @global SSH_ALIAS The alias of the SSH host. |
|
# @global SSH_CONFIG Path to the SSH configuration file. |
|
# @global SSH_USER The username for SSH login. |
|
# |
|
# @example |
|
# finalize_ssh_login_message |
|
# ----------------------------------------------------------------------------- |
|
finalize_ssh_login_message() { |
|
printf "π Now try logging in with: ssh %s\n" "$SSH_ALIAS" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Adds a new SSH host entry to the SSH configuration file. |
|
# @details This function appends a new host entry to the SSH configuration |
|
# file (`~/.ssh/config`), including the alias, hostname, username, |
|
# and port. If any required fields are missing, the function exits |
|
# with an error. |
|
# |
|
# @global SSH_ALIAS The alias name for the SSH config entry. |
|
# @global HOSTNAME The full hostname (FQDN) or IP address. |
|
# @global SSH_USER The selected SSH username. |
|
# @global SSH_PORT The SSH port number. |
|
# @global SSH_CONFIG Path to the SSH configuration file. |
|
# |
|
# @throws Exits if required fields (`SSH_ALIAS`, `HOSTNAME`, or `SSH_USER`) |
|
# are missing. |
|
# |
|
# @example |
|
# add_host_to_ssh_config |
|
# ----------------------------------------------------------------------------- |
|
add_host_to_ssh_config() { |
|
printf "πΉ Adding '%s' to SSH config.\n" "$SSH_ALIAS" |
|
[[ -z "$SSH_ALIAS" || -z "$HOSTNAME" || -z "$SSH_USER" ]] && { printf "β Error: Missing required fields.\n" >&2; exit 1; } |
|
|
|
printf "\nHost %s\n HostName %s\n User %s\n Port %s\n" \ |
|
"$SSH_ALIAS" "$HOSTNAME" "$SSH_USER" "$SSH_PORT" >> "$SSH_CONFIG" |
|
|
|
printf "β
Added '%s' to SSH config.\n" "$SSH_ALIAS" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Main execution flow for SSH key setup and deployment. |
|
# @details This function orchestrates the entire SSH key copy process by: |
|
# - Ensuring necessary SSH prerequisites are met. |
|
# - Prompting the user to select an SSH host. |
|
# - Selecting or defining an SSH username. |
|
# - Listing or generating an SSH key. |
|
# - Copying the SSH key to the selected host while handling conflicts. |
|
# |
|
# @calls check_prerequisites Ensures required SSH tools and files exist. |
|
# @calls select_host Prompts the user to select or add a new SSH host. |
|
# @calls select_username Determines the SSH username for authentication. |
|
# @calls select_ssh_key Lists available SSH keys or generates a new one. |
|
# @calls copy_ssh_key Copies the SSH key to the selected host. |
|
# |
|
# @example |
|
# main |
|
# ----------------------------------------------------------------------------- |
|
main() { |
|
check_prerequisites |
|
check_for_config |
|
select_host |
|
select_username |
|
select_ssh_key |
|
copy_ssh_key |
|
} |
|
|
|
main |