在日常的服务器管理和开发工作中,我们经常需要通过 SSH 连接到多台远程主机。管理这些连接,特别是当某些主机需要密码登录而另一些使用密钥登录时,可能会变得繁琐。
让 Google Gemini
帮忙写了一个 nssh
是一个 Bash 脚本工具,旨在通过与 ~/.ssh/config
文件集成,简化这一过程,并提供主机列表选择、自定义密码文件支持等功能。
功能特性
- 与
~/.ssh/config
深度集成: 利用您现有的 SSH 配置文件进行主机参数(如 HostName, User, Port, IdentityFile)的解析。 - 自定义密码文件支持: 通过在
~/.ssh/config
中为特定主机添加pwfilepath
指令,nssh
可以从指定的文件中读取密码并使用sshpass
自动登录。 - 智能认证回退: 如果主机未配置
pwfilepath
,nssh
会自动回退到标准的 SSH 行为(通常是密钥认证或提示手动输入密码)。 - 主机列表与选择: 使用
nssh -l
或nssh --list
命令,可以列出~/.ssh/config
中定义的所有主机,并通过数字选择快速连接。 - 帮助信息: 通过
nssh -h
或nssh --help
提供详细的使用说明。 - 参数透传: 支持将额外的 SSH 选项或远程命令直接传递给底层的
ssh
命令。 - 兼容性: 旨在避免因自定义指令(如
pwfilepath
)导致原生ssh
命令解析配置文件出错。
依赖与环境
依赖项
- Bash: 版本需 >= 4.0(因为脚本中使用了
mapfile
命令)。大多数现代 Linux 发行版都满足此要求。- 检查 Bash 版本:
bash --version
- 检查 Bash 版本:
sshpass
: 用于非交互式地向 SSH 提供密码。如果系统中未安装,脚本会提示。- 安装 (Ubuntu/Debian):
sudo apt update && sudo apt install sshpass
- 安装 (Fedora/CentOS/RHEL):
sudo yum install sshpass
或sudo dnf install sshpass
- 安装 (Arch Linux):
sudo pacman -S sshpass
- 安装 (Ubuntu/Debian):
- OpenSSH 客户端: 即标准的
ssh
命令。 - 标准 GNU Coreutils: 脚本使用了
awk
,sed
,grep
,stat
,sort
等常见工具,这些在大多数 Linux 发行版中都是标配。
支持环境
- 主流现代 Linux 发行版:
- Ubuntu (如 20.04, 22.04, 24.04 LTS)
- Debian 及其衍生版
- Fedora, CentOS Stream, RHEL 及其兼容版
- Arch Linux 及其衍生版
- 潜在问题:
- macOS / BSD:
stat -c "%a"
(GNUstat
语法) 需要替换为对应系统的stat
语法 (例如 macOS 上是stat -f "%Lp"
)。macOS 默认 Bash 版本可能较低,需要通过 Homebrew 安装新版 Bash。 - 非常古老的 Linux发行版: Bash 版本可能低于 4.0,导致
mapfile
失败。 - 极度精简的 Linux 环境: 可能缺少某些依赖工具。
- macOS / BSD:
安装
- 创建脚本文件:
将下面的脚本内容保存到一个名为nssh
的文件中。推荐的存放位置是~/bin
(如果已将其加入PATH
环境变量) 或/usr/local/bin
(需要管理员权限)。
例如,保存到/usr/local/bin/nssh
。 -
赋予执行权限:
sudo chmod +x /usr/local/bin/nssh
配置
nssh
的核心配置依赖于两个文件:标准的 SSH
客户端配置文件 (~/.ssh/config
) 和您为密码登录主机指定的密码文件。
1. SSH 客户端配置文件 (~/.ssh/config
)
确保该文件存在且权限为 600
:
touch ~/.ssh/config
chmod 600 ~/.ssh/config
在该文件中,您可以像往常一样定义主机。对于 nssh
,关键是为需要密码登录的主机添加一个自定义的 pwfilepath
指令。
示例 ~/.ssh/config
:
# 主机1: 使用密钥登录 (nssh 会正常处理)
Host server-key
HostName key-server.example.com
User admin
Port 22
IdentityFile ~/.ssh/id_rsa_server_key
# 主机2: 使用密码登录 (nssh 特殊处理)
Host server-pass-prod
HostName prod.example.com
User deploy_user
Port 2222
# nssh 自定义指令: 指向包含密码的文件
pwfilepath ~/.ssh/my_server_passwords.txt
# 主机3: 另一个使用密码的主机 (可以指向同一个密码文件)
Host dev-vm
HostName 192.168.1.100
User developer
pwfilepath ~/.ssh/my_server_passwords.txt
2. 密码文件
根据 pwfilepath
指令中设置的路径创建密码文件。
示例密码文件 (例如 ~/.ssh/my_server_passwords.txt):
# 文件格式: Host别名=密码
# Host别名必须与 ~/.ssh/config 中的 'Host' 定义完全一致
server-pass-prod=MySup3rS3cur3P@ssw0rd!
dev-vm=simpleDevPass123
重要: 密码文件包含敏感信息,必须严格限制其权限:
chmod 600 ~/.ssh/my_server_passwords.txt
脚本内容
#!/bin/bash
# --- nssh ---
# Wrapper for ssh to allow password-based authentication via a password file
# if specified in ~/.ssh/config using a custom 'pwfilepath' directive.
# Also provides a host listing and selection feature.
# --- Configuration ---
SSH_CONFIG_FILE="$HOME/.ssh/config"
# --- Helper Functions ---
print_help() {
echo "nssh - Enhanced SSH connection tool"
echo
echo "Usage: nssh [options] <host_alias> [ssh_options_or_remote_command...]"
echo " nssh -l | --list"
echo " nssh -h | --help"
echo
echo "Options:"
echo " <host_alias> The host alias defined in your ~/.ssh/config to connect to."
echo " -l, --list List all host aliases from ~/.ssh/config and prompt for selection."
echo " -h, --help Show this help message and exit."
echo " [ssh_options_or_remote_command] Optional arguments to pass directly to the underlying ssh command."
echo
echo "Description:"
echo " nssh simplifies SSH connections by:"
echo " 1. Allowing password-based authentication using a 'pwfilepath' directive in ~/.ssh/config."
echo " If 'pwfilepath' is set for a host, nssh reads the password from the specified file."
echo " The password file should contain entries like: host_alias=password"
echo " Ensure the password file has restrictive permissions (e.g., chmod 600)."
echo " 2. Falling back to standard key-based (or interactive password) SSH if 'pwfilepath' is not set."
echo " 3. Providing a host selection menu via '-l' or '--list'."
echo
echo "Example ~/.ssh/config entry for password-based auth:"
echo " Host my_server_pass"
echo " HostName server.example.com"
echo " User myuser"
echo " pwfilepath ~/.ssh/my_passwords.txt"
echo
echo "Example ~/.ssh/config entry for key-based auth:"
echo " Host my_server_key"
echo " HostName key.example.com"
echo " User anotheruser"
echo " IdentityFile ~/.ssh/id_rsa_server_key"
echo
echo "To connect: nssh my_server_pass"
echo " nssh my_server_key"
echo " nssh -l (then choose from list)"
}
# Function to parse ~/.ssh/config and extract all Host aliases
parse_ssh_config_for_hosts() {
local config_file="$SSH_CONFIG_FILE"
local hosts_array=()
if [ ! -f "$config_file" ]; then
echo "Warning: SSH config file '$config_file' not found." >&2
return 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line_clean=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/#.*//')
if [[ "$line_clean" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
IFS=' ' read -ra patterns <<< "$(echo "$line_clean" | awk '{$1=""; print $0}' | sed 's/^[[:space:]]*//')"
for pattern in "${patterns[@]}"; do
if [[ -n "$pattern" && "$pattern" != "*" ]]; then
if ! [[ " ${hosts_array[*]} " =~ " ${pattern} " ]]; then
hosts_array+=("$pattern")
fi
fi
done
fi
done < "$config_file"
IFS=$'\n' sorted_hosts_array=($(sort <<<"${hosts_array[*]}"))
unset IFS
for host in "${sorted_hosts_array[@]}"; do
echo "$host"
done
return 0
}
# Function to get a specific config value for a host from ~/.ssh/config
get_ssh_config_value() {
local host_alias="$1"
local key_to_find="$2"
local config_file="$SSH_CONFIG_FILE"
local value=""
local in_host_block=0
local host_match_level=0
if [ ! -f "$config_file" ]; then
return
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line_clean=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/#.*//')
if [[ -z "$line_clean" ]]; then continue; fi
if [[ "$line_clean" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
current_host_patterns=$(echo "$line_clean" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}')
in_host_block=0
for pattern in $current_host_patterns; do
if [[ "$host_alias" == "$pattern" ]]; then
in_host_block=1
host_match_level=2
break
fi
done
elif [[ "$in_host_block" -eq 1 && "$host_match_level" -ge 2 ]]; then
if echo "$line_clean" | grep -iqE "^${key_to_find}[[:space:]]+"; then
value=$(echo "$line_clean" | sed -E -e "s/^[[:space:]]*${key_to_find}[[:space:]]+(.*)/\1/" -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
fi
fi
done < "$config_file"
if [ -z "$value" ]; then
in_host_block=0
local temp_value_wildcard=""
local temp_value_global=""
while IFS= read -r line || [[ -n "$line" ]]; do
line_clean=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/#.*//')
if [[ -z "$line_clean" ]]; then continue; fi
if [[ "$line_clean" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
current_host_patterns=$(echo "$line_clean" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}')
in_host_block=0
for pattern in $current_host_patterns; do
if [[ "$pattern" == "*" ]]; then
in_host_block=2
break
elif [[ "${pattern: -1}" == "*" && "${host_alias}" == "${pattern:0:-1}"* ]]; then
in_host_block=1
break
fi
done
elif [[ "$in_host_block" -gt 0 ]]; then
if echo "$line_clean" | grep -iqE "^${key_to_find}[[:space:]]+"; then
local current_block_value=$(echo "$line_clean" | sed -E -e "s/^[[:space:]]*${key_to_find}[[:space:]]+(.*)/\1/" -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [[ "$in_host_block" -eq 1 ]]; then
temp_value_wildcard="$current_block_value"
elif [[ "$in_host_block" -eq 2 ]]; then
temp_value_global="$current_block_value"
fi
fi
fi
done < "$config_file"
if [ -n "$temp_value_wildcard" ]; then
value="$temp_value_wildcard"
elif [ -n "$temp_value_global" ]; then
value="$temp_value_global"
fi
fi
echo "$value"
}
# --- Main Logic ---
if ! command -v sshpass &> /dev/null && [[ "$1" != "-h" && "$1" != "--help" && "$1" != "-l" && "$1" != "--list" ]]; then
echo "Error: sshpass command not found. Please install it (e.g., sudo apt install sshpass)."
exit 1
fi
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
print_help
exit 0
fi
if [[ "$1" == "-l" || "$1" == "--list" ]]; then
mapfile -t hosts < <(parse_ssh_config_for_hosts)
if [ ${#hosts[@]} -eq 0 ]; then
echo "No hosts found in $SSH_CONFIG_FILE or the file does not exist."
exit 1
fi
echo "Available hosts:"
for i in "${!hosts[@]}"; do
printf "%3d) %s\n" $((i + 1)) "${hosts[$i]}"
done
echo
read -p "Enter number to connect (or q to quit): " choice
if [[ "$choice" =~ ^[qQ]$ ]]; then
exit 0
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#hosts[@]} ]; then
echo "Invalid selection."
exit 1
fi
HOST_ALIAS="${hosts[$((choice - 1))]}"
echo "Selected host: $HOST_ALIAS"
set -- # Clear positional parameters for list mode, as no further args are expected
elif [ -z "$1" ]; then
print_help
exit 1
else
HOST_ALIAS="$1"
shift
fi
PASSWORD_FILE_PATH_CONFIG=$(get_ssh_config_value "$HOST_ALIAS" "pwfilepath")
if [ -n "$PASSWORD_FILE_PATH_CONFIG" ]; then
PASSWORD_FILE_PATH=$(eval echo "$PASSWORD_FILE_PATH_CONFIG")
if [ ! -f "$PASSWORD_FILE_PATH" ]; then
echo "Error: Password file '$PASSWORD_FILE_PATH' specified for host '$HOST_ALIAS' not found."
exit 1
fi
PERMISSIONS=$(stat -c "%a" "$PASSWORD_FILE_PATH") # GNU stat syntax
if [ "$PERMISSIONS" != "600" ] && [ "$PERMISSIONS" != "400" ]; then
echo "Warning: Password file '$PASSWORD_FILE_PATH' has insecure permissions ($PERMISSIONS). Should be 600 or 400."
fi
PASSWORD_ENTRY=$(grep -E "^${HOST_ALIAS}=" "$PASSWORD_FILE_PATH")
if [ -z "$PASSWORD_ENTRY" ]; then
echo "Error: Password for host '$HOST_ALIAS' not found in '$PASSWORD_FILE_PATH'."
echo "Expected format: $HOST_ALIAS=yourpassword"
exit 1
fi
PASSWORD=$(echo "$PASSWORD_ENTRY" | cut -d'=' -f2-)
if [ -z "$PASSWORD" ]; then
echo "Error: Password for host '$HOST_ALIAS' is empty in '$PASSWORD_FILE_PATH'."
exit 1
fi
echo "Attempting password authentication for '$HOST_ALIAS' using password from '$PASSWORD_FILE_PATH'..."
ACTUAL_HOSTNAME=$(get_ssh_config_value "$HOST_ALIAS" "HostName")
ACTUAL_USER=$(get_ssh_config_value "$HOST_ALIAS" "User")
ACTUAL_PORT_RAW=$(get_ssh_config_value "$HOST_ALIAS" "Port")
if [ -z "$ACTUAL_HOSTNAME" ]; then
echo "Error: HostName for '$HOST_ALIAS' not found in $SSH_CONFIG_FILE for password auth."
exit 1
fi
SSH_CMD_ARGS=()
if [ -n "$ACTUAL_PORT_RAW" ]; then SSH_CMD_ARGS+=("-p" "$ACTUAL_PORT_RAW"); fi
SSH_CMD_ARGS+=("-o" "PubkeyAuthentication=no")
SSH_CMD_ARGS+=("-o" "PasswordAuthentication=yes")
if [ -n "$ACTUAL_USER" ]; then SSH_CMD_ARGS+=("${ACTUAL_USER}@${ACTUAL_HOSTNAME}"); else SSH_CMD_ARGS+=("${ACTUAL_HOSTNAME}"); fi
SSH_CMD_ARGS+=("$@")
sshpass -p "$PASSWORD" ssh -F /dev/null "${SSH_CMD_ARGS[@]}"
exit $?
else
echo "No 'pwfilepath' configured for '$HOST_ALIAS'. Attempting key-based SSH..."
KEY_HOSTNAME=$(get_ssh_config_value "$HOST_ALIAS" "HostName")
KEY_USER=$(get_ssh_config_value "$HOST_ALIAS" "User")
KEY_PORT_RAW=$(get_ssh_config_value "$HOST_ALIAS" "Port")
KEY_IDENTITYFILE_RAW=$(get_ssh_config_value "$HOST_ALIAS" "IdentityFile")
if [ -z "$KEY_HOSTNAME" ]; then
echo "Info: HostName for '$HOST_ALIAS' not found in $SSH_CONFIG_FILE for key-based auth."
echo "This could also mean the host alias '$HOST_ALIAS' is not defined in the config file."
echo "Falling back to direct ssh call for '$HOST_ALIAS', which might use system defaults or fail."
ssh "$HOST_ALIAS" "$@"
exit $?
fi
SSH_KEY_CMD_ARGS=()
if [ -n "$KEY_PORT_RAW" ]; then SSH_KEY_CMD_ARGS+=("-p" "$KEY_PORT_RAW"); fi
if [ -n "$KEY_IDENTITYFILE_RAW" ]; then
EXPANDED_IDENTITYFILE=$(eval echo "$KEY_IDENTITYFILE_RAW")
SSH_KEY_CMD_ARGS+=("-i" "$EXPANDED_IDENTITYFILE")
fi
if [ -n "$KEY_USER" ]; then SSH_KEY_CMD_ARGS+=("${KEY_USER}@${KEY_HOSTNAME}"); else SSH_KEY_CMD_ARGS+=("${KEY_HOSTNAME}"); fi
SSH_KEY_CMD_ARGS+=("$@")
echo "Attempting key-based SSH for '$HOST_ALIAS' with explicitly parsed parameters (ignoring user ssh_config for pwfilepath compatibility)..."
ssh -F /dev/null "${SSH_KEY_CMD_ARGS[@]}"
exit $?
fi
使用方法
1. 获取帮助信息
nssh -h
# 或者
nssh --help
这将显示详细的用法和说明。
2. 列出并选择主机进行连接 nssh -l
Available hosts:
1) dev-vm
2) server-key
3) server-pass-prod
Enter number to connect (or q to quit): 3
Selected host: server-pass-prod
Attempting password authentication for 'server-pass-prod' using password from '/home/user/.ssh/my_server_passwords.txt'...
[...连接成功...]
3. 直接连接主机
nssh server-pass-prod
与默认 ssh
用法保持一致,不管是使用密码还是使用密钥连接,都是兼容的。
安全注意事项
- 密码文件安全: 将密码以明文形式存储在文件中存在固有风险。尽管 nssh 要求密码文件权限为 600 (只有所有者可读写),但这并不能防止拥有您用户账户访问权限的人读取密码。
sshpass
:sshpass
工具通过非交互方式提供密码,密码会在执行期间存在于内存中。SSH
密钥认证优先: 强烈建议尽可能使用SSH
密钥对进行认证。密钥认证比密码认证更安全,也更方便(一旦设置好,通常可以免密登录)。nssh
的密码功能主要是为了应对那些由于限制无法使用密钥认证的场景,或者作为一种过渡方案。- 谨慎使用: 请充分理解此工具的工作方式和潜在安全风险后再使用。
特别注意
直接使用 ~/.ssh/confg
文件来作为 nssh
的配置文件会导致原 ssh
不可用,因为 ssh
不认识自定义配置 pwfilepath
导致报错无法继续。如果在使用 nssh
的同时需要继续使用 ssh
,推荐使用一个特定的文件来作为 nssh
的配置文件,比如 ~/.ssh/nssh_config
,同时删除 ~/.ssh/confg
中带有 pwfilepath
配置项的主机。
这样,脚本的开头配置文件路径需要修改为:
SSH_CONFIG_FILE="$HOME/.ssh/nssh_config"
总结
nssh
是一个便捷的 Shell
脚本,旨在通过智能处理密码认证和提供主机选择功能来简化 SSH
连接。简单的配置即可兼容密码连接,且与 SSH
原始用法保持一致。
标题:Linux 下让 .ssh/config 同时支持密钥和密码连接 SSH
原文链接:https://beltxman.com/4535.html
若无特殊说明本站内容为 行星带 原创,未经同意请勿转载。