#!/usr/bin/env bash # # ssh.sh — SSH 公钥/密钥 交互式批量配置脚本 # # 功能: # - 列出本机所有可登录用户(root 及 uid>=1000),交互选择「全部 / 指定」 # - 给所选用户: # 1) 粘贴一把公钥 → 追加授权(写入 authorized_keys,允许用对应私钥登录) # 2) 为用户生成新 ed25519 密钥对 → 同时授权该公钥,并打印私钥供下载 # 3) 可选 sshd 加固(root 仅密钥登录) # - 全程「追加不覆盖」:已有公钥/密钥的用户可再加一把,绝不删旧的 # # 一键运行: curl -fsSL https://claude.dahe2016.com/ssh.sh | bash # (脚本是交互式的,会从 /dev/tty 读输入,管道运行也 OK) # set -euo pipefail # ===== 预设公钥(操作4用):内置一把固定公钥,目标机自带,无需粘贴/扫描 ===== # 公钥公开无害;对应【私钥】存在分发站「公网访问不到」的安全目录(/www/server/claude-keys), # 需要私钥时在分发站执行: sudo claude-key link preset (出一次性下载链接) PRESET_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTlbsXqzqK8oYqnfWxpAySPGvaTwIdfvSBzmJsJ7Quk preset" # ===================================================================== [[ "${EUID}" -ne 0 ]] && { echo "请用 root 运行: sudo bash $0" >&2; exit 1; } [[ -r /dev/tty ]] || { echo "❌ 需要交互终端(/dev/tty 不可用),请在真实终端运行。" >&2; exit 1; } log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; } warn() { printf '\033[1;33m[!] %s\033[0m\n' "$*" >&2; } err() { printf '\033[1;31m❌ %s\033[0m\n' "$*" >&2; } hr() { printf '\033[0;36m%s\033[0m\n' "------------------------------------------------------------"; } ask() { local p="$1" d="${2:-}" a; read -r -p "${p}" a =1000,且非 nologin/false 壳) ---------- declare -a U_NAME U_UID U_HOME U_SHELL collect_users() { U_NAME=(); U_UID=(); U_HOME=(); U_SHELL=() local name uid home shell while IFS=: read -r name _ uid _ _ home shell; do [[ "${uid}" -eq 0 || "${uid}" -ge 1000 ]] || continue [[ "${uid}" -ge 65534 ]] && continue # 跳过 nobody case "${shell}" in */nologin|*/false) continue;; esac [[ -n "${home}" ]] || continue U_NAME+=("${name}"); U_UID+=("${uid}"); U_HOME+=("${home}"); U_SHELL+=("${shell}") done < /etc/passwd } print_users() { hr; printf " %-3s %-16s %-7s %-22s %s\n" "#" "用户" "UID" "家目录" "Shell"; hr local i for i in "${!U_NAME[@]}"; do printf " %-3s %-16s %-7s %-22s %s\n" "$((i+1))" "${U_NAME[$i]}" "${U_UID[$i]}" "${U_HOME[$i]}" "${U_SHELL[$i]}" done hr } # 解析选择 → 把所选用户的下标写入全局 SELECTED 数组。支持 all / 序号 / 用户名 混合 declare -a SELECTED parse_selection() { local raw="$1" tok i found; SELECTED=() if [[ "${raw}" == "all" || "${raw}" == "ALL" || -z "${raw}" ]]; then for i in "${!U_NAME[@]}"; do SELECTED+=("${i}"); done return 0 fi for tok in ${raw//,/ }; do found="" if [[ "${tok}" =~ ^[0-9]+$ ]]; then i=$((tok-1)); [[ -n "${U_NAME[$i]:-}" ]] && { SELECTED+=("${i}"); found=1; } else for i in "${!U_NAME[@]}"; do [[ "${U_NAME[$i]}" == "${tok}" ]] && { SELECTED+=("${i}"); found=1; break; }; done fi [[ -z "${found}" ]] && warn "忽略无效项: ${tok}" done # 去重 local uniq=() x seen for x in "${SELECTED[@]}"; do seen=""; for i in "${uniq[@]:-}"; do [[ "${i}" == "${x}" ]] && seen=1; done [[ -z "${seen}" ]] && uniq+=("${x}") done SELECTED=("${uniq[@]}") [[ ${#SELECTED[@]} -gt 0 ]] } # ---------- 把一把公钥追加授权给某用户(幂等,带权限修正) ---------- authorize_key() { local name="$1" home="$2" pub="$3" local d="${home}/.ssh" f="${home}/.ssh/authorized_keys" grp grp="$(id -gn "${name}" 2>/dev/null || echo "${name}")" mkdir -p "${d}"; chmod 700 "${d}"; touch "${f}" if grep -qF "${pub}" "${f}" 2>/dev/null; then log "[${name}] 该公钥已存在,跳过" else printf '%s\n' "${pub}" >> "${f}"; log "[${name}] 已授权公钥 → ${f}" fi chmod 600 "${f}"; chown -R "${name}:${grp}" "${d}" } # ---------- 为某用户生成新 ed25519 密钥对,返回私钥路径 ---------- gen_keypair() { local name="$1" home="$2" grp d key ts grp="$(id -gn "${name}" 2>/dev/null || echo "${name}")" d="${home}/.ssh"; mkdir -p "${d}"; chmod 700 "${d}" key="${d}/id_ed25519" if [[ -e "${key}" ]]; then ts="$(date +%Y%m%d%H%M%S 2>/dev/null || echo new)" key="${d}/id_ed25519_${ts}" # 已有则另起名,不覆盖旧的 fi ssh-keygen -t ed25519 -N "" -C "${name}@$(hostname 2>/dev/null || echo host)" -f "${key}" >/dev/null chown -R "${name}:${grp}" "${d}" printf '%s' "${key}" } # ============================ 动作 ============================ action_paste_key() { log "粘贴公钥授权(可登录所选用户)" echo "请粘贴一把【公钥】(以 ssh-ed25519 / ssh-rsa / ecdsa- 开头的一整行),回车确认:" >&2 local pub; pub="$(ask '公钥> ')" pub="$(printf '%s' "${pub}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" [[ -z "${pub}" ]] && { warn "未输入,跳过"; return; } case "${pub}" in ssh-ed25519*|ssh-rsa*|ecdsa-*|sk-ssh-*|ssh-dss*) : ;; *) warn "这看起来不像公钥(应以 ssh-ed25519/ssh-rsa/ecdsa- 开头)。仍要继续? " [[ "$(ask '继续? [y/N] ' N)" =~ ^[Yy]$ ]] || return ;; esac local i; for i in "${SELECTED[@]}"; do authorize_key "${U_NAME[$i]}" "${U_HOME[$i]}" "${pub}"; done } action_gen_key() { command -v ssh-keygen >/dev/null 2>&1 || { err "未找到 ssh-keygen,请先装 openssh-client/openssh"; return; } log "为所选用户生成新密钥对(并授权自登录,打印私钥)" local i name home key for i in "${SELECTED[@]}"; do name="${U_NAME[$i]}"; home="${U_HOME[$i]}" key="$(gen_keypair "${name}" "${home}")" authorize_key "${name}" "${home}" "$(cat "${key}.pub")" hr echo -e "\033[1;36m[${name}] 私钥(保存到本地 ~/.ssh/,chmod 600,用它登录该用户):\033[0m" echo " 服务器路径: ${key}"; echo cat "${key}" echo; echo -e "\033[1;36m[${name}] 公钥:\033[0m"; cat "${key}.pub" hr done warn "私钥仅此一次完整显示在屏幕,请立刻复制保存。服务器上也留有一份: 上面的「服务器路径」。" } action_harden() { local cfg="/etc/ssh/sshd_config"; [[ -f "${cfg}" ]] || { warn "无 ${cfg},跳过"; return; } warn "加固后 root 将【不能再用密码登录】,只能用密钥。确认每个要 root 登录的客户端都已有授权公钥!" [[ "$(ask '确认加固 sshd(root 仅密钥)? [y/N] ' N)" =~ ^[Yy]$ ]] || { log "已取消加固"; return; } cp -a "${cfg}" "${cfg}.bak.$(date +%s 2>/dev/null || echo bak)" sed -i -e 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' \ -e 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' "${cfg}" grep -q '^PubkeyAuthentication' "${cfg}" || echo 'PubkeyAuthentication yes' >> "${cfg}" grep -q '^PermitRootLogin' "${cfg}" || echo 'PermitRootLogin prohibit-password' >> "${cfg}" systemctl restart sshd 2>/dev/null || systemctl restart ssh 2>/dev/null || service ssh restart 2>/dev/null || true log "✅ sshd 已加固并重启(备份: ${cfg}.bak.*)" } # 操作4: 用【预设公钥】授权(公钥内置脚本顶部 PRESET_PUBKEY,对应私钥在服务器安全目录) action_preset_key() { if [[ -z "${PRESET_PUBKEY}" ]]; then warn "脚本未预设公钥(PRESET_PUBKEY 为空)。请在 ssh.sh 顶部填好,或改用操作 1 粘贴。"; return fi log "用预设公钥授权所选用户(公钥):" echo " ${PRESET_PUBKEY}" >&2 local i; for i in "${SELECTED[@]}"; do authorize_key "${U_NAME[$i]}" "${U_HOME[$i]}" "${PRESET_PUBKEY}"; done } # ============================ 主流程 ============================ collect_users [[ ${#U_NAME[@]} -gt 0 ]] || { err "未发现可配置用户"; exit 1; } while true; do print_users echo "选择要配置的用户: 输入序号/用户名(空格或逗号分隔),或 all 全部,直接回车=全部" >&2 sel="$(ask '目标用户> ' all)" if ! parse_selection "${sel}"; then warn "没选到有效用户,重来"; continue; fi names=""; for i in "${SELECTED[@]}"; do names+="${U_NAME[$i]} "; done log "已选用户: ${names}" echo "选择操作:" >&2 echo " 1) 粘贴一把【新】公钥并授权(允许用对应私钥登录这些用户)" >&2 echo " 4) 用【预设】公钥授权(已内置固定公钥,私钥在服务器安全目录)" >&2 echo " 2) 为这些用户生成新密钥对(打印私钥供下载,并授权自登录)" >&2 echo " 3) 加固 sshd(root 仅密钥登录)" >&2 echo " q) 退出" >&2 act="$(ask '操作> ')" case "${act}" in 1) action_paste_key ;; 2) action_gen_key ;; 3) action_harden ;; 4) action_preset_key ;; q|Q|"") log "结束。"; break ;; *) warn "无效操作: ${act}" ;; esac [[ "$(ask $'\n还要继续配置吗? [y/N] ' N)" =~ ^[Yy]$ ]] || { log "✅ 全部完成。"; break; } done