feat(security): integrate one-click server hardening into deploy flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-12 15:09:16 +08:00
parent 932b1762f2
commit 73d0d9d8c2
8 changed files with 382 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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`

291
scripts/security/harden-server.sh Executable file
View File

@ -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 "$@"

View File

@ -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"

View File

@ -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