ReplaceKey/setup_server_security.sh
2026-03-05 19:40:30 +08:00

343 lines
14 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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=3findtime=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 "$@"