#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKUP_DIR="/root/security_setup_backup_$(date +%Y%m%d_%H%M%S)" ROLLBACK_NEEDED=false SELECTED_PUBKEYS=() # ───────────────────────────────────────────── # 错误回滚机制 # ───────────────────────────────────────────── cleanup_on_error() { local exit_code=$? # EXIT 陷阱在正常退出时也会触发,仅在出错时回滚 if [[ $exit_code -ne 0 && "$ROLLBACK_NEEDED" == "true" ]]; then echo "" echo "[ROLLBACK] 检测到错误,正在恢复备份..." >&2 if [[ -f "$BACKUP_DIR/sshd_config" ]]; then cp "$BACKUP_DIR/sshd_config" /etc/ssh/sshd_config echo "[ROLLBACK] 已恢复 /etc/ssh/sshd_config" >&2 fi if [[ -f "$BACKUP_DIR/jail.local" ]]; then cp "$BACKUP_DIR/jail.local" /etc/fail2ban/jail.local echo "[ROLLBACK] 已恢复 /etc/fail2ban/jail.local" >&2 elif [[ -f "$BACKUP_DIR/jail.local.absent" ]]; then rm -f /etc/fail2ban/jail.local echo "[ROLLBACK] 已移除新创建的 /etc/fail2ban/jail.local" >&2 fi echo "[ROLLBACK] 回滚完成。备份文件保留在: $BACKUP_DIR" >&2 fi } trap cleanup_on_error ERR EXIT # ───────────────────────────────────────────── # 步骤 2:前置检查 # ───────────────────────────────────────────── check_prerequisites() { echo "==> [1/6] 前置检查..." # 检查 root 权限 if [[ $EUID -ne 0 ]]; then echo "[ERROR] 此脚本必须以 root 权限运行,请使用: sudo bash $0" >&2 exit 1 fi # 检查 OS if [[ ! -f /etc/os-release ]]; then echo "[ERROR] 无法检测操作系统,/etc/os-release 不存在" >&2 exit 1 fi source /etc/os-release if [[ "$ID" != "ubuntu" && "$ID" != "debian" && "$ID_LIKE" != *"debian"* ]]; then echo "[ERROR] 此脚本仅支持 Ubuntu/Debian 系统,当前系统: ${PRETTY_NAME:-未知}" >&2 exit 1 fi echo " 操作系统: ${PRETTY_NAME}" # 检查必要命令 local missing=() for cmd in ssh-keygen systemctl apt-get; do if ! command -v "$cmd" &>/dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then echo "[ERROR] 缺少必要命令: ${missing[*]}" >&2 exit 1 fi echo " 前置检查通过。" } # ───────────────────────────────────────────── # 步骤 3:公钥文件发现 # ───────────────────────────────────────────── find_pubkeys() { echo "==> [2/6] 扫描公钥文件..." local pubkey_files=() while IFS= read -r -d '' f; do pubkey_files+=("$f") done < <(find "$SCRIPT_DIR" -maxdepth 1 -name "*.pub" -print0 2>/dev/null | sort -z) if [[ ${#pubkey_files[@]} -eq 0 ]]; then echo "[ERROR] 未找到任何 .pub 公钥文件。" >&2 echo " 请将公钥文件(如 mykey.pub)放置在脚本同一目录下: $SCRIPT_DIR" >&2 exit 1 elif [[ ${#pubkey_files[@]} -eq 1 ]]; then echo " 找到 1 个公钥文件: $(basename "${pubkey_files[0]}")" echo " 自动选择该文件。" SELECTED_PUBKEYS=("${pubkey_files[0]}") else echo " 找到 ${#pubkey_files[@]} 个公钥文件:" local i=1 for f in "${pubkey_files[@]}"; do printf " [%d] %s\n" "$i" "$(basename "$f")" ((i++)) done echo " [A] 添加全部" echo "" while true; do read -rp " 请选择 [1-${#pubkey_files[@]}/A]: " choice choice="${choice^^}" # 转大写 if [[ "$choice" == "A" ]]; then SELECTED_PUBKEYS=("${pubkey_files[@]}") echo " 已选择全部 ${#pubkey_files[@]} 个公钥文件。" break elif [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#pubkey_files[@]} )); then SELECTED_PUBKEYS=("${pubkey_files[$((choice - 1))]}") echo " 已选择: $(basename "${SELECTED_PUBKEYS[0]}")" break else echo " 无效选择,请重新输入。" fi done fi } # ───────────────────────────────────────────── # 步骤 4:备份 # ───────────────────────────────────────────── create_backups() { echo "==> [3/6] 创建备份..." mkdir -p "$BACKUP_DIR" if [[ -f /etc/ssh/sshd_config ]]; then cp /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config" echo " 已备份: /etc/ssh/sshd_config" fi if [[ -f /etc/fail2ban/jail.local ]]; then cp /etc/fail2ban/jail.local "$BACKUP_DIR/jail.local" echo " 已备份: /etc/fail2ban/jail.local" else # 标记文件原本不存在,回滚时需删除 touch "$BACKUP_DIR/jail.local.absent" fi ROLLBACK_NEEDED=true echo " 备份目录: $BACKUP_DIR" } # ───────────────────────────────────────────── # 步骤 5:配置公钥认证 # ───────────────────────────────────────────── configure_pubkey_auth() { echo "==> [4/6] 配置公钥认证..." local ssh_dir="/root/.ssh" local auth_keys="$ssh_dir/authorized_keys" # 确保目录和文件存在,权限正确 mkdir -p "$ssh_dir" chmod 700 "$ssh_dir" touch "$auth_keys" chmod 600 "$auth_keys" local added=0 local skipped=0 for pubkey_file in "${SELECTED_PUBKEYS[@]}"; do echo " 处理: $(basename "$pubkey_file")" # 验证公钥格式 if ! ssh-keygen -l -f "$pubkey_file" &>/dev/null; then echo " [WARNING] 文件格式无效,跳过: $(basename "$pubkey_file")" >&2 ((skipped++)) || true continue fi # 提取新公钥的指纹 local new_fingerprint new_fingerprint="$(ssh-keygen -l -f "$pubkey_file" | awk '{print $2}')" # 去重检查:遍历 authorized_keys 中每行,比较指纹 local duplicate=false while IFS= read -r existing_line || [[ -n "$existing_line" ]]; do [[ -z "$existing_line" || "$existing_line" == \#* ]] && continue local tmp_file tmp_file="$(mktemp /tmp/pubkey_check.XXXXXX)" echo "$existing_line" > "$tmp_file" local existing_fp existing_fp="$(ssh-keygen -l -f "$tmp_file" 2>/dev/null | awk '{print $2}')" || true rm -f "$tmp_file" if [[ "$existing_fp" == "$new_fingerprint" ]]; then duplicate=true break fi done < "$auth_keys" if [[ "$duplicate" == "true" ]]; then echo " [SKIP] 公钥已存在(指纹: $new_fingerprint),跳过。" ((skipped++)) || true else cat "$pubkey_file" >> "$auth_keys" echo " [OK] 已添加公钥,指纹: $new_fingerprint" ((added++)) || true fi done echo " 公钥配置完成:添加 $added 个,跳过 $skipped 个。" } # ───────────────────────────────────────────── # 步骤 6:安装 fail2ban # ───────────────────────────────────────────── install_fail2ban() { echo "==> [5/6] 安装 fail2ban..." if command -v fail2ban-client &>/dev/null; then echo " fail2ban 已安装,跳过安装步骤。" else echo " 正在安装 fail2ban..." apt-get update -qq apt-get install -y fail2ban echo " fail2ban 安装完成。" fi systemctl enable fail2ban echo " fail2ban 已设置为开机自启。" } # ───────────────────────────────────────────── # 步骤 7:配置 fail2ban # ───────────────────────────────────────────── configure_fail2ban() { echo "==> [6/6] 配置 fail2ban..." cat > /etc/fail2ban/jail.local << 'EOF' # 由 setup_server_security.sh 生成 [DEFAULT] bantime = -1 findtime = 10m maxretry = 3 banaction = iptables-multiport ignoreip = 127.0.0.1/8 ::1 [sshd] enabled = true port = ssh filter = sshd logpath = %(sshd_log)s backend = %(sshd_backend)s maxretry = 3 bantime = -1 EOF echo " 已写入 /etc/fail2ban/jail.local" echo " 参数:bantime=-1(永久封禁),maxretry=3,findtime=10m" } # ───────────────────────────────────────────── # 步骤 9:服务重启与验证 # ───────────────────────────────────────────── verify_and_report() { echo "" echo "==> 重启 fail2ban 并验证..." systemctl restart fail2ban # 验证服务状态 local f2b_status f2b_status="$(systemctl is-active fail2ban 2>/dev/null || true)" if [[ "$f2b_status" != "active" ]]; then echo "[ERROR] fail2ban 服务未能正常启动(状态: $f2b_status)" >&2 exit 1 fi # 验证 jail 激活(等待服务稳定) sleep 2 local jail_status jail_status="$(fail2ban-client status sshd 2>&1)" || true # 验证 authorized_keys 权限 local ak_perm ssh_perm ak_perm="$(stat -c "%a" /root/.ssh/authorized_keys 2>/dev/null || echo "?")" ssh_perm="$(stat -c "%a" /root/.ssh 2>/dev/null || echo "?")" # ── 汇总报告 ────────────────────────────── echo "" echo "╔══════════════════════════════════════════════════════════╗" echo "║ SSH 安全配置完成 - 汇总报告 ║" echo "╠══════════════════════════════════════════════════════════╣" echo "║ [公钥认证] ║" printf "║ %-54s ║\n" "authorized_keys 路径: /root/.ssh/authorized_keys" printf "║ %-54s ║\n" "目录权限: $ssh_perm 文件权限: $ak_perm" echo "║ 已添加公钥指纹: ║" while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] && continue local tmp; tmp="$(mktemp /tmp/fp_report.XXXXXX)" echo "$line" > "$tmp" local fp; fp="$(ssh-keygen -l -f "$tmp" 2>/dev/null | awk '{print $2, $4}')" || fp="(无法解析)" rm -f "$tmp" printf "║ %-52s ║\n" "$fp" done < /root/.ssh/authorized_keys echo "║ ║" echo "║ [fail2ban] ║" printf "║ %-54s ║\n" "服务状态: $f2b_status" printf "║ %-54s ║\n" "配置文件: /etc/fail2ban/jail.local" printf "║ %-54s ║\n" "bantime=-1(永久封禁) maxretry=3 findtime=10m" echo "║ ║" echo "║ [未修改项] ║" printf "║ %-54s ║\n" "SSH 端口: 保持 22 不变" printf "║ %-54s ║\n" "PasswordAuthentication: 未禁用(保留密码登录)" printf "║ %-54s ║\n" "sshd_config: 未修改" echo "║ ║" printf "║ %-54s ║\n" "备份位置: $BACKUP_DIR" echo "╚══════════════════════════════════════════════════════════╝" echo "" echo "fail2ban-client status sshd 输出:" echo "$jail_status" echo "" echo "手动验证命令:" echo " fail2ban-client get sshd bantime # 应输出 -1" echo " fail2ban-client get sshd maxretry # 应输出 3" echo " cat /root/.ssh/authorized_keys" } # ───────────────────────────────────────────── # 主流程 # ───────────────────────────────────────────── main() { echo "" echo "======================================================" echo " SSH 安全配置自动化脚本" echo "======================================================" echo "" check_prerequisites find_pubkeys create_backups configure_pubkey_auth install_fail2ban configure_fail2ban verify_and_report # 正常退出,禁用回滚 ROLLBACK_NEEDED=false } main "$@"