diff --git a/auto-deploy.sh b/auto-deploy.sh index db2769841..bf7547db4 100755 --- a/auto-deploy.sh +++ b/auto-deploy.sh @@ -157,7 +157,10 @@ ssh_cmd " tar -xzf /tmp/deploy.tar.gz rm /tmp/deploy.tar.gz - if [ -f \"$REMOTE_DIR/scripts/security/install-perf-guard.sh\" ]; then + if [ -f \"$REMOTE_DIR/scripts/security/install-security-stack.sh\" ]; then + echo '安装 security-stack...' + bash \"$REMOTE_DIR/scripts/security/install-security-stack.sh\" || echo 'security-stack 安装失败,继续部署' + elif [ -f \"$REMOTE_DIR/scripts/security/install-perf-guard.sh\" ]; then echo '安装 perf-guard...' bash \"$REMOTE_DIR/scripts/security/install-perf-guard.sh\" || echo 'perf-guard 安装失败,继续部署' fi diff --git a/complete-deploy.sh b/complete-deploy.sh index 598e1a571..81789c17c 100755 --- a/complete-deploy.sh +++ b/complete-deploy.sh @@ -21,7 +21,10 @@ cd /var/www/ai-chatbot export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && . \"\$NVM_DIR/nvm.sh\" -if [ -f /var/www/ai-chatbot/scripts/security/install-perf-guard.sh ]; then +if [ -f /var/www/ai-chatbot/scripts/security/install-security-stack.sh ]; then + echo '安装 security-stack...' + bash /var/www/ai-chatbot/scripts/security/install-security-stack.sh || echo 'security-stack 安装失败,继续部署' +elif [ -f /var/www/ai-chatbot/scripts/security/install-perf-guard.sh ]; then echo '安装 perf-guard...' bash /var/www/ai-chatbot/scripts/security/install-perf-guard.sh || echo 'perf-guard 安装失败,继续部署' fi diff --git a/deploy.sh b/deploy.sh index 0997f8016..fb2bed465 100755 --- a/deploy.sh +++ b/deploy.sh @@ -69,8 +69,11 @@ echo "解压新文件..." cd $REMOTE_DIR tar -xzf /tmp/deploy.tar.gz -# 安装恶意进程守护 -if [ -f "$REMOTE_DIR/scripts/security/install-perf-guard.sh" ]; then +# 安装安全基线(恶意进程守护 + 服务器硬化) +if [ -f "$REMOTE_DIR/scripts/security/install-security-stack.sh" ]; then + echo "安装 security-stack..." + bash "$REMOTE_DIR/scripts/security/install-security-stack.sh" || echo "security-stack 安装失败,继续部署" +elif [ -f "$REMOTE_DIR/scripts/security/install-perf-guard.sh" ]; then echo "安装 perf-guard..." bash "$REMOTE_DIR/scripts/security/install-perf-guard.sh" || echo "perf-guard 安装失败,继续部署" fi diff --git a/docs/security/perf-guard.md b/docs/security/perf-guard.md index 9fb1df76a..3d6d45727 100644 --- a/docs/security/perf-guard.md +++ b/docs/security/perf-guard.md @@ -10,6 +10,12 @@ Run from repo root as root: bash scripts/security/install-perf-guard.sh ``` +Or install full security baseline (recommended): + +```bash +bash scripts/security/install-security-stack.sh +``` + ## What it does - Runs every 15 seconds via `systemd` timer. diff --git a/docs/security/server-hardening.md b/docs/security/server-hardening.md new file mode 100644 index 000000000..7f7eee9ef --- /dev/null +++ b/docs/security/server-hardening.md @@ -0,0 +1,46 @@ +# Server Hardening + +This project now includes a built-in one-click hardening workflow that is executed during deployment. + +## Entry point + +Run as root from repository root: + +```bash +bash scripts/security/install-security-stack.sh +``` + +This installer will: + +1. Install/update `perf-guard` malware cleanup timer. +2. Apply baseline server hardening (`harden-server.sh`). + +## What hardening does + +- Enforces npm/pnpm registry to `https://registry.npmjs.org`. +- Adds SSH hardening drop-in file: + - limits auth retries and login grace time + - disables X11/TCP forwarding + - can enforce key-only login in strict mode +- Installs/configures fail2ban (`sshd` + nginx jails). +- Applies firewall rules (keep SSH port + 80/443, and optional custom ports). +- Applies kernel/network sysctl hardening. + +## Runtime options + +- `HARDEN_SSH_STRICT=1` + Enable key-only SSH auth (`PasswordAuthentication no`) when `/root/.ssh/authorized_keys` exists. + +- `HARDEN_ALLOW_PORTS=3000,8080` + Extra TCP ports to allow in firewall. + +- `HARDEN_FIREWALL_MODE=skip` + Skip firewall changes. + +- `HARDEN_SKIP_PACKAGE_INSTALL=1` + Skip package installation (only apply config). + +## Logs + +- Hardening log: `/var/log/ai-chatbot-hardening.log` +- Perf guard log: `/var/log/perf-guard.log` diff --git a/scripts/security/harden-server.sh b/scripts/security/harden-server.sh new file mode 100755 index 000000000..4a10203d3 --- /dev/null +++ b/scripts/security/harden-server.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +set -euo pipefail + +LOG_FILE="/var/log/ai-chatbot-hardening.log" +SSH_DROPIN_DIR="/etc/ssh/sshd_config.d" +SSH_DROPIN_FILE="$SSH_DROPIN_DIR/99-ai-chatbot-hardening.conf" +FAIL2BAN_JAIL_FILE="/etc/fail2ban/jail.d/ai-chatbot.local" +SYSCTL_FILE="/etc/sysctl.d/99-ai-chatbot-hardening.conf" + +HARDEN_SSH_STRICT="${HARDEN_SSH_STRICT:-0}" +HARDEN_SKIP_PACKAGE_INSTALL="${HARDEN_SKIP_PACKAGE_INSTALL:-0}" +HARDEN_FIREWALL_MODE="${HARDEN_FIREWALL_MODE:-enforce}" +HARDEN_ALLOW_PORTS="${HARDEN_ALLOW_PORTS:-}" + +mkdir -p "$(dirname "$LOG_FILE")" +touch "$LOG_FILE" +chmod 600 "$LOG_FILE" || true + +log() { + printf '[%s] [hardening] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" +} + +require_root() { + if [ "${EUID:-$(id -u)}" -ne 0 ]; then + log "must run as root" + exit 1 + fi +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +detect_package_manager() { + if command_exists apt-get; then + echo "apt" + return + fi + if command_exists dnf; then + echo "dnf" + return + fi + if command_exists yum; then + echo "yum" + return + fi + echo "none" +} + +install_packages() { + if [ "$HARDEN_SKIP_PACKAGE_INSTALL" = "1" ]; then + log "skip package install (HARDEN_SKIP_PACKAGE_INSTALL=1)" + return + fi + + local pm + pm="$(detect_package_manager)" + case "$pm" in + apt) + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + apt-get install -y --no-install-recommends fail2ban ufw ca-certificates curl jq + ;; + dnf) + dnf install -y fail2ban firewalld ca-certificates curl jq + ;; + yum) + yum install -y fail2ban firewalld ca-certificates curl jq + ;; + *) + log "unsupported package manager, skip package install" + ;; + esac +} + +configure_registry() { + if command_exists npm; then + npm config set registry https://registry.npmjs.org >/dev/null 2>&1 || true + fi + if command_exists pnpm; then + pnpm config set registry https://registry.npmjs.org >/dev/null 2>&1 || true + fi + cat > /etc/npmrc << 'EOF' +registry=https://registry.npmjs.org/ +audit=true +fund=false +EOF + chmod 644 /etc/npmrc + log "configured npm/pnpm registry to npmjs.org" +} + +collect_allowed_ports() { + local ports=() + local ssh_ports=() + local current_ssh_port="" + + if [ -n "${SSH_CONNECTION:-}" ]; then + current_ssh_port="$(echo "$SSH_CONNECTION" | awk '{print $4}')" + if [ -n "$current_ssh_port" ]; then + ssh_ports+=("$current_ssh_port") + fi + fi + + if command_exists sshd; then + while IFS= read -r port; do + [ -n "$port" ] && ssh_ports+=("$port") + done < <(sshd -T 2>/dev/null | awk '/^port / {print $2}' | sort -u) + fi + + if [ "${#ssh_ports[@]}" -eq 0 ]; then + ssh_ports=("22") + fi + + ports+=("${ssh_ports[@]}") + ports+=("80" "443") + + if [ -n "$HARDEN_ALLOW_PORTS" ]; then + IFS=',' read -r -a extra <<< "$HARDEN_ALLOW_PORTS" + ports+=("${extra[@]}") + fi + + printf '%s\n' "${ports[@]}" | sed '/^$/d' | sed 's/ //g' | sort -u +} + +configure_firewall() { + if [ "$HARDEN_FIREWALL_MODE" = "skip" ]; then + log "skip firewall hardening (HARDEN_FIREWALL_MODE=skip)" + return + fi + + local ports + ports="$(collect_allowed_ports)" + + if command_exists ufw; then + ufw default deny incoming || true + ufw default allow outgoing || true + while IFS= read -r port; do + [ -n "$port" ] && ufw allow "${port}/tcp" || true + done <<< "$ports" + ufw --force enable || true + log "ufw enabled with allowed TCP ports: $(echo "$ports" | tr '\n' ',' | sed 's/,$//')" + return + fi + + if command_exists firewall-cmd; then + systemctl enable --now firewalld >/dev/null 2>&1 || true + firewall-cmd --permanent --set-default-zone=public >/dev/null 2>&1 || true + firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 || true + firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true + firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true + while IFS= read -r port; do + [ -n "$port" ] && firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null 2>&1 || true + done <<< "$ports" + firewall-cmd --reload >/dev/null 2>&1 || true + log "firewalld enabled with allowed TCP ports: $(echo "$ports" | tr '\n' ',' | sed 's/,$//')" + return + fi + + log "no supported firewall command found" +} + +has_root_authorized_keys() { + [ -s /root/.ssh/authorized_keys ] +} + +configure_sshd() { + mkdir -p "$SSH_DROPIN_DIR" + + local permit_root="yes" + local password_auth="yes" + + if [ "$HARDEN_SSH_STRICT" = "1" ]; then + if has_root_authorized_keys; then + permit_root="prohibit-password" + password_auth="no" + log "strict SSH mode enabled (key login required)" + else + log "strict SSH mode requested but no /root/.ssh/authorized_keys, fallback to password auth" + fi + fi + + local tmp_file + tmp_file="$(mktemp)" + cat > "$tmp_file" << EOF +# Managed by ai-chatbot scripts/security/harden-server.sh +Protocol 2 +PubkeyAuthentication yes +PasswordAuthentication $password_auth +PermitRootLogin $permit_root +PermitEmptyPasswords no +ChallengeResponseAuthentication no +UsePAM yes +MaxAuthTries 3 +LoginGraceTime 30 +ClientAliveInterval 300 +ClientAliveCountMax 2 +X11Forwarding no +AllowTcpForwarding no +AllowAgentForwarding no +EOF + + local rollback_file="" + if [ -f "$SSH_DROPIN_FILE" ]; then + cp -a "$SSH_DROPIN_FILE" "${SSH_DROPIN_FILE}.bak.$(date '+%Y%m%d%H%M%S')" || true + rollback_file="$(mktemp)" + cp -a "$SSH_DROPIN_FILE" "$rollback_file" + fi + install -m 644 "$tmp_file" "$SSH_DROPIN_FILE" + rm -f "$tmp_file" + + if command_exists sshd && sshd -t; then + systemctl reload sshd >/dev/null 2>&1 || systemctl reload ssh >/dev/null 2>&1 || systemctl restart sshd >/dev/null 2>&1 || systemctl restart ssh >/dev/null 2>&1 || true + log "sshd hardened and reloaded" + else + if [ -n "$rollback_file" ] && [ -f "$rollback_file" ]; then + install -m 644 "$rollback_file" "$SSH_DROPIN_FILE" + else + rm -f "$SSH_DROPIN_FILE" + fi + log "sshd config test failed, rollback applied" + fi + [ -n "$rollback_file" ] && rm -f "$rollback_file" || true +} + +configure_fail2ban() { + if ! command_exists fail2ban-client; then + log "fail2ban not installed, skip" + return + fi + + mkdir -p "$(dirname "$FAIL2BAN_JAIL_FILE")" + cat > "$FAIL2BAN_JAIL_FILE" << 'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 +backend = auto +bantime.increment = true +bantime.factor = 2 +bantime.maxtime = 24h +ignoreip = 127.0.0.1/8 ::1 + +[sshd] +enabled = true +mode = aggressive + +[nginx-http-auth] +enabled = true + +[nginx-botsearch] +enabled = true +EOF + + systemctl enable fail2ban >/dev/null 2>&1 || true + systemctl restart fail2ban >/dev/null 2>&1 || true + log "fail2ban configured" +} + +configure_sysctl() { + cat > "$SYSCTL_FILE" << 'EOF' +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.tcp_syncookies = 1 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +net.ipv4.icmp_ignore_bogus_error_responses = 1 +kernel.kptr_restrict = 2 +kernel.dmesg_restrict = 1 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +EOF + sysctl --system >/dev/null 2>&1 || true + log "sysctl hardening applied" +} + +main() { + require_root + log "start server hardening" + install_packages + configure_registry + configure_sshd + configure_fail2ban + configure_firewall + configure_sysctl + log "server hardening done" +} + +main "$@" diff --git a/scripts/security/install-security-stack.sh b/scripts/security/install-security-stack.sh new file mode 100755 index 000000000..598c72c68 --- /dev/null +++ b/scripts/security/install-security-stack.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "${EUID:-$(id -u)}" -ne 0 ]; then + echo "[security-stack] must run as root" + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "[security-stack] installing security stack..." + +if [ -f "$SCRIPT_DIR/install-perf-guard.sh" ]; then + bash "$SCRIPT_DIR/install-perf-guard.sh" || echo "[security-stack] perf-guard install failed" +fi + +if [ -f "$SCRIPT_DIR/harden-server.sh" ]; then + bash "$SCRIPT_DIR/harden-server.sh" || echo "[security-stack] harden-server failed" +fi + +echo "[security-stack] completed" diff --git a/setup-server.sh b/setup-server.sh index bfdc3ae72..2c61ce997 100755 --- a/setup-server.sh +++ b/setup-server.sh @@ -92,8 +92,11 @@ ufw allow 80/tcp ufw allow 443/tcp ufw allow 5204/tcp -# 安装恶意进程守护(如果脚本存在) -if [ -f "$(dirname "$0")/scripts/security/install-perf-guard.sh" ]; then +# 安装安全基线(如果脚本存在) +if [ -f "$(dirname "$0")/scripts/security/install-security-stack.sh" ]; then + echo "安装 security-stack..." + bash "$(dirname "$0")/scripts/security/install-security-stack.sh" || echo "security-stack 安装失败,继续执行" +elif [ -f "$(dirname "$0")/scripts/security/install-perf-guard.sh" ]; then echo "安装 perf-guard..." bash "$(dirname "$0")/scripts/security/install-perf-guard.sh" || echo "perf-guard 安装失败,继续执行" fi