Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active March 23, 2025 12:54
Show Gist options
  • Save lbussy/928efaa37157e57690361ce4cf19a059 to your computer and use it in GitHub Desktop.
Save lbussy/928efaa37157e57690361ce4cf19a059 to your computer and use it in GitHub Desktop.
Manage your shared SSH keys and .ssh config file

πŸ›‘οΈ Secure SSH Key Copy Tool with Safety Checks & Automation

πŸ”Ž Table of Contents

πŸ“– Overview

This script provides a secure and automated way to copy an SSH key to a remote server. It performs the following tasks:

  • βœ… Ensures required SSH utilities are available.
  • βœ… Allows the user to select an SSH host or add a new one.
  • βœ… Prompts for an SSH username with intelligent suggestions.
  • βœ… Lists available SSH keys or generates a new one.
  • βœ… Detects and resolves SSH host key conflicts to prevent authentication failures.
  • βœ… Copies the SSH public key to the remote server securely.
  • βœ… Provides clear feedback and error handling for failed authentication attempts.

🏁 Usage

To use the script, run the following command:

./ssh_copy.sh

Follow the interactive prompts to:

  1. Select or add a new SSH host.
  2. Choose a username for SSH authentication.
  3. Select an existing SSH key or generate a new one.
  4. Copy the SSH public key to the selected server.

⚠️ Note: This script must be run from a terminal with SSH access configured.

βš™οΈ Dependencies

The script requires the following commands to be installed:

  • sh - Secure Shell client.
  • ssh-keygen - Generates SSH key pairs.
  • ssh-copy-id - Automates SSH key copying.
  • awk - Processes text for extracting SSH configurations.
  • grep - Searches for patterns in text.
  • ping - Tests name resolution.

To ensure all dependencies are installed, the script automatically checks for them at runtime.

πŸ“Œ Features

Feature Description
Automated Host Selection Lists existing SSH hosts or allows adding a new one.
Username Detection Detects possible SSH usernames and offers suggestions.
Key Management Lists existing keys and provides an option to generate a new one.
Conflict Resolution Detects and removes outdated SSH host keys to prevent connection failures.
Authentication Handling Ensures secure authentication with fallback options.

πŸ—οΈ Function Reference

1️⃣ check_prerequisites()

Ensures all required SSH utilities and configuration files exist.

  • Verifies the presence of ssh, ssh-keygen, ssh-copy-id, awk, and grep.
  • Creates the ~/.ssh/config file if it does not exist.
  • xits if any required dependency is missing.

2️⃣ check_for_config()

  • Ensures the ssh config file is present.
  • Allows creating the file if it does not exist.

3️⃣ select_host()

Prompts the user to select or add a new SSH host.

  • Lists existing SSH hosts found in ~/.ssh/config.
  • Allows adding a new host by entering an FQDN or IP.
  • Supports mDNS (.local) hostname resolution.
  • Ensures valid and reachable hostnames before proceeding.

4️⃣ select_username()

Prompts the user to choose an SSH username.

  • Detects the default system user and previously used SSH usernames.
  • Offers suggestions based on ~/.ssh/config.
  • Allows manual entry of a custom username.

5️⃣ select_ssh_key()

Lists available SSH keys and allows selection.

  • Displays existing keys stored in ~/.ssh/.
  • Defaults to id_ed25519 if available.
  • Provides an option to generate a new key.

6️⃣ copy_ssh_key()

Copies the SSH public key to the remote host and verifies authentication.

  • Calls handle_known_hosts_conflict() before proceeding.
  • Uses ssh-copy-id to copy the key.
  • Verifies authentication success after key transfer.
  • Falls back to password authentication if key-based login fails.

7️⃣ main()

Main execution flow of the script.

  • Calls all necessary functions in the correct sequence.

πŸ”’ Security Considerations

  • Ensure SSH keys are stored securely in ~/.ssh/ with correct permissions (600).
  • Avoid using weak passwords for SSH keys.
  • Disable password authentication on remote hosts after configuring key-based authentication.

πŸ“œ License

The MIT License (MIT)

Copyright Β© 2024 Lee C. Bussy

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.

πŸ“Œ Summary

  • βœ… Fully interactive SSH key deployment script
  • βœ… Handles authentication conflicts and key management
  • βœ… Works seamlessly with ~/.ssh/config for easy host selection
  • βœ… Ensures a smooth and secure key-based authentication setup

πŸš€ Next Steps

  1. Run the script:
./ssh_copy.sh
  1. Try logging in with SSH:
ssh <your-host-alias>
  1. Configure your remote server to disable password authentication for security.
#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment