lx_auto_key_deploy
SSH 密钥分发工具
一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/lx_auto_key_deploy.sh) -u 用户名 -p 密码 -i 10.10.10.254
bash <(curl -sL ../sh/lx_auto_key_deploy.sh)
效果预览
补充说明
lx_auto_key_deploy 是批量向多台主机自动分发 SSH 公钥并实现免密登录的工具,支持交互式菜单与命令行传参两种运行方式,自动完成网络连通性检测、SSH 密码登录验证、公钥推送、免密登录校验及成功记录持久化,适用于运维批量初始化服务器的场景。
功能特点
- 双模式运行:不带参数进入交互菜单,传参直接执行分发/管理操作
- 命令行动态传参:支持
-u/-p/-i一键批量分发,-l/-d/-c管理清单 - 密钥自动生成:未检测到密钥时自动生成 RSA 4096 密钥对
- 公钥批量分发:自动调用 ssh-copy-id 推送本机公钥到多台主机
- 免密验证:推送后使用 BatchMode 校验是否真正实现免密登录
- 清单持久化:成功主机自动写入清单文件,重复分发自动跳过
- IP 校验:自动过滤非法 IPv4 地址,逐台统计成功/失败/跳过
- 网络诊断:逐台测试 ping 连通性并诊断 SSH 端口可达性
传参说明
./lx_auto_key_deploy.sh [选项]
| 选项 | 长选项 | 说明 |
|---|---|---|
-u |
--user |
指定 SSH 用户名 |
-p |
--pass |
指定 SSH 密码 |
-i |
--ip |
指定目标 IP 列表(空格分隔) |
-l |
--list |
显示已分发主机清单 |
-d |
--delete |
删除指定 IP 记录 |
-c |
--clear |
清空所有记录 |
-h |
--help |
显示帮助信息 |
示例
# 进入交互式菜单
./lx_auto_key_deploy.sh
# 批量分发密钥到多台主机
./lx_auto_key_deploy.sh -u root -p password -i "10.10.10.251 10.10.10.252 10.10.10.253"
# 查看已分发主机清单
./lx_auto_key_deploy.sh --list
# 删除指定主机记录
./lx_auto_key_deploy.sh --delete 10.10.10.254
# 清空所有记录
./lx_auto_key_deploy.sh --clear
交互式使用流程
1. 运行脚本,自动检测并生成 SSH 密钥(若不存在)
2. 首次使用自动安装依赖 sshpass(缺失时交互确认)
3. 进入管理菜单,选择【3 新增密钥分发】
4. 输入远端统一用户名与密码
5. 输入要分发的主机 IP 列表(空格分隔)
6. 脚本逐台完成 网络检测 → 密码登录 → 公钥推送 → 免密验证
7. 分发成功的主机自动记录,下次运行自动跳过
文件说明
| 路径 | 说明 |
|---|---|
/root/.ssh/id_rsa |
本机 SSH 私钥(不存在时自动生成) |
/root/.ssh/id_rsa.pub |
本机 SSH 公钥(用于分发) |
/root/.ssh_dist_list.txt |
已完成分发的主机清单文件 |
注意事项
- 需要 root 权限运行,目标主机使用统一账号密码
- 依赖
sshpass,缺失时脚本会询问是否自动安装 - 命令行传参时密码暴露在 shell 历史中,敏感场景建议使用交互模式
- 公钥推送采用 StrictHostKeyChecking=no,免手动确认指纹
- 已分发成功的主机再次运行会自动跳过,无需重复推送
- 若目标主机禁止密码登录(PasswordAuthentication no),分发将失败
-d仅删除本地清单记录,不会回收目标主机的已授权公钥
脚本源码
#!/bin/bash
gl_hui='\e[37m'
gl_hong='\033[31m'
gl_lv='\033[32m'
gl_huang='\033[33m'
gl_lan='\033[34m'
gl_zi='\033[35m'
gl_bufan='\033[96m'
gl_bai='\033[0m'
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
# 静默模式日志函数
log_info_silent() { [[ "$SILENT_MODE" != "1" ]] && log_info "$*"; }
log_ok_silent() { [[ "$SILENT_MODE" != "1" ]] && log_ok "$*"; }
log_warn_silent() { [[ "$SILENT_MODE" != "1" ]] && log_warn "$*"; }
break_end() {
[[ "$SILENT_MODE" != "1" ]] && {
echo -e "${gl_lv}操作完成${gl_bai}"
echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
read -r -n 1 -s -r -p ""
echo ""
clear
}
}
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
exit_script() {
[[ "$SILENT_MODE" != "1" ]] && {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
"${gl_cheng}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=6
for ((i=0; i<20; i++)); do
if (( i > 0 && i % 3 == 0 && dot_idx < total_dots )); then
dot_buffer+=${dots[$dot_idx]}
((dot_idx++))
fi
echo -ne "\r\033[K${gl_bufan}${frames[i % frame_len]}${gl_bai} 正在退出 ${dot_buffer}"
sleep_fractional 0.06
done
echo -e "\r\033[K${gl_lv}✓${gl_bai} 成功退出\n"
clear
}
exit 0
}
KEY_PATH="/root/.ssh/id_rsa"
LIST_FILE="/root/.ssh_dist_list.txt"
SUCCESS_IP_LIST=()
CURRENT_USER=""
SILENT_MODE=0
# 显示帮助信息
show_help() {
cat << EOF
${gl_huang}SSH密钥分发工具${gl_bai}
${gl_bufan}用法:${gl_bai}
$0 [选项]
${gl_bufan}选项:${gl_bai}
-u, --user USER 指定SSH用户名
-p, --pass PASS 指定SSH密码
-i, --ip IP_LIST 指定目标IP列表(空格分隔)
-l, --list 显示已分发主机清单
-d, --delete IP 删除指定IP记录
-c, --clear 清空所有记录
-s, --silent 静默模式(不显示冗余信息)
-h, --help 显示此帮助信息
${gl_bufan}示例:${gl_bai}
$0 -u root -p password -i "10.10.10.251 10.10.10.252"
$0 --list
$0 --delete 10.10.10.254
$0 --clear
$0 -s -u root -p pass -i "10.10.10.251" # 静默模式
${gl_bufan}交互模式:${gl_bai}
$0 # 不带参数进入交互菜单
EOF
exit 0
}
load_list(){
SUCCESS_IP_LIST=()
if [[ -f "$LIST_FILE" ]]; then
while IFS= read -r line; do
# 去除可能的回车符和空格
line=$(echo "$line" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -n "$line" ]] && SUCCESS_IP_LIST+=("$line")
done < "$LIST_FILE"
fi
}
save_list(){
# 使用普通数组进行去重(兼容所有Bash版本)
local unique_list=()
local -A seen=()
for ip in "${SUCCESS_IP_LIST[@]}"; do
# 检查是否已经存在
if [[ -z "${seen[$ip]:-}" ]]; then
unique_list+=("$ip")
seen[$ip]=1
fi
done
# 写入文件
> "$LIST_FILE"
for ip in "${unique_list[@]}"; do
echo "$ip" >> "$LIST_FILE"
done
SUCCESS_IP_LIST=("${unique_list[@]}")
}
valid_ip() {
local ip="$1"
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
local a b c d
IFS='.' read -r a b c d <<< "$ip"
(( a <= 255 && b <= 255 && c <= 255 && d <= 255 )) && return 0
fi
return 1
}
ip_in_list() {
local target="$1"
for ip in "${SUCCESS_IP_LIST[@]}"; do
[[ "$ip" == "$target" ]] && return 0
done
return 1
}
add_success_ip() {
local target_ip="$1"
load_list
if ! ip_in_list "$target_ip"; then
SUCCESS_IP_LIST+=("$target_ip")
save_list
log_info_silent "已将 ${target_ip} 存入清单文件"
fi
}
del_success_ip() {
local target_ip="$1"
load_list
local new_arr=()
local found=0
for ip in "${SUCCESS_IP_LIST[@]}"; do
if [[ "$ip" != "$target_ip" ]]; then
new_arr+=("$ip")
else
found=1
fi
done
if [[ $found -eq 1 ]]; then
SUCCESS_IP_LIST=("${new_arr[@]}")
save_list
log_ok_silent "已删除记录:${target_ip}"
return 0
else
log_warn_silent "清单中不存在IP:${target_ip}"
return 1
fi
}
print_success_list() {
local username="$1"
load_list
[[ "$SILENT_MODE" != "1" ]] && clear
echo -e "${gl_huang}>>> 已完成密钥分发主机清单${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ ${#SUCCESS_IP_LIST[@]} -eq 0 ]]; then
echo -e "${gl_huang}暂无成功记录${gl_bai}"
else
local idx=1
for ip in "${SUCCESS_IP_LIST[@]}"; do
echo -e "${gl_lv}${idx}. ${ip}${gl_bai}"
if [[ -n "$username" ]]; then
echo -e " ${gl_bufan}- 测试命令:ssh ${username}@${ip}${gl_bai}"
else
echo -e " ${gl_bufan}- 测试命令:ssh root@${ip}${gl_bai}"
fi
((idx++))
done
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
show_management_menu() {
echo ""
echo -e "${gl_zi}>>> SSH密钥分发管理菜单${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1.${gl_bai} 删除主机记录${gl_bai}"
echo -e "${gl_bufan}2.${gl_bai} 清空所有记录${gl_bai}"
echo -e "${gl_bufan}3.${gl_bai} 新增密钥分发${gl_bai}"
echo -e "${gl_hong}0.${gl_bai} 退出脚本${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
diagnose_and_fix_ssh() {
local host="$1"
log_info_silent "诊断 ${host} 的SSH连接问题 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if nc -zv -w3 "$host" 2>&1 | grep -q "refused"; then
log_warn_silent "${host} SSH端口拒绝连接,服务未运行"
return 1
fi
return 0
}
run_batch_task() {
local user="$1"
local pass="$2"
shift 2
local ip_list=("$@")
local success_count=0 fail_count=0 skip_count=0
# 更新当前用户名
CURRENT_USER="$user"
for TARGET_IP in "${ip_list[@]}"; do
# 去除可能的空格和换行
TARGET_IP=$(echo "$TARGET_IP" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if ! valid_ip "$TARGET_IP"; then
log_error "${TARGET_IP} 非法IP地址,跳过"
((fail_count++))
continue
fi
# 检查是否已经分发过
load_list
if ip_in_list "$TARGET_IP"; then
log_info_silent "${TARGET_IP} 已经完成密钥分发,跳过"
((skip_count++))
continue
fi
[[ "$SILENT_MODE" != "1" ]] && echo -e "\n${gl_bufan}————————————————————————${gl_bai}"
[[ "$SILENT_MODE" != "1" ]] && echo -e "${gl_zi}>>> 正在处理主机: ${TARGET_IP}${gl_bai}"
log_info_silent "测试网络连通性 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if ping -c 2 -W 3 "$TARGET_IP" >/dev/null 2>&1; then
log_ok_silent "${TARGET_IP} 网络可达"
else
log_error "${TARGET_IP} 网络不通,跳过"
((fail_count++))
continue
fi
diagnose_and_fix_ssh "$TARGET_IP"
log_info_silent "测试密码SSH连接 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if sshpass -p "$pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${TARGET_IP}" "echo ok" 2>/dev/null; then
log_ok_silent "${TARGET_IP} SSH密码连接成功"
else
log_error "${TARGET_IP} SSH登录失败,跳过"
((fail_count++))
continue
fi
log_info_silent "推送SSH公钥 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if sshpass -p "$pass" ssh-copy-id -o StrictHostKeyChecking=no -i "${KEY_PATH}.pub" "${user}@${TARGET_IP}" >/dev/null 2>&1; then
log_ok_silent "${TARGET_IP} 密钥推送完成"
else
log_error "${TARGET_IP} 公钥推送失败"
((fail_count++))
continue
fi
log_info_silent "验证免密登录 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if ssh -o BatchMode=yes -o ConnectTimeout=5 "${user}@${TARGET_IP}" "echo ok" 2>/dev/null; then
log_ok_silent "${TARGET_IP} 免密登录验证成功"
add_success_ip "$TARGET_IP"
((success_count++))
else
log_warn_silent "${TARGET_IP} 密钥已推送,但免密登录校验失败"
((fail_count++))
fi
[[ "$SILENT_MODE" != "1" ]] && echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
[[ "$SILENT_MODE" != "1" ]] && echo -e "${gl_lv}[测试命令] ssh ${user}@${TARGET_IP}${gl_bai}"
[[ "$SILENT_MODE" != "1" ]] && echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
done
echo -e ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_zi}>>> 本轮任务汇总${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " 总共处理: ${#ip_list[@]} 台主机"
[[ $skip_count -gt 0 ]] && echo -e " 已跳过(已分发): ${gl_huang}${skip_count}${gl_bai} 台"
echo -e " 成功: ${gl_lv}${success_count}${gl_bai} 台"
echo -e " 失败: ${gl_hong}${fail_count}${gl_bai} 台"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
print_success_list "$CURRENT_USER"
}
delete_ip_interactive() {
load_list
if [[ ${#SUCCESS_IP_LIST[@]} -eq 0 ]]; then
log_warn "清单为空,无需删除"
return
fi
print_success_list "$CURRENT_USER"
echo -e "${gl_huang}请输入要删除的IP地址(输入多个IP用空格分隔):${gl_bai}"
read -p $'\033[33m> \033[0m' del_input
if [[ -z "$del_input" ]]; then
log_info "取消删除操作"
return
fi
local del_ips=($del_input)
local deleted=0
local not_found=0
local invalid=0
for ip in "${del_ips[@]}"; do
if valid_ip "$ip"; then
load_list
if ip_in_list "$ip"; then
del_success_ip "$ip"
((deleted++))
else
log_warn "清单中不存在IP:${ip}"
((not_found++))
fi
else
log_error "IP格式错误:${ip}"
((invalid++))
fi
done
echo -e "\n${gl_bufan}删除结果:${gl_bai}"
[[ $deleted -gt 0 ]] && echo -e " 成功删除: ${gl_lv}${deleted}${gl_bai} 个"
[[ $not_found -gt 0 ]] && echo -e " 未找到: ${gl_huang}${not_found}${gl_bai} 个"
[[ $invalid -gt 0 ]] && echo -e " IP格式错误: ${gl_hong}${invalid}${gl_bai} 个"
print_success_list "$CURRENT_USER"
}
interactive_mode() {
load_list
while true; do
if [[ ${#SUCCESS_IP_LIST[@]} -gt 0 ]]; then
print_success_list "$CURRENT_USER"
echo -e "${gl_huang}检测到 ${#SUCCESS_IP_LIST[@]} 台主机已完成密钥分发${gl_bai}"
else
echo -e "${gl_huang}当前没有任何已分发的记录${gl_bai}"
fi
show_management_menu
read -p $'\033[33m请输入你的选择: \033[0m' choice
case $choice in
0)
exit_script
;;
1)
delete_ip_interactive
;;
2)
echo -e "${gl_hong}⚠️ 确认清空所有记录吗?[y/N]${gl_bai}"
read -p $'\033[33m> \033[0m' confirm
if [[ "${confirm,,}" == "y" ]]; then
SUCCESS_IP_LIST=()
rm -f "$LIST_FILE"
log_ok "所有记录已清空"
print_success_list "$CURRENT_USER"
else
log_info "取消清空操作"
fi
;;
3)
echo -e ""
echo -e "${gl_zi}>>> 开始新增密钥分发${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}提示:已分发的IP将被自动跳过${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入远端SSH用户名: ")" CRED_USER
[[ -z "$CRED_USER" ]] && log_error "用户名不能为空!" && continue
read -s -e -p "$(echo -e "${gl_bai}请输入远端SSH密码: ")" CRED_PASS
echo ""
[[ -z "$CRED_PASS" ]] && log_error "密码不能为空!" && continue
log_info "凭证录入完成,登录用户:${CRED_USER}"
# 更新当前用户名
CURRENT_USER="$CRED_USER"
echo -e "${gl_huang}请输入要分发的主机IP列表(用空格分隔):${gl_bai}"
echo -e "${gl_huang}示例:10.10.10.251 10.10.10.252 10.10.10.253${gl_bai}"
read -p $'\033[33m> \033[0m' ip_input
if [[ -z "$ip_input" ]]; then
log_warn "未输入任何IP,返回主菜单"
continue
fi
targets=($ip_input)
run_batch_task "${CRED_USER}" "${CRED_PASS}" "${targets[@]}"
break_end
;;
*)
log_error "无效选项,请选择 0-4"
;;
esac
done
}
parse_args() {
local user=""
local pass=""
local ip_list=""
local show_list=0
local delete_ip=""
local clear_list=0
if [[ $# -eq 0 ]]; then
return 0
fi
while [[ $# -gt 0 ]]; do
case $1 in
-u|--user)
user="$2"
shift 2
;;
-p|--pass)
pass="$2"
shift 2
;;
-i|--ip)
ip_list="$2"
shift 2
;;
-l|--list)
show_list=1
shift
;;
-d|--delete)
delete_ip="$2"
shift 2
;;
-c|--clear)
clear_list=1
shift
;;
-s|--silent)
SILENT_MODE=1
shift
;;
-h|--help)
show_help
;;
*)
log_error "未知参数: $1"
show_help
;;
esac
done
if [[ $show_list -eq 1 ]]; then
load_list
print_success_list "$CURRENT_USER"
exit 0
fi
if [[ $clear_list -eq 1 ]]; then
if [[ "$SILENT_MODE" != "1" ]]; then
echo -e "${gl_hong}⚠️ 确认清空所有记录吗?[y/N]${gl_bai}"
read -p $'\033[33m> \033[0m' confirm
if [[ "${confirm,,}" != "y" ]]; then
log_info "取消清空操作"
exit 0
fi
fi
SUCCESS_IP_LIST=()
rm -f "$LIST_FILE"
log_ok "所有记录已清空"
print_success_list "$CURRENT_USER"
exit 0
fi
if [[ -n "$delete_ip" ]]; then
load_list
del_success_ip "$delete_ip"
print_success_list "$CURRENT_USER"
exit 0
fi
if [[ -n "$user" && -n "$pass" && -n "$ip_list" ]]; then
CURRENT_USER="$user"
# 将IP字符串转换为数组
IFS=' ' read -r -a ip_array <<< "$ip_list"
run_batch_task "$user" "$pass" "${ip_array[@]}"
exit 0
fi
if [[ -n "$user" || -n "$pass" || -n "$ip_list" ]]; then
log_error "参数不完整,需要同时提供 -u, -p 和 -i 参数"
show_help
fi
return 0
}
main() {
load_list
if [[ "$SILENT_MODE" != "1" ]]; then
if [ ! -f "$KEY_PATH" ]; then
log_info "未检测到密钥,自动生成SSH密钥 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t rsa -b 4096 -f "$KEY_PATH" -N "" -q
log_ok "密钥生成完成"
else
log_info "使用现有密钥: ${KEY_PATH}"
fi
else
if [ ! -f "$KEY_PATH" ]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t rsa -b 4096 -f "$KEY_PATH" -N "" -q >/dev/null 2>&1
fi
fi
if ! command -v sshpass &>/dev/null; then
if [[ "$SILENT_MODE" == "1" ]]; then
apt update && apt install -y sshpass >/dev/null 2>&1
else
log_warn "缺少依赖 sshpass"
read -p "是否自动安装? [Y/n] " ans
ans=${ans:-Y}
if [[ "${ans,,}" == "y" ]]; then
apt update && apt install -y sshpass
else
log_error "缺少sshpass,程序终止"
exit 1
fi
fi
fi
parse_args "$@"
if [[ $# -eq 0 ]]; then
interactive_mode
fi
}
main "$@"
创建本地脚本
new_script="lx_auto_key_deploy.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"