From cc74124fc58d3c5c01b67cebdae8aadfd7f32db3 Mon Sep 17 00:00:00 2001 From: BeacherZ <60752063+BeacherZ@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:10:15 +0800 Subject: [PATCH] Delete key.sh --- key.sh | 1558 -------------------------------------------------------- 1 file changed, 1558 deletions(-) delete mode 100644 key.sh diff --git a/key.sh b/key.sh deleted file mode 100644 index f0be350..0000000 --- a/key.sh +++ /dev/null @@ -1,1558 +0,0 @@ -#!/usr/bin/env bash -#============================================================= -# SSH Security Installer (key.sh) -# Multi-distro: Debian/Ubuntu, RHEL/CentOS/Fedora/Rocky/Alma, -# Alpine, Arch/Manjaro, openSUSE -#============================================================= - - -RED="\033[31m"; GREEN="\033[32m"; YELLOW="\033[33m" -BLUE="\033[34m"; PURPLE="\033[35m"; CYAN="\033[36m" -GRAY="\033[90m"; BOLD="\033[1m"; RESET="\033[0m" - -INFO="${GREEN}[INFO]${RESET}"; WARN="${YELLOW}[WARN]${RESET}"; ERROR="${RED}[ERROR]${RESET}" - -JAIL_CONF="/etc/fail2ban/jail.local" -LOG_FILE="/var/log/fail2ban.log" -TARGET_JAIL="sshd" - -SSHD_MAIN="/etc/ssh/sshd_config" -SSHD_CONF_DIR="/etc/ssh/sshd_config.d" -SSHD_CUSTOM="${SSHD_CONF_DIR}/00-key.sh.conf" -SSHD_LEGACY="${SSHD_CONF_DIR}/99-key.sh.conf" - -[ "$EUID" -ne 0 ] && SUDO="sudo" || SUDO="" - -PKG_MGR=""; INIT_SYS=""; SSH_LOG=""; OS_NAME=""; OS_ID=""; OS_VER=""; OS_SHORT="" - -# ============ 命令行帮助 ============ -case "${1:-}" in - -h|--help) - cat < 从 GitHub 拉取公钥 - -u 从自定义 URL 拉取公钥 - -f <文件> 从本地文件导入公钥 - -p <端口> 修改 SSH 端口 - -d 禁用密码登录 - -o 覆盖模式(清空已存公钥) - -其他: - -h, --help 显示帮助 -HELP - exit 0 ;; -esac - -# ============ 能力检测 ============ -detect_os() { - if [ -f /etc/os-release ]; then - . /etc/os-release - OS_NAME="${NAME:-Unknown}"; OS_ID="${ID:-unknown}"; OS_VER="${VERSION_ID:-}" - else - OS_NAME=$(uname -s); OS_VER=$(uname -r); OS_ID="unknown" - fi - case "$OS_ID" in - debian) OS_SHORT="Debian" ;; - ubuntu) OS_SHORT="Ubuntu" ;; - linuxmint) OS_SHORT="Linux Mint" ;; - kali) OS_SHORT="Kali" ;; - rhel) OS_SHORT="RHEL" ;; - centos) OS_SHORT="CentOS" ;; - fedora) OS_SHORT="Fedora" ;; - rocky) OS_SHORT="Rocky" ;; - almalinux) OS_SHORT="AlmaLinux" ;; - ol) OS_SHORT="Oracle Linux" ;; - amzn) OS_SHORT="Amazon Linux" ;; - alpine) OS_SHORT="Alpine" ;; - arch) OS_SHORT="Arch" ;; - manjaro) OS_SHORT="Manjaro" ;; - endeavouros) OS_SHORT="EndeavourOS" ;; - opensuse*) OS_SHORT="openSUSE" ;; - sles) OS_SHORT="SLES" ;; - *) - local first="${OS_ID:0:1}" - OS_SHORT="$(echo "$first" | tr '[:lower:]' '[:upper:]')${OS_ID:1}" - ;; - esac -} - -detect_pkg_mgr() { - for pm in apt dnf yum zypper pacman apk; do - command -v "$pm" &>/dev/null && { PKG_MGR="$pm"; return; } - done - PKG_MGR="unknown" -} - -detect_init() { - if [ -d /run/systemd/system ]; then INIT_SYS="systemd" - elif [ -d /run/openrc ]; then INIT_SYS="openrc" - elif [ -x /sbin/init ]; then INIT_SYS="sysvinit" - else INIT_SYS="unknown"; fi -} - -detect_ssh_log() { - case "$OS_ID" in - debian|ubuntu|linuxmint|kali) SSH_LOG="/var/log/auth.log" ;; - rhel|centos|fedora|rocky|almalinux|ol|amzn) SSH_LOG="/var/log/secure" ;; - alpine) SSH_LOG="/var/log/messages" ;; - arch|manjaro|endeavouros) SSH_LOG="/var/log/auth.log" ;; - opensuse*|sles) SSH_LOG="/var/log/messages" ;; - *) SSH_LOG="/var/log/auth.log" ;; - esac -} - -# ============ 服务管理抽象 ============ -svc_start() { case "$INIT_SYS" in systemd) $SUDO systemctl start "$1" 2>/dev/null;; openrc) $SUDO rc-service "$1" start 2>/dev/null;; sysvinit) $SUDO service "$1" start 2>/dev/null;; esac; } -svc_stop() { case "$INIT_SYS" in systemd) $SUDO systemctl stop "$1" 2>/dev/null;; openrc) $SUDO rc-service "$1" stop 2>/dev/null;; sysvinit) $SUDO service "$1" stop 2>/dev/null;; esac; } -svc_restart(){ case "$INIT_SYS" in systemd) $SUDO systemctl restart "$1" 2>/dev/null;; openrc) $SUDO rc-service "$1" restart 2>/dev/null;; sysvinit) $SUDO service "$1" restart 2>/dev/null;; esac; } -svc_enable() { case "$INIT_SYS" in systemd) $SUDO systemctl enable "$1" 2>/dev/null;; openrc) $SUDO rc-update add "$1" default 2>/dev/null;; sysvinit) $SUDO update-rc.d "$1" defaults 2>/dev/null || chkconfig "$1" on 2>/dev/null;; esac; } -svc_disable(){ case "$INIT_SYS" in systemd) $SUDO systemctl disable "$1" 2>/dev/null;; openrc) $SUDO rc-update del "$1" default 2>/dev/null;; sysvinit) $SUDO update-rc.d -f "$1" remove 2>/dev/null || chkconfig "$1" off 2>/dev/null;; esac; } - -# ============ 包管理抽象 ============ -pkg_install() { - case "$PKG_MGR" in - apt) $SUDO apt-get update -qq && $SUDO apt-get install -y "$@" ;; - dnf) $SUDO dnf install -y "$@" ;; - yum) $SUDO yum install -y "$@" ;; - zypper) $SUDO zypper --non-interactive install "$@" ;; - pacman) $SUDO pacman -Sy --noconfirm "$@" ;; - apk) $SUDO apk add --no-cache "$@" ;; - *) echo -e "${ERROR} 未知包管理器"; return 1 ;; - esac -} -pkg_remove() { - case "$PKG_MGR" in - apt) $SUDO apt-get remove --purge -y "$@"; $SUDO apt-get autoremove -y ;; - dnf) $SUDO dnf remove -y "$@" ;; - yum) $SUDO yum remove -y "$@" ;; - zypper) $SUDO zypper --non-interactive remove "$@" ;; - pacman) $SUDO pacman -R --noconfirm "$@" ;; - apk) $SUDO apk del "$@" ;; - *) return 1 ;; - esac -} -# 重新安装包,用于修复残缺安装(如 /etc/fail2ban 被误删) -pkg_reinstall() { - case "$PKG_MGR" in - apt) $SUDO apt-get install --reinstall -y "$@" ;; - dnf) $SUDO dnf reinstall -y "$@" ;; - yum) $SUDO yum reinstall -y "$@" ;; - zypper) $SUDO zypper --non-interactive install --force-resolution "$@" ;; - pacman) $SUDO pacman -S --noconfirm "$@" ;; - apk) $SUDO apk add --no-cache "$@" ;; - *) return 1 ;; - esac -} - -# 检测 EOL 系统(仅用于提示,不自动修改配置) -check_eol_system() { - case "$OS_ID" in - debian) - case "$OS_VER" in - 8|9|10|11) - echo -e "\n${YELLOW}${BOLD}[提示] 检测到 Debian ${OS_VER},该系统已停止官方支持。${RESET}" - echo -e "${YELLOW}apt 源可能已失效,导致无法正常安装软件。${RESET}" - echo -e "${YELLOW}如果安装失败,可将 /etc/apt/sources.list 改为 archive 源:${RESET}\n" - local codename="" - case "$OS_VER" in - 8) codename="jessie" ;; - 9) codename="stretch" ;; - 10) codename="buster" ;; - 11) codename="bullseye" ;; - esac - if [ -n "$codename" ]; then - echo -e "${CYAN}示例:${RESET}" - echo -e " deb http://archive.debian.org/debian ${codename} main contrib non-free" - echo -e " deb http://archive.debian.org/debian ${codename}-updates main contrib non-free" - echo -e "\n${CYAN}修改后执行:sudo apt update${RESET}" - fi - echo "" - ;; - esac - ;; - ubuntu) - case "$OS_VER" in - 14.04|16.04|18.04|20.04|21.04|21.10|22.10) - echo -e "\n${YELLOW}${BOLD}[提示] 检测到 Ubuntu ${OS_VER},该系统已停止官方支持。${RESET}" - echo -e "${YELLOW}apt 源可能已失效,建议升级到 LTS 版本。${RESET}\n" - ;; - esac - ;; - esac -} - -# ============ SSH 配置管理 ============ -# 读取顺序:优先读 00-key.sh.conf(我们写入的地方)→ 主文件 → 其他 .d 文件 -# 与 OpenSSH "First Match Wins" 生效顺序一致 -get_sshd_config_val() { - local key="$1" default_val="$2" - local val="" - - # 1. 优先读取我们写入的 00-key.sh.conf - if [ -f "$SSHD_CUSTOM" ]; then - val=$(grep -iE "^[[:space:]]*${key}[[:space:]=]+" "$SSHD_CUSTOM" | tail -n 1 | \ - sed -E 's/^[[:space:]]*[^[:space:]=]+[[:space:]=]+//' | awk '{print $1}') - fi - - # 2. 主文件 - if [ -z "$val" ] && [ -f "$SSHD_MAIN" ]; then - val=$(grep -iE "^[[:space:]]*${key}[[:space:]=]+" "$SSHD_MAIN" | tail -n 1 | \ - sed -E 's/^[[:space:]]*[^[:space:]=]+[[:space:]=]+//' | awk '{print $1}') - fi - - # 3. 其他 .d 文件 - if [ -z "$val" ] && [ -d "$SSHD_CONF_DIR" ]; then - for conf in "${SSHD_CONF_DIR}"/*.conf; do - [ -f "$conf" ] || continue - [ "$conf" = "$SSHD_CUSTOM" ] && continue - val=$(grep -iE "^[[:space:]]*${key}[[:space:]=]+" "$conf" | tail -n 1 | \ - sed -E 's/^[[:space:]]*[^[:space:]=]+[[:space:]=]+//' | awk '{print $1}') - [ -n "$val" ] && break - done - fi - - [ -n "$val" ] && { echo "$val"; return; } - echo "$default_val" -} - -# 写入 SSH 配置 -# 优先写入 00-key.sh.conf(字典序最靠前 = 最高优先级 + 避开 Match 块陷阱) -# 无 sshd_config.d 目录时才写主文件 -set_sshd_config() { - local param="$1" value="$2" - - if [ -d "$SSHD_CONF_DIR" ]; then - # 0. 清理旧版遗留的 99-key.sh.conf(防重复) - if [ -f "$SSHD_LEGACY" ]; then - $SUDO rm -f "$SSHD_LEGACY" - fi - # 1. 注释主文件中的同参数项(防御 Include 在文件末尾的边界情况) - if [ -f "$SSHD_MAIN" ]; then - $SUDO sed -i -E "s/^[[:space:]]*#?[[:space:]]*(${param})[[:space:]=].*/# \1 (disabled by key.sh)/" "$SSHD_MAIN" - fi - # 2. 注释其他 .conf 中的同参数项,避免 First Match Wins 冲突 - for conf in "${SSHD_CONF_DIR}"/*.conf; do - [ -f "$conf" ] || continue - [ "$conf" = "$SSHD_CUSTOM" ] && continue - $SUDO sed -i -E "s/^[[:space:]]*#?[[:space:]]*(${param})[[:space:]=].*/# \1 (disabled by key.sh)/" "$conf" - done - # 3. 确保自定义文件存在 - [ ! -f "$SSHD_CUSTOM" ] && echo "${param} ${value}" | $SUDO tee "$SSHD_CUSTOM" > /dev/null - # 4. 删除自定义文件中的旧值,写入新值 - $SUDO sed -i -E "/^[[:space:]]*#?[[:space:]]*(${param})[[:space:]=]/d" "$SSHD_CUSTOM" - echo "${param} ${value}" | $SUDO tee -a "$SSHD_CUSTOM" > /dev/null - else - # 传统主文件模式(无 sshd_config.d 目录的老系统) - $SUDO sed -i -E "/^[[:space:]]*#?[[:space:]]*(${param})[[:space:]=]/d" "$SSHD_MAIN" - if grep -q -iE "^[[:space:]]*Match[[:space:]]+" "$SSHD_MAIN"; then - # 存在 Match 块,插入到第一个 Match 之前(大小写不敏感) - local tmpf; tmpf=$(mktemp) - awk -v p="${param}" -v v="${value}" ' - BEGIN { inserted=0 } - tolower($0) ~ /^[[:space:]]*match[[:space:]]/ && !inserted { - print p " " v - inserted=1 - } - { print } - ' "$SSHD_MAIN" > "$tmpf" - $SUDO cp "$tmpf" "$SSHD_MAIN" - rm -f "$tmpf" - else - echo "${param} ${value}" | $SUDO tee -a "$SSHD_MAIN" > /dev/null - fi - fi -} - -restart_sshd() { - echo -e "${INFO} 正在检测 SSH 配置文件语法..." - if command -v sshd &>/dev/null; then - if ! $SUDO sshd -t; then - echo -e "${ERROR} SSH 配置文件测试失败!检测到语法错误,已取消重启以防止断连锁死!" - return 1 - fi - fi - - echo -e "${INFO} 正在重启 SSH 服务..." - local restart_ok=1 - if [ "$INIT_SYS" = "systemd" ]; then - if systemctl list-unit-files 2>/dev/null | grep -q "ssh.socket"; then - echo -e "${INFO} 检测到 systemd ssh.socket,正在强制关闭并屏蔽 (mask)..." - $SUDO systemctl stop ssh.socket &>/dev/null - $SUDO systemctl disable ssh.socket &>/dev/null - $SUDO systemctl mask ssh.socket &>/dev/null - fi - $SUDO systemctl daemon-reload &>/dev/null - local ssh_unit="" - if systemctl list-unit-files 2>/dev/null | grep -q "^ssh.service"; then - ssh_unit="ssh" - elif systemctl list-unit-files 2>/dev/null | grep -q "^sshd.service"; then - ssh_unit="sshd" - fi - if [ -n "$ssh_unit" ]; then - # 防御性 unmask:避免上层曾 mask 过 ssh.service 导致 enable 静默失败 - $SUDO systemctl unmask "${ssh_unit}.service" &>/dev/null || true - svc_enable "$ssh_unit" - svc_restart "$ssh_unit" && restart_ok=0 - else - echo -e "${ERROR} 未找到 ssh.service 或 sshd.service!" - fi - elif [ "$INIT_SYS" = "openrc" ]; then - svc_restart sshd && restart_ok=0 - else - svc_restart sshd 2>/dev/null && restart_ok=0 - if [ "$restart_ok" -ne 0 ]; then - svc_restart ssh 2>/dev/null && restart_ok=0 - fi - fi - - if [ "$restart_ok" -eq 0 ]; then - echo -e "${INFO} ${GREEN}SSH 服务重启成功!${RESET}" - return 0 - else - echo -e "${ERROR} SSH 服务重启失败,请检查配置文件!" - return 1 - fi -} - -# ============ 依赖安装 ============ -check_dependencies() { - local pkgs=() - for dep in curl ssh-keygen awk; do - command -v "$dep" &>/dev/null && continue - case "$dep" in - curl) pkgs+=("curl") ;; - ssh-keygen) case "$PKG_MGR" in apt) pkgs+=("openssh-client");; dnf|yum) pkgs+=("openssh-clients");; apk) pkgs+=("openssh-client");; pacman) pkgs+=("openssh");; zypper) pkgs+=("openssh");; esac ;; - awk) command -v gawk &>/dev/null && continue; pkgs+=("gawk") ;; - esac - done - [ ${#pkgs[@]} -gt 0 ] && { echo -e "${WARN} 正在安装依赖: ${pkgs[*]}"; pkg_install "${pkgs[@]}"; } -} - -init_ssh_dir() { - mkdir -p "${HOME}/.ssh"; chmod 700 "${HOME}/.ssh" - touch "${HOME}/.ssh/authorized_keys"; chmod 600 "${HOME}/.ssh/authorized_keys" -} - -# 追加公钥 -# - 保留选项前缀(如 from="1.2.3.4",command="...") -# - 丢弃尾部注释(避免污染元数据标签) -# - 用 base64 主体做去重 -append_key_with_meta() { - local pub_content="$1" source_tag="$2" auth_file="${HOME}/.ssh/authorized_keys" - local ts; ts=$(date "+%Y-%m-%d %H:%M:%S") - init_ssh_dir - while IFS= read -r line; do - [ -z "$line" ] && continue - line=$(echo "$line" | sed -e 's/[[:space:]]*$//') - - # 从 ssh-/ecdsa-/sk- 开始,保留「密钥类型 + Base64 主体」两列,丢弃尾部注释 - # 保留此前的 option 前缀(如果有),以便 from=/command= 等限制不丢失 - local core_key - core_key=$(echo "$line" | awk '{ - for(i=1;i<=NF;i++){ - if($i ~ /^(ssh-|ecdsa-|sk-)/){ - for(j=1;j<=i+1 && j<=NF;j++) printf "%s%s", $j, (j/dev/null; then - echo -e "${WARN} 该公钥已经存在于 authorized_keys 中,已跳过重复追加。" - continue - fi - - echo "${core_key} [${ts}|${source_tag}]" >> "$auth_file" - echo -e "${INFO} ${GREEN}已成功追加公钥 (${source_tag})${RESET}" - done <<< "$pub_content" - chmod 600 "$auth_file" -} - -# 统计 authorized_keys 中有效公钥数量 -count_authorized_keys() { - local auth_file="${HOME}/.ssh/authorized_keys" - if [ -f "$auth_file" ]; then - awk '!/^[[:space:]]*(#|$)/{c++} END{print c+0}' "$auth_file" - else - echo "0" - fi -} - -# 判断 authorized_keys 是否有有效公钥(支持带 option 前缀 + sk- 硬件密钥) -has_valid_pubkey() { - local auth_file="${HOME}/.ssh/authorized_keys" - [ -f "$auth_file" ] || return 1 - grep -qE '(^|[[:space:]])(ssh-|ecdsa-|sk-)' "$auth_file" 2>/dev/null -} - -# ============ Fail2Ban ============ -# 参数读取:优先读 [sshd] 段 → 读不到再读 [DEFAULT] 段 -# 递增参数(bantime.increment/factor/maxtime)只应在 [DEFAULT] 段生效 -get_f2b_conf() { - local key=$1 - [ -f "$JAIL_CONF" ] || return - # 1. 先读 [sshd] 段 - local result - result=$(awk -v t="$TARGET_JAIL" -v k="$key" ' - BEGIN { in_block=0; result="" } - $0 ~ "^\\[" t "\\][[:space:]]*$" { in_block=1; next } - /^[[:space:]]*\[/ { in_block=0 } - in_block && $0 ~ "^[[:space:]]*" k "[[:space:]]*=" { - value=$0 - sub(/^[[:space:]]*[^=]+=[[:space:]]*/, "", value) - sub(/[[:space:]]+$/, "", value) - result = value - } - END { if (result != "") print result } - ' "$JAIL_CONF") - # 2. [sshd] 段没有 → 从 [DEFAULT] 段取 - if [ -z "$result" ]; then - result=$(awk -v k="$key" ' - BEGIN { in_block=0; result="" } - /^\[DEFAULT\][[:space:]]*$/ { in_block=1; next } - /^[[:space:]]*\[/ { in_block=0 } - in_block && $0 ~ "^[[:space:]]*" k "[[:space:]]*=" { - value=$0 - sub(/^[[:space:]]*[^=]+=[[:space:]]*/, "", value) - sub(/[[:space:]]+$/, "", value) - result = value - } - END { if (result != "") print result } - ' "$JAIL_CONF") - fi - echo "$result" -} - -# 参数写入: -# - bantime.increment / bantime.factor / bantime.maxtime 属于 [DEFAULT] 段 -# - 其余参数写入 [sshd] 段 -set_f2b_conf() { - local key=$1 val=$2 - local target_section="$TARGET_JAIL" - case "$key" in - bantime.increment|bantime.factor|bantime.maxtime) - target_section="DEFAULT" - ;; - esac - - if [ ! -f "$JAIL_CONF" ]; then - { - echo "[DEFAULT]" - echo "[$TARGET_JAIL]" - } | $SUDO tee "$JAIL_CONF" > /dev/null - fi - # 确保目标段存在 - if ! grep -q "^\[${target_section}\]" "$JAIL_CONF"; then - echo "" | $SUDO tee -a "$JAIL_CONF" > /dev/null - echo "[${target_section}]" | $SUDO tee -a "$JAIL_CONF" > /dev/null - fi - - # 判断该 key 是否已存在于目标段 - local exists - exists=$(awk -v t="$target_section" -v k="$key" ' - BEGIN { in_block=0; found=0 } - $0 ~ "^\\[" t "\\][[:space:]]*$" { in_block=1; next } - /^[[:space:]]*\[/ { in_block=0 } - in_block && $0 ~ "^[[:space:]]*" k "[[:space:]]*=" { found=1 } - END { print found } - ' "$JAIL_CONF") - - if [ "$exists" = "1" ]; then - # 精确替换目标段内该 key 的值 - local tmpf; tmpf=$(mktemp) - awk -v t="$target_section" -v k="$key" -v v="$val" ' - BEGIN { in_block=0; done=0 } - $0 ~ "^\\[" t "\\][[:space:]]*$" { in_block=1; print; next } - /^[[:space:]]*\[/ { in_block=0 } - in_block && !done && $0 ~ "^[[:space:]]*" k "[[:space:]]*=" { - print k " = " v - done=1 - next - } - { print } - ' "$JAIL_CONF" > "$tmpf" - $SUDO cp "$tmpf" "$JAIL_CONF" - rm -f "$tmpf" - else - # 追加到目标段头下方 - $SUDO sed -i "/^\[${target_section}\]/a ${key} = ${val}" "$JAIL_CONF" - fi -} - -restart_f2b() { - echo -e "${INFO} 正在重载 Fail2Ban 配置..." - svc_restart fail2ban - for i in {1..5}; do - if fail2ban-client ping >/dev/null 2>&1; then - echo -e "${INFO} ${GREEN}成功!配置已生效。${RESET}"; return 0 - fi; sleep 1 - done - echo -e "${ERROR} Fail2Ban 重启超时或失败。" - echo -e "${YELLOW}请手动运行 'journalctl -u fail2ban -n 50' 排查错误。${RESET}" -} - -get_fail2ban_status() { - if command -v fail2ban-client >/dev/null 2>&1 && fail2ban-client ping >/dev/null 2>&1; then - local count - count=$(fail2ban-client status "$TARGET_JAIL" 2>/dev/null | grep -i "Currently banned" | awk '{print $NF}') - echo -e "${GREEN}防护中 (已封禁${count:-0} IP)${RESET}" - elif command -v fail2ban-client >/dev/null 2>&1; then - echo -e "${YELLOW}已安装 / 已停止${RESET}" - else - echo -e "${YELLOW}未安装${RESET}" - fi -} - -fmt_f2b_unit() { - local val=$1 type=$2 - if [[ "$val" =~ ^[0-9]+$ ]]; then - [ "$type" == "time" ] && echo "${val}秒" || { [ "$type" == "factor" ] && echo "${val}倍" || echo "$val"; } - else echo "$val"; fi -} - -validate_time() { [[ "$1" =~ ^[0-9]+[smhdw]?$ ]]; } -validate_int() { [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; } - -# systemd 环境默认走 journal,不写死 logpath(避免 WARN) -# port 动态读取当前 SSH 端口,避免装 F2B 前已改端口时防护失效 -# 递增参数写入 [DEFAULT] 段(Fail2Ban 要求) -generate_default_jail_conf() { - local backend="auto" - local logpath_line="logpath = ${SSH_LOG}" - if [ "$INIT_SYS" = "systemd" ]; then - backend="systemd" - logpath_line="" - fi - local ssh_filter="sshd" - [ "$OS_ID" = "alpine" ] && ssh_filter="alpine-sshd" - local banaction="iptables-multiport" - if ! command -v iptables &>/dev/null && command -v nft &>/dev/null; then - banaction="nftables-multiport" - fi - local current_port - current_port=$(get_sshd_config_val "Port" "22") - [ -z "$current_port" ] && current_port="22" - cat </dev/null - if ! command -v fail2ban-client >/dev/null 2>&1; then - echo -e "${WARN} 未检测到 Fail2Ban 服务。" - read -rp "是否立即安装 Fail2Ban?(y/N): " install_confirm - [[ ! "$install_confirm" =~ ^[Yy]$ ]] && { echo -e "${WARN} 已取消安装。"; return 1; } - - echo -e "${INFO} 正在安装 Fail2Ban 及相关依赖..." - local f2b_pkgs=() - case "$PKG_MGR" in - apt) f2b_pkgs=("fail2ban" "python3-systemd" "rsyslog");; - dnf|yum) f2b_pkgs=("fail2ban" "rsyslog");; - zypper) f2b_pkgs=("fail2ban" "rsyslog");; - pacman) f2b_pkgs=("fail2ban");; - apk) f2b_pkgs=("fail2ban");; - esac - - pkg_install "${f2b_pkgs[@]}" - hash -r 2>/dev/null - - if ! command -v fail2ban-client >/dev/null 2>&1; then - echo -e "\n${ERROR} Fail2Ban 安装失败!${RESET}" - check_eol_system - echo -e "${YELLOW}请先解决软件源问题,然后重新运行本脚本。${RESET}" - read -rp "按回车键返回..." - return 1 - fi - - if [ ! -d "/etc/fail2ban" ]; then - echo -e "\n${ERROR} /etc/fail2ban 目录不存在,安装可能不完整。${RESET}" - echo -e "${YELLOW}请检查 Fail2Ban 是否安装成功:dpkg -l | grep fail2ban${RESET}" - read -rp "按回车键返回..." - return 1 - fi - - [ ! -f "$SSH_LOG" ] && { $SUDO touch "$SSH_LOG"; svc_enable rsyslog 2>/dev/null; svc_start rsyslog 2>/dev/null; } - [ ! -f "$JAIL_CONF" ] && generate_default_jail_conf | $SUDO tee "$JAIL_CONF" > /dev/null - [ "$INIT_SYS" = "systemd" ] && $SUDO systemctl unmask fail2ban &>/dev/null || true - svc_enable fail2ban; svc_start fail2ban - echo -e "${INFO} ${GREEN}Fail2Ban 安装并启动完成!${RESET}"; sleep 1; return 0 - fi - - # 已安装 fail2ban-client,但 /etc/fail2ban 目录缺失 = 残缺安装 - if [ ! -d "/etc/fail2ban" ]; then - echo -e "\n${ERROR} Fail2Ban 处于残缺状态:命令存在但 /etc/fail2ban 目录缺失。${RESET}" - echo -e "${YELLOW}可能是之前的卸载操作没有清理干净。${RESET}\n" - echo -e " ${GREEN}1.${RESET} 尝试修复(重新安装 Fail2Ban 以重建配置目录)" - echo -e " ${GREEN}2.${RESET} 强制卸载 Fail2Ban 残留" - echo -e " ${GREEN}0.${RESET} 返回" - read -rp "请选择 [0-2]: " f2b_fix_opt - case "$f2b_fix_opt" in - 1) - echo -e "${INFO} 正在重新安装 Fail2Ban..." - local f2b_pkgs=() - case "$PKG_MGR" in - apt) f2b_pkgs=("fail2ban" "python3-systemd" "rsyslog");; - dnf|yum) f2b_pkgs=("fail2ban" "rsyslog");; - zypper) f2b_pkgs=("fail2ban" "rsyslog");; - pacman) f2b_pkgs=("fail2ban");; - apk) f2b_pkgs=("fail2ban");; - esac - pkg_reinstall "${f2b_pkgs[@]}" - if [ -d "/etc/fail2ban" ]; then - echo -e "${INFO} ${GREEN}修复成功!${RESET}" - sleep 1 - else - echo -e "${ERROR} 修复失败,请手动处理:dpkg -l | grep fail2ban${RESET}" - read -rp "按回车键返回..." - return 1 - fi - ;; - 2) - echo -e "${WARN} 正在强制卸载 Fail2Ban..." - svc_stop fail2ban 2>/dev/null - svc_disable fail2ban 2>/dev/null - pkg_remove fail2ban - # 兜底:apt 失败时直接用 dpkg 清(dpkg 数据库损坏场景) - hash -r 2>/dev/null - if command -v fail2ban-client &>/dev/null; then - echo -e "${WARN} 检测到残留,尝试 dpkg 强制清除..." - $SUDO dpkg --purge --force-all fail2ban 2>/dev/null || true - $SUDO rm -rf /var/lib/dpkg/info/fail2ban.* 2>/dev/null - hash -r 2>/dev/null - fi - $SUDO rm -rf /etc/fail2ban - $SUDO rm -f /usr/bin/fail2ban-client /usr/bin/fail2ban-server /usr/local/bin/fail2ban-* 2>/dev/null - echo -e "${INFO} ${GREEN}Fail2Ban 已强制卸载。${RESET}" - read -rp "按回车键返回..." - return 1 - ;; - *) - return 1 - ;; - esac - fi - - if [ ! -f "$JAIL_CONF" ]; then - generate_default_jail_conf | $SUDO tee "$JAIL_CONF" > /dev/null - else - if ! grep -q "^\[DEFAULT\]" "$JAIL_CONF"; then - local be="auto"; [ "$INIT_SYS" = "systemd" ] && be="systemd" - $SUDO sed -i "1i [DEFAULT]\nbackend = ${be}" "$JAIL_CONF" - fi - # 迁移老配置:把 [sshd] 段错放的递增参数挪到 [DEFAULT] - migrate_f2b_increment - if ! grep -q "^\[${TARGET_JAIL}\]" "$JAIL_CONF"; then - generate_default_jail_conf | grep -A99 "^\[${TARGET_JAIL}\]" | $SUDO tee -a "$JAIL_CONF" > /dev/null - else - # 保证 [sshd] 段基本参数齐全(不包含递增参数,那些在 [DEFAULT]) - local current_port - current_port=$(get_sshd_config_val "Port" "22") - [ -z "$current_port" ] && current_port="22" - local defaults=( - "enabled=true" "port=${current_port}" "filter=sshd" "maxretry=5" - "bantime=600" "findtime=3600" - ) - for item in "${defaults[@]}"; do - local k="${item%%=*}" v="${item#*=}" - [ -n "$(get_f2b_conf "$k")" ] || set_f2b_conf "$k" "$v" - done - fi - fi - return 0 -} - -uninstall_f2b() { - echo -e "\n${RED}${BOLD}警告:即将卸载 Fail2Ban 及其配置!${RESET}" - read -rp "确认卸载吗?(y/N): " confirm - [[ ! "$confirm" =~ ^[Yy]$ ]] && { echo -e "${INFO} 已取消卸载。"; read -rp "按回车键继续..."; return; } - svc_stop fail2ban; svc_disable fail2ban - pkg_remove fail2ban - read -rp "是否同时删除配置目录 /etc/fail2ban ?(y/N): " del_conf - [[ "$del_conf" =~ ^[Yy]$ ]] && { $SUDO rm -rf /etc/fail2ban; echo -e "${INFO} 已删除 /etc/fail2ban"; } - echo -e "${INFO} ${GREEN}Fail2Ban 卸载完成。${RESET}"; read -rp "按回车键继续..." -} - -change_f2b_param() { - local name=$1 key=$2 type=$3 - local current; current=$(get_f2b_conf "$key") - echo -e "\n${INFO} 正在修改: ${CYAN}${name}${RESET}" - echo -e "当前值: ${GREEN}$(fmt_f2b_unit "$current" "$type")${RESET}" - [ "$type" == "time" ] && echo -e "${GRAY}(支持后缀: s=秒, m=分, h=小时, d=天)${RESET}" - while true; do - read -rp "请输入新值 (留空取消): " new_val - [ -z "$new_val" ] && return - if [ "$type" == "time" ] && validate_time "$new_val"; then break; fi - if [ "$type" == "int" ] && validate_int "$new_val"; then break; fi - if [ "$type" == "factor" ] && validate_int "$new_val"; then break; fi - echo -e "${ERROR} 格式错误,请重试。" - done - set_f2b_conf "$key" "$new_val"; restart_f2b -} - -toggle_f2b_service() { - echo -e "\n${CYAN}------------------- 服务开关 -------------------${RESET}" - if fail2ban-client ping >/dev/null 2>&1; then - read -rp "是否停止并禁用 Fail2Ban? (y/N): " confirm - [[ "$confirm" =~ ^[Yy]$ ]] && { svc_stop fail2ban; svc_disable fail2ban; echo -e "${WARN} 服务已停止。${RESET}"; } - else - read -rp "是否启用并启动 Fail2Ban? (y/N): " confirm - if [[ "$confirm" =~ ^[Yy]$ ]]; then - svc_enable fail2ban; svc_start fail2ban - for i in {1..5}; do - if fail2ban-client ping >/dev/null 2>&1; then echo -e "${INFO} ${GREEN}服务已成功启动。${RESET}"; read -rp "按回车键继续..."; return; fi; sleep 1 - done - echo -e "${ERROR} 启动失败或超时。" - fi - fi - read -rp "按回车键继续..." -} - -unban_f2b_ip() { - echo -e "\n${CYAN}------------------ 手动解封 IP ------------------${RESET}" - local banned_list - banned_list=$(fail2ban-client status "$TARGET_JAIL" 2>/dev/null | grep "Banned IP list" | awk -F':' '{print $2}' | sed 's/^[ \t]*//') - [ -z "$banned_list" ] && banned_list="无" - echo -e "当前被封禁列表: ${YELLOW}${banned_list}${RESET}" - read -rp "输入要解封的 IP (留空取消): " target_ip; [ -z "$target_ip" ] && return - $SUDO fail2ban-client set "$TARGET_JAIL" unbanip "$target_ip" - [ $? -eq 0 ] && echo -e "${INFO} ${GREEN}解封成功: $target_ip${RESET}" || echo -e "${ERROR} 操作失败。" - read -rp "按回车键继续..." -} - -add_f2b_whitelist() { - echo -e "\n${CYAN}------------------ 白名单管理 ------------------${RESET}" - local current_list; current_list=$(get_f2b_conf "ignoreip") - echo -e "当前白名单: ${YELLOW}${current_list:-继承全局或无}${RESET}" - local current_ip; current_ip=$(echo "$SSH_CLIENT" | awk '{print $1}') - read -rp "输入要放行的 IP (回车默认当前连接 IP: ${current_ip:-无}): " input_ip - [ -z "$input_ip" ] && input_ip="$current_ip" - [ -z "$input_ip" ] && echo -e "${ERROR} 无法获取 IP。" && return - if echo "$current_list" | grep -Fq "$input_ip"; then - echo -e "${WARN} 该 IP 已在白名单中。" - else - if [ -z "$current_list" ]; then set_f2b_conf "ignoreip" "$input_ip" - else set_f2b_conf "ignoreip" "$current_list $input_ip"; fi - restart_f2b - fi - read -rp "按回车键继续..." -} - -view_f2b_logs() { - clear - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} Fail2Ban 审计日志 (最近 20 条)${RESET}" - echo -e "${CYAN}============================================================${RESET}" - if [ ! -f "$LOG_FILE" ]; then - echo -e "${WARN} 日志文件不存在: $LOG_FILE" - else - local out - out=$(grep -E "(Ban|Unban)" "$LOG_FILE" 2>/dev/null | tail -n 20) - if [ -z "$out" ]; then - echo -e "${WARN} 暂无封禁/解封记录${RESET}" - else - echo "$out" | awk '{ - gsub(/Unban/, "\033[32m&\033[0m"); - gsub(/Ban/, "\033[31m&\033[0m"); - print - }' - fi - fi - echo -e "${CYAN}============================================================${RESET}" - read -rp "按回车键返回..." -} - -menu_f2b_exponential() { - while true; do - clear - local inc fac max - inc=$(get_f2b_conf "bantime.increment") - fac=$(get_f2b_conf "bantime.factor") - max=$(get_f2b_conf "bantime.maxtime") - local S_INC; [ "$inc" == "true" ] && S_INC="${GREEN}启用${RESET}" || S_INC="${YELLOW}禁用${RESET}" - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} 高级: 指数封禁设置 (针对 sshd)${RESET}" - echo -e "${CYAN}============================================================${RESET}" - echo -e " 说明: 对重复犯错的恶意 IP,封禁时间按设定系数成倍递增" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e " ${GREEN}1.${RESET} 递增模式开关 [${S_INC}]" - echo -e " ${GREEN}2.${RESET} 增长系数 [${YELLOW}${fac:-未设置}${RESET}]$(fmt_f2b_unit "$fac" "factor")" - echo -e " ${GREEN}3.${RESET} 封禁上限 [${YELLOW}${max:-未设置}${RESET}]$(fmt_f2b_unit "$max" "time")" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e " ${GREEN}0.${RESET} 返回上级" - echo -e "${CYAN}============================================================${RESET}" - echo -e "${GRAY}提示: 输入对应序号后可自定义该参数${RESET}" - read -rp "请选择 [0-3]: " sc - case "$sc" in - 1) [ "$inc" == "true" ] && ns="false" || ns="true"; set_f2b_conf "bantime.increment" "$ns"; restart_f2b ;; - 2) change_f2b_param "增长系数 (倍数)" "bantime.factor" "factor" ;; - 3) change_f2b_param "封禁上限 (时间)" "bantime.maxtime" "time" ;; - 0) return ;; - *) echo -e "${ERROR} 无效选项!"; sleep 1 ;; - esac - done -} - -manage_fail2ban_menu() { - if ! check_f2b_install; then read -rp "按回车键返回主菜单..."; return; fi - while true; do - clear - VAL_MAX=$(get_f2b_conf "maxretry"); VAL_BAN=$(get_f2b_conf "bantime"); VAL_FIND=$(get_f2b_conf "findtime") - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} Fail2Ban 防护管理${RESET}" - echo -e "${CYAN}============================================================${RESET}" - echo -e " 服务状态: $(get_fail2ban_status)" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e " ${GREEN}1.${RESET} 最大重试次数 [${YELLOW}${VAL_MAX:-默认}${RESET}]" - echo -e " ${GREEN}2.${RESET} 初始封禁时长 [${YELLOW}${VAL_BAN:-默认}${RESET}]$(fmt_f2b_unit "$VAL_BAN" "time")" - echo -e " ${GREEN}3.${RESET} 监测时间窗口 [${YELLOW}${VAL_FIND:-默认}${RESET}]$(fmt_f2b_unit "$VAL_FIND" "time")" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e " ${GREEN}4.${RESET} 手动解封 IP" - echo -e " ${GREEN}5.${RESET} 添加 IP 白名单" - echo -e " ${GREEN}6.${RESET} 查看封禁日志 (最近20条)" - echo -e " ${GREEN}7.${RESET} 指数递增封禁设置 ->" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e " ${GREEN}8.${RESET} 启用 / 停止 服务" - echo -e " ${GREEN}9.${RESET} 卸载 Fail2Ban" - echo -e " ${GREEN}0.${RESET} 返回主菜单" - echo -e "${CYAN}============================================================${RESET}" - read -rp "请选择 [0-9]: " choice - case "$choice" in - 1) change_f2b_param "最大重试次数" "maxretry" "int" ;; - 2) change_f2b_param "初始封禁时长" "bantime" "time" ;; - 3) change_f2b_param "监测时间窗口" "findtime" "time" ;; - 4) unban_f2b_ip ;; - 5) add_f2b_whitelist ;; - 6) view_f2b_logs ;; - 7) menu_f2b_exponential ;; - 8) toggle_f2b_service ;; - 9) uninstall_f2b - # 卸载后如果 fail2ban-client 已消失,直接返回主菜单 - if ! command -v fail2ban-client &>/dev/null; then - return - fi - ;; - 0) return ;; - *) echo -e "${ERROR} 无效选项!"; sleep 1 ;; - esac - done -} - -# ============ 状态面板 ============ -show_status() { - local port; port=$(get_sshd_config_val "Port" "22") - local pwd_auth; pwd_auth=$(get_sshd_config_val "PasswordAuthentication" "yes") - local pubkey_auth; pubkey_auth=$(get_sshd_config_val "PubkeyAuthentication" "yes") - local f2b_stat; f2b_stat=$(get_fail2ban_status) - local key_count; key_count=$(count_authorized_keys) - - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} SSH 安全配置工具${RESET}" - echo -e "${CYAN}============================================================${RESET}" - echo -e " 系统架构 : ${GREEN}${OS_SHORT} ${OS_VER}${RESET}" - if [[ "${pubkey_auth,,}" == "yes" ]]; then - if [ "$key_count" -gt 0 ]; then - echo -e " 密钥登录 : ${GREEN}已启用 (${key_count} 把公钥)${RESET}" - else - echo -e " 密钥登录 : ${YELLOW}已启用 (但无公钥,无法密钥登录)${RESET}" - fi - else - echo -e " 密钥登录 : ${YELLOW}已禁用${RESET}" - fi - if [[ "${pwd_auth,,}" == "no" ]]; then - echo -e " 密码登录 : ${GREEN}已禁用 (PasswordAuthentication no)${RESET}" - else - echo -e " 密码登录 : ${YELLOW}已启用 (推荐配置密钥后禁用)${RESET}" - fi - echo -e " Fail2Ban : ${f2b_stat}" - echo -e " SSH 端口 : ${CYAN}${port}${RESET}" - echo -e "${CYAN}============================================================${RESET}" -} - -# ============ 密钥生成 ============ -generate_vps_keypair() { - echo -e "\n${INFO} 准备在 VPS 上生成新的 ED25519 密钥..." - local key_file="${HOME}/.ssh/PrivateKey.pem" - local pub_file="${HOME}/.ssh/PublicKey.pub" - - if [ -f "$key_file" ] || [ -f "$pub_file" ] || [ -f "${key_file}.pub" ]; then - echo -e "${WARN} 检测到已有旧同名密钥,即将覆盖清理..." - rm -f "$key_file" "$pub_file" "${key_file}.pub" - fi - - # -C "" 保证生成的密钥无后缀标识信息 - ssh-keygen -t ed25519 -C "" -f "$key_file" -N "" -q - if [ ! -f "${key_file}.pub" ]; then - echo -e "${ERROR} 密钥生成失败!请检查 $HOME/.ssh 是否可写或磁盘空间是否充足。" - read -rp "按回车键返回..." - return 1 - fi - mv "${key_file}.pub" "$pub_file" - - local pub_content - pub_content=$(cat "$pub_file") - - append_key_with_meta "$pub_content" "VPS本地生成" - - echo -e "\n${GREEN}====================== 密钥生成成功 ======================${RESET}" - echo -e " VPS 上的私钥路径 : ${CYAN}${key_file}${RESET}" - echo -e " VPS 上的公钥路径 : ${CYAN}${pub_file}${RESET}" - echo -e " 授权目标文件 : 已将公钥写入 ${CYAN}${HOME}/.ssh/authorized_keys${RESET}" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e "${YELLOW}${BOLD}[私钥文本 (PrivateKey.pem)] - 用于登录此 VPS:${RESET}" - echo -e "${YELLOW}" - cat "$key_file" - echo -e "${RESET}" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - echo -e "${GREEN}${BOLD}[公钥文本 (PublicKey.pub)] - 用于上传至 GitHub:${RESET}" - echo -e "${GREEN}${pub_content}${RESET}" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - - echo -e "${BOLD}${PURPLE}[💡 新手一劳永逸指南]${RESET}" - echo -e " ${BOLD}一、保存密钥到本地电脑 (二选一):${RESET}" - echo -e " ${GREEN}• 方式 A (推荐·直接下载):${RESET}打开 SSH 客户端自带的 ${CYAN}SFTP / 文件传输${RESET} 功能 (如 FinalShell、Xshell、WinSCP、Termius 等)," - echo -e " 定位到服务器目录:${CYAN}${HOME}/.ssh/${RESET}" - echo -e " 将 ${CYAN}PrivateKey.pem${RESET} (私钥) 和 ${CYAN}PublicKey.pub${RESET} (公钥) 直接下载保存到本地电脑。" - echo -e " ${GREEN}• 方式 B (手动复制):${RESET}" - echo -e " 1. 复制上面的【私钥文本】,在本地新建纯文本文件保存为:${CYAN}PrivateKey.pem${RESET}" - echo -e " 2. 复制上面的【公钥文本】,在本地新建纯文本文件保存为:${CYAN}PublicKey.pub${RESET}" - echo -e "\n ${BOLD}二、绑定 GitHub 打通长期复用工作流(可选):${RESET}" - echo -e " 1. 打开 ${CYAN}https://github.com/settings/keys${RESET} ,点击 \"New SSH key\";" - echo -e " 2. 将 ${CYAN}PublicKey.pub${RESET} 里的【公钥文本】粘贴进去并保存;" - echo -e " 3. ${GREEN}以后在其他 VPS 上运行本脚本,只需选择【选项 1】输入 GitHub 用户名即可快速拉取!${RESET}\n" - - read -rp "确认已保存/下载密钥,是否立即删除 VPS 上的暂存密钥文件?(Y/n): " rm_confirm - if [[ -z "$rm_confirm" || "$rm_confirm" =~ ^[Yy]$ ]]; then - rm -f "$key_file" "$pub_file" - echo -e "${INFO} ${GREEN}已成功删除 VPS 上的暂存密钥文件。${RESET}" - else - echo -e "${WARN} 密钥文件仍保留在: ${key_file} 和 ${pub_file} (请务必防范私钥泄露)" - fi -} - -# ============ 密钥登录开关 ============ -toggle_pubkey_login() { - local current - current=$(get_sshd_config_val "PubkeyAuthentication" "yes") - - if [[ "${current,,}" == "no" ]]; then - echo -e "\n当前密钥登录已${GREEN}禁用${RESET}。" - local key_count; key_count=$(count_authorized_keys) - if [ "$key_count" -eq 0 ]; then - echo -e "${YELLOW}[提示] 当前 authorized_keys 中还没有公钥,启用后仍需先添加公钥才能通过密钥登录。${RESET}" - fi - read -rp "是否要启用密钥登录?(y/N): " confirm - if [[ "$confirm" =~ ^[Yy]$ ]]; then - set_sshd_config "PubkeyAuthentication" "yes" - restart_sshd - echo -e "${INFO} ${GREEN}密钥登录已成功启用。${RESET}" - fi - else - echo -e "\n${YELLOW}${BOLD}[警告] 禁用密钥登录后,如果密码登录也已禁用,你将无法登录 VPS!${RESET}" - if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then - echo -e "${CYAN}${BOLD}[提示] 检测到您正在使用 SSH 远程会话,修改后切勿关闭当前窗口!${RESET}" - fi - - local pwd_auth - pwd_auth=$(get_sshd_config_val "PasswordAuthentication" "yes") - if [[ "${pwd_auth,,}" == "no" ]]; then - echo -e "${RED} 检测到密码登录已禁用,禁用密钥登录后你将无法登录此 VPS!${RESET}" - read -rp "确认仍然要禁用密钥登录吗?(y/N): " confirm_risky - [[ ! "$confirm_risky" =~ ^[Yy]$ ]] && { echo -e "${INFO} 已取消操作。"; read -rp "按回车键继续..."; return; } - else - read -rp "确认禁用密钥登录吗?(y/N): " confirm - [[ ! "$confirm" =~ ^[Yy]$ ]] && { echo -e "${INFO} 已取消操作。"; read -rp "按回车键继续..."; return; } - fi - - set_sshd_config "PubkeyAuthentication" "no" - restart_sshd - echo -e "${INFO} ${GREEN}密钥登录已禁用,现在只能通过密码登录。${RESET}" - fi - read -rp "按回车键继续..." -} - -# ============ 密钥配置菜单 ============ -install_key_menu() { - while true; do - clear - init_ssh_dir - - # 动态统计当前公钥数量 - local key_count - key_count=$(count_authorized_keys) - local key_count_label - if [ "$key_count" -gt 0 ]; then - key_count_label=" (${GREEN}${key_count} 把公钥${RESET})" - else - key_count_label=" (${YELLOW}无公钥${RESET})" - fi - - # 动态显示当前密钥登录开关状态,避免新人困惑 - local pubkey_status - pubkey_status=$(get_sshd_config_val "PubkeyAuthentication" "yes") - local pubkey_label - if [[ "${pubkey_status,,}" == "yes" ]]; then - pubkey_label="[${GREEN}已启用${RESET}]" - else - pubkey_label="[${YELLOW}已禁用${RESET}]" - fi - - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} SSH 密钥登录管理${RESET}" - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}请选择 SSH 密钥配置方式:${RESET}" - echo -e " ${GREEN}1.${RESET} 从 GitHub 获取公钥 (${CYAN}适合:已将公钥上传至 GitHub 的用户${RESET})" - echo -e " ${GREEN}2.${RESET} 在 VPS 上全新生成密钥 (${CYAN}适合:本地没有密钥的新手,生成后可传 GitHub${RESET})" - echo -e " ${GREEN}3.${RESET} 从自定义 URL 获取公钥 (${CYAN}适合:有公钥直链的用户${RESET})" - echo -e " ${GREEN}4.${RESET} 管理已存公钥${key_count_label}" - echo -e " ${GREEN}5.${RESET} 密钥登录开关 ${pubkey_label}" - echo -e " ${GREEN}0.${RESET} 返回主菜单" - echo -e "${CYAN}============================================================${RESET}" - read -rp "请输入选项 [0-5]: " key_opt - - local test_hint="" - local do_restart=0 - - case "$key_opt" in - 1) - echo -e "\n${YELLOW}${BOLD}[使用前提]${RESET}" - echo -e "需先将本地公钥上传至 GitHub: ${CYAN}https://github.com/settings/keys${RESET}\n" - read -rp "请输入您的 GitHub 用户名: " gh_user - if [ -z "$gh_user" ]; then - echo -e "${ERROR} 输入不能为空!" - read -rp "按回车键继续..." - continue - fi - echo -e "${INFO} 正在从 GitHub 拉取公钥..." - local pub_key - pub_key=$(curl -fsSL "https://github.com/${gh_user}.keys") - if [ -z "$pub_key" ] || [[ "$pub_key" == "Not Found" ]]; then - echo -e "\n${ERROR} 获取公钥失败!可能是用户名不正确,或该 GitHub 账号未配置公钥。" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - read -rp "是否要在 VPS 上全新生成密钥 (选项 2)?(y/N): " switch_opt2 - if [[ "$switch_opt2" =~ ^[Yy]$ ]]; then - generate_vps_keypair - test_hint="请将已保存的私钥导入本地 SSH 客户端,新建终端测试连接。" - do_restart=1 - else - read -rp "按回车键继续..." - continue - fi - else - append_key_with_meta "$pub_key" "GitHub: ${gh_user}" - test_hint="请使用该 GitHub 公钥对应的本地私钥,新建终端测试连接。" - do_restart=1 - fi - ;; - 2) - generate_vps_keypair - test_hint="请将已保存的私钥导入本地 SSH 客户端,新建终端测试连接。" - do_restart=1 - ;; - 3) - read -rp "请输入公钥 URL: " key_url - if [ -z "$key_url" ]; then - echo -e "${ERROR} URL 不能为空!" - read -rp "按回车键继续..." - continue - fi - local pub_key - pub_key=$(curl -fsSL "$key_url") - if [ -z "$pub_key" ]; then - echo -e "${ERROR} 从 URL 获取公钥失败!" - read -rp "按回车键继续..." - continue - fi - append_key_with_meta "$pub_key" "自定义URL" - test_hint="请使用该公钥对应的本地私钥,新建终端测试连接。" - do_restart=1 - ;; - 4) - manage_keys_menu - continue - ;; - 5) - toggle_pubkey_login - continue - ;; - 0) return ;; - *) - echo -e "${ERROR} 无效选项!" - sleep 1 - continue - ;; - esac - - if [ "$do_restart" == "1" ]; then - set_sshd_config "PubkeyAuthentication" "yes" - restart_sshd - echo -e "\n${CYAN}------------------------------------------------------------${RESET}" - echo -e "${YELLOW}${BOLD}[重点测试]${RESET} ${test_hint}" - echo -e "测试成功后,再返回主菜单【禁用密码登录】!" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - read -rp "按回车键返回密钥管理子菜单..." - fi - done -} - -# ============ 已存公钥管理 ============ -manage_keys_menu() { - init_ssh_dir - local auth_file="${HOME}/.ssh/authorized_keys" - - while true; do - clear - echo -e "${CYAN}============================================================${RESET}" - echo -e "${BOLD}${PURPLE} SSH 已存公钥管理${RESET}" - echo -e "${CYAN}============================================================${RESET}" - - local key_lines=() - local key_contents=() - local line_num=0 - - if [ -f "$auth_file" ]; then - while IFS= read -r line || [ -n "$line" ]; do - ((line_num++)) - if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then - continue - fi - key_lines+=("$line_num") - key_contents+=("$line") - done < "$auth_file" - fi - - if [ ${#key_contents[@]} -eq 0 ]; then - echo -e "\n${WARN} 当前 ${auth_file} 中没有找到任何有效公钥!" - echo -e "${CYAN}============================================================${RESET}" - read -rp "按回车键返回..." - return - fi - - printf " %-4s | %-19s | %-8s | %-16s\n" "序号" " 添加时间" "公钥类型" " 备注来源" - echo -e "${CYAN}------------------------------------------------------------${RESET}" - - local idx=1 - for key in "${key_contents[@]}"; do - local add_time="历史存量/未标记" - local key_tag="未知/手动导入" - - if [[ "$key" =~ \[([0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2})\|([^\]]+)\] ]]; then - add_time="${BASH_REMATCH[1]}" - key_tag="${BASH_REMATCH[2]}" - fi - - # 修正:带选项前缀的公钥也能正确显示密钥类型 - local key_type - key_type=$(echo "$key" | awk '{ - for(i=1;i<=NF;i++){ - if($i ~ /^(ssh-|ecdsa-|sk-)/){ print $i; exit } - } - print "未知" - }') - - printf " ${GREEN}[%2d]${RESET} | ${YELLOW}%19s${RESET} | ${CYAN}%-8s${RESET} | ${PURPLE}%-16s${RESET}\n" "$idx" "$add_time" "$key_type" "$key_tag" - ((idx++)) - done - - echo -e "${CYAN}============================================================${RESET}" - echo -e " 输入 ${RED}[序号]${RESET} : 删除指定公钥" - echo -e " 输入 ${RED}[all]${RESET} : 清空全部公钥" - echo -e " 输入 ${GREEN}[0]${RESET} : 返回上级菜单" - echo -e "${CYAN}============================================================${RESET}" - read -rp "请输入操作指令: " key_action - - if [ "$key_action" == "0" ]; then - return - elif [ "$key_action" == "all" ]; then - # 危险检查:密码登录已禁用时清空全部公钥会锁死 - if [[ "$(get_sshd_config_val "PasswordAuthentication" "yes")" == "no" ]]; then - echo -e "${RED}${BOLD}[危险] 密码登录已禁用,清空全部公钥后你将无法登录此 VPS!${RESET}" - read -rp "确认仍然要清空吗?(y/N): " risky_all - [[ ! "$risky_all" =~ ^[Yy]$ ]] && continue - fi - read -rp "确认要清空所有公钥吗?(y/N): " confirm_all - if [[ "$confirm_all" =~ ^[Yy]$ ]]; then - > "$auth_file" - echo -e "${INFO} 已清空所有公钥!" - sleep 1 - continue - fi - elif [[ "$key_action" =~ ^[0-9]+$ ]] && [ "$key_action" -ge 1 ] && [ "$key_action" -le "${#key_contents[@]}" ]; then - local target_idx=$((key_action - 1)) - local target_line_num="${key_lines[$target_idx]}" - - # 危险检查:密码登录已禁用 + 只剩最后一个公钥时删除会锁死 - if [[ "$(get_sshd_config_val "PasswordAuthentication" "yes")" == "no" ]] && [ "${#key_contents[@]}" -eq 1 ]; then - echo -e "${RED}${BOLD}[危险] 密码登录已禁用,删除最后一个公钥后你将无法登录此 VPS!${RESET}" - read -rp "确认仍然要删除吗?(y/N): " risky_del - [[ ! "$risky_del" =~ ^[Yy]$ ]] && continue - fi - - read -rp "确认删除序号 [${key_action}] 的公钥吗?(y/N): " confirm_del - if [[ "$confirm_del" =~ ^[Yy]$ ]]; then - sed -i "${target_line_num}d" "$auth_file" - chmod 600 "$auth_file" - echo -e "${INFO} ${GREEN}序号 [${key_action}] 的公钥已成功删除!${RESET}" - sleep 1 - continue - fi - else - echo -e "${ERROR} 输入无效,请重新输入!" - sleep 1 - continue - fi - done -} - -# ============ 密码登录开关 ============ -toggle_password_login() { - local current - current=$(get_sshd_config_val "PasswordAuthentication" "yes") - - if [[ "${current,,}" == "no" ]]; then - echo -e "\n当前密码登录已${GREEN}禁用${RESET}。" - read -rp "是否要启用密码登录?(y/N): " confirm - if [[ "$confirm" =~ ^[Yy]$ ]]; then - set_sshd_config "PasswordAuthentication" "yes" - restart_sshd - echo -e "${INFO} 已成功启用密码登录。" - - read -rp "是否需要为当前用户 ($(whoami)) 设置新密码?(y/N): " pwd_confirm - if [[ "$pwd_confirm" =~ ^[Yy]$ ]]; then - passwd "$(whoami)" - fi - fi - else - echo -e "\n${YELLOW}${BOLD}[警告] 禁用密码登录前,请务必确认密钥登录能够成功!${RESET}" - # 危险检查:密钥登录不可用时禁用密码登录会锁死 - local pubkey_auth; pubkey_auth=$(get_sshd_config_val "PubkeyAuthentication" "yes") - local pubkey_ok=0 - if [[ "${pubkey_auth,,}" == "yes" ]] && has_valid_pubkey; then - pubkey_ok=1 - fi - if [ "$pubkey_ok" -eq 0 ]; then - if [[ "${pubkey_auth,,}" != "yes" ]]; then - echo -e "${RED}${BOLD}[危险] 密钥登录已禁用,禁用密码登录后你将无法登录此 VPS!${RESET}" - else - echo -e "${RED}${BOLD}[危险] authorized_keys 中没有任何公钥,禁用密码登录后你将无法登录此 VPS!${RESET}" - fi - read -rp "确认仍然要禁用吗?(y/N): " risky_confirm - [[ ! "$risky_confirm" =~ ^[Yy]$ ]] && { echo -e "${INFO} 已取消操作。"; read -rp "按回车键继续..."; return; } - fi - if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then - echo -e "${CYAN}${BOLD}[提示] 检测到您正在使用 SSH 远程会话,修改后切勿关闭当前窗口,请先新建终端测试连接!${RESET}" - fi - read -rp "确认彻底禁用密码登录吗?(y/N): " confirm - if [[ "$confirm" =~ ^[Yy]$ ]]; then - set_sshd_config "PasswordAuthentication" "no" - set_sshd_config "ChallengeResponseAuthentication" "no" - set_sshd_config "KbdInteractiveAuthentication" "no" - restart_sshd - echo -e "${INFO} ${GREEN}密码登录已成功禁用!现在只能通过密钥登录。${RESET}" - fi - fi - read -rp "按回车键返回主菜单..." -} - -# ============ 修改 SSH 端口 ============ -change_ssh_port() { - local current_port - current_port=$(get_sshd_config_val "Port" "22") - echo -e "\n当前 SSH 端口为: ${CYAN}${current_port}${RESET}" - if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then - echo -e "${YELLOW}${BOLD}[提示] 检测到您正在使用 SSH 远程会话,修改端口后请勿关闭当前窗口,请先新建终端验证!${RESET}" - fi - read -rp "请输入新的 SSH 端口 (1024-65535): " new_port - - if [[ ! "$new_port" =~ ^[0-9]+$ ]] || [ "$new_port" -lt 1 ] || [ "$new_port" -gt 65535 ]; then - echo -e "${ERROR} 端口格式不正确!" - read -rp "按回车键返回主菜单..." - return - fi - - if command -v getenforce &>/dev/null && [ "$(getenforce)" != "Disabled" ]; then - echo -e "${INFO} 检测到 SELinux 启用,正在申请放行端口 ${new_port}..." - if command -v semanage &>/dev/null; then - $SUDO semanage port -a -t ssh_port_t -p tcp "$new_port" 2>/dev/null || \ - $SUDO semanage port -m -t ssh_port_t -p tcp "$new_port" 2>/dev/null - else - echo -e "${WARN} 未安装 semanage,如使用 RHEL/CentOS 系系统,可能需要手动配置 SELinux。" - fi - fi - - if command -v ufw &>/dev/null && ufw status | grep -q "active"; then - echo -e "${INFO} 正在向 ufw 防火墙放行端口 ${new_port}/tcp..." - $SUDO ufw allow "$new_port"/tcp >/dev/null - elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then - echo -e "${INFO} 正在向 firewalld 防火墙放行端口 ${new_port}/tcp..." - $SUDO firewall-cmd --permanent --add-port="${new_port}/tcp" >/dev/null - $SUDO firewall-cmd --reload >/dev/null - fi - - set_sshd_config "Port" "$new_port" - - if restart_sshd; then - echo -e "${INFO} ${GREEN}SSH 端口已顺利修改为 ${new_port}${RESET}" - - # 同步更新 Fail2Ban 端口 - if [ -f "$JAIL_CONF" ] && grep -q "^\[${TARGET_JAIL}\]" "$JAIL_CONF"; then - set_f2b_conf "port" "$new_port" - if command -v fail2ban-client &>/dev/null && fail2ban-client ping >/dev/null 2>&1; then - echo -e "${INFO} 正在同步更新 Fail2Ban 防护端口..." - restart_f2b - fi - fi - - if command -v ss &>/dev/null; then - echo -e "${INFO} 系统实际监听服务端口状态:" - ss -tulpn | grep ssh || true - fi - echo -e "${WARN} 注意:如使用的是阿里云/腾讯云/AWS等,请务必在【云服务器安全组】中开放 TCP ${new_port} 端口!" - fi - - read -rp "按回车键返回主菜单..." -} - -# ============ 主逻辑 ============ -detect_os; detect_pkg_mgr; detect_init; detect_ssh_log -check_dependencies - -# ============ 命令行参数解析 ============ -# 先收集所有参数,循环结束后按依赖顺序统一执行 -OVERWRITE=0 -CLI_GH_USER="" -CLI_KEY_URL="" -CLI_KEY_FILE="" -CLI_PORT="" -CLI_DISABLE_PWD=0 - -while getopts "og:u:f:p:d" OPT; do - case $OPT in - o) OVERWRITE=1 ;; - g) CLI_GH_USER="$OPTARG" ;; - u) CLI_KEY_URL="$OPTARG" ;; - f) CLI_KEY_FILE="$OPTARG" ;; - p) CLI_PORT="$OPTARG" ;; - d) CLI_DISABLE_PWD=1 ;; - *) exit 1 ;; - esac -done - -# -o 必须配合公钥来源使用,否则会误清空 authorized_keys -if [ "$OVERWRITE" -eq 1 ] && [ -z "$CLI_GH_USER" ] && [ -z "$CLI_KEY_URL" ] && [ -z "$CLI_KEY_FILE" ]; then - echo -e "${ERROR} -o(覆盖模式)必须与 -g、-u 或 -f 一起使用!" - echo -e "${YELLOW}单独执行 -o 会清空 authorized_keys 中所有公钥,已阻止。${RESET}" - exit 1 -fi - -# 有任意 CLI 参数就进入批量执行模式 -if [ -n "$CLI_GH_USER" ] || [ -n "$CLI_KEY_URL" ] || [ -n "$CLI_KEY_FILE" ] || \ - [ -n "$CLI_PORT" ] || [ "$CLI_DISABLE_PWD" -eq 1 ]; then - - need_restart_sshd=0 - need_restart_f2b=0 - - if [ -n "$CLI_GH_USER" ] || [ -n "$CLI_KEY_URL" ] || [ -n "$CLI_KEY_FILE" ]; then - init_ssh_dir - [ "$OVERWRITE" == "1" ] && > "${HOME}/.ssh/authorized_keys" - fi - - # 1. 导入公钥 - if [ -n "$CLI_GH_USER" ]; then - PUB_KEY=$(curl -fsSL "https://github.com/${CLI_GH_USER}.keys") - [ -z "$PUB_KEY" ] && echo -e "${ERROR} 获取 GitHub 公钥失败" && exit 1 - append_key_with_meta "$PUB_KEY" "GitHub: ${CLI_GH_USER}" - set_sshd_config "PubkeyAuthentication" "yes" - need_restart_sshd=1 - fi - - if [ -n "$CLI_KEY_URL" ]; then - PUB_KEY=$(curl -fsSL "${CLI_KEY_URL}") - [ -z "$PUB_KEY" ] && echo -e "${ERROR} 从 URL 获取公钥失败" && exit 1 - append_key_with_meta "$PUB_KEY" "自定义URL" - set_sshd_config "PubkeyAuthentication" "yes" - need_restart_sshd=1 - fi - - if [ -n "$CLI_KEY_FILE" ]; then - if [ -f "${CLI_KEY_FILE}" ]; then - PUB_KEY=$(cat "${CLI_KEY_FILE}") - append_key_with_meta "$PUB_KEY" "本地文件导入" - set_sshd_config "PubkeyAuthentication" "yes" - need_restart_sshd=1 - else - echo -e "${ERROR} 找不到公钥文件 ${CLI_KEY_FILE}" && exit 1 - fi - fi - - # 2. 修改端口 - if [ -n "$CLI_PORT" ]; then - if [[ ! "$CLI_PORT" =~ ^[0-9]+$ ]] || [ "$CLI_PORT" -lt 1 ] || [ "$CLI_PORT" -gt 65535 ]; then - echo -e "${ERROR} 端口格式不正确!" && exit 1 - fi - - if command -v getenforce &>/dev/null && [ "$(getenforce)" != "Disabled" ]; then - echo -e "${INFO} 检测到 SELinux 启用,正在申请放行端口 ${CLI_PORT}..." - if command -v semanage &>/dev/null; then - $SUDO semanage port -a -t ssh_port_t -p tcp "$CLI_PORT" 2>/dev/null || \ - $SUDO semanage port -m -t ssh_port_t -p tcp "$CLI_PORT" 2>/dev/null - else - echo -e "${WARN} 未安装 semanage,如使用 RHEL/CentOS 系系统,可能需要手动配置 SELinux。" - fi - fi - - if command -v ufw &>/dev/null && ufw status | grep -q "active"; then - echo -e "${INFO} 正在向 ufw 防火墙放行端口 ${CLI_PORT}/tcp..." - $SUDO ufw allow "$CLI_PORT"/tcp >/dev/null - elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then - echo -e "${INFO} 正在向 firewalld 防火墙放行端口 ${CLI_PORT}/tcp..." - $SUDO firewall-cmd --permanent --add-port="${CLI_PORT}/tcp" >/dev/null - $SUDO firewall-cmd --reload >/dev/null - fi - - # 先删除主文件里的旧 Port(避免同时监听多个端口) - $SUDO sed -i -E '/^[[:space:]]*Port[[:space:]=]/d' /etc/ssh/sshd_config - set_sshd_config "Port" "$CLI_PORT" - need_restart_sshd=1 - - if [ -f "$JAIL_CONF" ] && grep -q "^\[${TARGET_JAIL}\]" "$JAIL_CONF"; then - set_f2b_conf "port" "$CLI_PORT" - need_restart_f2b=1 - fi - fi - - # 3. 禁用密码登录(危险检查) - if [ "$CLI_DISABLE_PWD" -eq 1 ]; then - if ! has_valid_pubkey || \ - [[ "$(get_sshd_config_val "PubkeyAuthentication" "yes")" != "yes" ]]; then - echo -e "${ERROR} 密钥登录不可用(未启用或无公钥),禁用密码登录将导致你无法登录!" - read -rp "确认仍然执行?(y/N): " confirm - [[ ! "$confirm" =~ ^[Yy]$ ]] && exit 1 - fi - set_sshd_config "PasswordAuthentication" "no" - set_sshd_config "ChallengeResponseAuthentication" "no" - set_sshd_config "KbdInteractiveAuthentication" "no" - need_restart_sshd=1 - fi - - # 统一重启 SSH 服务 - if [ "$need_restart_sshd" -eq 1 ]; then - if restart_sshd; then - if [ -n "$CLI_PORT" ]; then - if command -v ss &>/dev/null; then - echo -e "${INFO} 系统实际监听服务端口状态:" - ss -tulpn | grep ssh || true - fi - echo -e "${WARN} 注意:如使用的是阿里云/腾讯云/AWS等,请务必在【云服务器安全组】中开放 TCP ${CLI_PORT} 端口!" - fi - fi - fi - - # 同步重启 Fail2Ban - if [ "$need_restart_f2b" -eq 1 ]; then - if command -v fail2ban-client &>/dev/null && fail2ban-client ping >/dev/null 2>&1; then - echo -e "${INFO} 正在同步更新 Fail2Ban 防护端口..." - restart_f2b - fi - fi - - exit 0 -fi - -# ============ 交互式主菜单 ============ -while true; do - clear - show_status - echo -e " ${GREEN}1.${RESET} 密钥登录管理" - echo -e " ${GREEN}2.${RESET} 密码登录开关" - echo -e " ${GREEN}3.${RESET} Fail2Ban防护" - echo -e " ${GREEN}4.${RESET} SSH 端口修改" - echo -e " ${GREEN}0.${RESET} 退出脚本" - echo -e "${CYAN}============================================================${RESET}" - read -rp "请输入选项 [0-4]: " choice - - case "$choice" in - 1) install_key_menu ;; - 2) toggle_password_login ;; - 3) manage_fail2ban_menu ;; - 4) change_ssh_port ;; - 0) echo -e "\n感谢使用!"; exit 0 ;; - *) echo -e "${ERROR} 无效选项,请重新选择!"; sleep 1 ;; - esac -done \ No newline at end of file