From 78daf2b8cc1981f9eaf5f60097f1cb56efc57f5a Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 5 Mar 2026 19:40:30 +0800 Subject: [PATCH] init --- README.md | 58 +++++++ setup_server_security.sh | 342 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 README.md create mode 100755 setup_server_security.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..b422674 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# setup_server_security.sh + +一键为 Ubuntu/Debian 服务器配置 SSH 公钥认证并部署 fail2ban 入侵防御。 + +## 功能 + +- 自动扫描同目录下的 `.pub` 公钥文件,支持单选或全选 +- 通过指纹去重,避免重复写入 `authorized_keys` +- 安装并配置 fail2ban:SSH 登录失败 3 次永久封禁 +- 任意步骤失败自动回滚至备份状态 +- **不修改 `sshd_config`**,不禁用密码登录,不重启 sshd + +## 系统要求 + +- Ubuntu 18.04+ 或 Debian 10+ +- root 权限(`sudo`) + +## 使用方法 + +**1. 将脚本和公钥上传到同一目录** + +```bash +scp setup_server_security.sh mykey.pub user@server:~/ +``` + +**2. 登录服务器后执行** + +```bash +sudo bash setup_server_security.sh +``` + +目录下只有一个 `.pub` 文件时自动选择;多个文件时交互选择。 + +## fail2ban 配置参数 + +| 参数 | 值 | 说明 | +|---|---|---| +| `bantime` | `-1` | 永久封禁 | +| `maxretry` | `3` | 最大失败次数 | +| `findtime` | `10m` | 计数窗口 | +| `ignoreip` | `127.0.0.1/8 ::1` | 本地回环不封禁 | + +## 执行后验证 + +```bash +fail2ban-client get sshd bantime # 应输出 -1 +fail2ban-client get sshd maxretry # 应输出 3 +cat /root/.ssh/authorized_keys +``` + +## 备份与回滚 + +每次运行在 `/root/security_setup_backup_<时间戳>/` 自动备份: + +- `/etc/ssh/sshd_config` +- `/etc/fail2ban/jail.local` + +任意步骤出错时脚本自动从该目录恢复原始文件。备份目录不会自动删除,需手动清理。 diff --git a/setup_server_security.sh b/setup_server_security.sh new file mode 100755 index 0000000..b51e5c4 --- /dev/null +++ b/setup_server_security.sh @@ -0,0 +1,342 @@ +#!/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 "$@"