Ubuntu服务器上线后,最让人头疼的不是初始配置,而是日复一日的安全状态核查。磁盘是否突然写满、是否有异常登录、防火墙规则是否被误改、系统补丁是否遗漏——这些问题单靠人工逐台检查,不仅效率低下,还极易因疲劳导致疏漏。真正落地的解决方案,是把一套自动化安全巡检脚本部署到每一台Ubuntu服务器上,让它定时运行并把报告推送到你眼前。下面直接讲具体怎么部署、怎么写脚本、怎么配置定时任务和告警。

巡检脚本的核心检查项设计

一套合格的巡检脚本,至少要覆盖五个维度:系统资源、账户安全、网络连接、服务状态和日志异常。我们不需要花哨的框架,用Bash脚本直接调用系统命令就能完成90%的检查工作。先梳理每个维度具体要查什么,以及对应的命令。

系统资源方面,重点看磁盘使用率和内存。磁盘使用率超过80%就应该告警,否则日志写满分区会导致服务崩溃。用df -h结合awk提取使用率,内存则用free -m查看可用量。CPU负载用uptime获取最近15分钟的平均值,超过CPU核心数的80%就要关注。

账户安全是重灾区。检查/etc/passwd中UID为0的账户,除root外不应存在其他特权账户。检查/etc/shadow中密码为空的账户,这类账户等同于不设防。还要检查当前系统在线用户和最近登录记录,用last命令查看过去24小时内的登录IP,如果出现陌生IP段,立即标记。

网络连接检查要关注监听端口和异常外联。ss -tlnp列出所有TCP监听端口,对照预期的服务清单,多出来的端口可能是后门程序。用netstat -antp查看已建立的连接,筛选出非业务端口的外联IP,特别是连接到境外非业务IP的情况。防火墙状态用ufw status或iptables -L -n确认规则是否生效。

关键服务状态检查根据服务器角色来定。Web服务器检查nginx或apache进程,数据库检查mysql或postgresql进程,用systemctl is-active判断服务是否运行。还要检查这些服务的日志文件大小,超过一定阈值就该轮转清理。

日志异常分析重点看认证日志/var/log/auth.log。用grep筛选"Failed password"统计暴力破解尝试次数,筛选"Accepted"统计成功登录,两者比例异常就说明有人在撞库。

完整巡检脚本编写

下面是一份可直接使用的巡检脚本,保存为/opt/scripts/security_check.sh。脚本采用模块化设计,每个检查函数独立,方便后续增删。输出格式采用Markdown,便于邮件或消息推送时保持可读性。

#!/bin/bash
# Ubuntu服务器安全巡检脚本
# 版本: 1.0
# 用法: bash security_check.sh

REPORT_FILE="/tmp/security_report_$(date +%Y%m%d_%H%M).md"
THRESHOLD_DISK=80
THRESHOLD_CPU=5
CHECK_TIME=$(date '+%Y-%m-%d %H:%M:%S')
HOSTNAME=$(hostname)
IP_ADDR=$(hostname -I | awk '{print $1}')

# 初始化报告文件
echo "# 服务器安全巡检报告" > $REPORT_FILE
echo "主机名: $HOSTNAME" >> $REPORT_FILE
echo "IP地址: $IP_ADDR" >> $REPORT_FILE
echo "检查时间: $CHECK_TIME" >> $REPORT_FILE
echo "" >> $REPORT_FILE

# 1. 系统资源检查
echo "## 1. 系统资源" >> $REPORT_FILE
echo "### 磁盘使用率" >> $REPORT_FILE
df -h | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{print $1, $5, $6}' >> $REPORT_FILE
echo "" >> $REPORT_FILE

DISK_ALERT=$(df -h | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{print $5}' | sed 's/%//' | sort -rn | head -1)
if [ "$DISK_ALERT" -gt "$THRESHOLD_DISK" ]; then
    echo "[告警] 磁盘使用率超过${THRESHOLD_DISK}%,当前最高: ${DISK_ALERT}%" >> $REPORT_FILE
fi

echo "### 内存使用" >> $REPORT_FILE
free -h | grep -E '^Mem|^Swap' >> $REPORT_FILE
echo "" >> $REPORT_FILE

echo "### CPU负载" >> $REPORT_FILE
uptime | awk -F'load average:' '{print "负载:", $2}' >> $REPORT_FILE
CPU_LOAD=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $3}' | tr -d ' ')
CPU_CORES=$(nproc)
CPU_THRESHOLD=$(echo "$CPU_CORES * 0.8" | bc)
if [ $(echo "$CPU_LOAD > $CPU_THRESHOLD" | bc) -eq 1 ]; then
    echo "[告警] CPU负载过高: $CPU_LOAD (核心数: $CPU_CORES)" >> $REPORT_FILE
fi
echo "" >> $REPORT_FILE

# 2. 账户安全检查
echo "## 2. 账户安全" >> $REPORT_FILE
echo "### UID为0的账户" >> $REPORT_FILE
awk -F: '($3 == 0) {print $1}' /etc/passwd >> $REPORT_FILE
ROOT_COUNT=$(awk -F: '($3 == 0) {print $1}' /etc/passwd | wc -l)
if [ "$ROOT_COUNT" -gt 1 ]; then
    echo "[告警] 存在多个UID为0的账户" >> $REPORT_FILE
fi
echo "" >> $REPORT_FILE

echo "### 空密码账户" >> $REPORT_FILE
EMPTY_PASS=$(awk -F: '($2 == "" || $2 == "!") {print $1}' /etc/shadow 2>/dev/null)
if [ -n "$EMPTY_PASS" ]; then
    echo "$EMPTY_PASS" >> $REPORT_FILE
    echo "[告警] 存在空密码或锁定账户" >> $REPORT_FILE
else
    echo "未发现空密码账户" >> $REPORT_FILE
fi
echo "" >> $REPORT_FILE

echo "### 最近24小时登录记录" >> $REPORT_FILE
last -n 20 | head -20 >> $REPORT_FILE
echo "" >> $REPORT_FILE

echo "### 当前登录用户" >> $REPORT_FILE
who >> $REPORT_FILE
echo "" >> $REPORT_FILE

# 3. 网络连接检查
echo "## 3. 网络连接" >> $REPORT_FILE
echo "### 监听端口" >> $REPORT_FILE
ss -tlnp | grep LISTEN | awk '{print $4, $6}' >> $REPORT_FILE
echo "" >> $REPORT_FILE

echo "### 已建立的外部连接" >> $REPORT_FILE
ss -tnp state established | grep -v '127.0.0.1' | awk '{print $4, $5, $6}' | head -20 >> $REPORT_FILE
echo "" >> $REPORT_FILE

echo "### 防火墙状态" >> $REPORT_FILE
if command -v ufw &> /dev/null; then
    ufw status verbose >> $REPORT_FILE 2>&1
else
    iptables -L -n | head -30 >> $REPORT_FILE
fi
echo "" >> $REPORT_FILE

# 4. 关键服务状态
echo "## 4. 关键服务" >> $REPORT_FILE
SERVICES=("sshd" "cron" "rsyslog" "nginx" "apache2" "mysql" "postgresql" "docker")
for svc in "${SERVICES[@]}"; do
    if systemctl list-unit-files | grep -q "$svc"; then
        STATUS=$(systemctl is-active $svc 2>/dev/null)
        echo "- $svc: $STATUS" >> $REPORT_FILE
        if [ "$STATUS" != "active" ]; then
            echo "  [告警] $svc 服务未运行" >> $REPORT_FILE
        fi
    fi
done
echo "" >> $REPORT_FILE

# 5. 日志异常分析
echo "## 5. 日志异常" >> $REPORT_FILE
echo "### 认证失败统计(过去24小时)" >> $REPORT_FILE
if [ -f /var/log/auth.log ]; then
    FAILED_COUNT=$(grep "Failed password" /var/log/auth.log | grep -v grep | wc -l)
    echo "暴力破解尝试次数: $FAILED_COUNT" >> $REPORT_FILE
    if [ "$FAILED_COUNT" -gt 50 ]; then
        echo "[告警] 暴力破解尝试过多: ${FAILED_COUNT}次" >> $REPORT_FILE
    fi
    
    echo "### 失败尝试来源IP TOP10" >> $REPORT_FILE
    grep "Failed password" /var/log/auth.log | grep -oP 'from \K\S+' | sort | uniq -c | sort -rn | head -10 >> $REPORT_FILE
fi
echo "" >> $REPORT_FILE

echo "### 近期sudo使用记录" >> $REPORT_FILE
grep "sudo" /var/log/auth.log | tail -20 >> $REPORT_FILE
echo "" >> $REPORT_FILE

echo "---" >> $REPORT_FILE
echo "*报告生成完毕*" >> $REPORT_FILE

# 输出报告路径
echo "报告已生成: $REPORT_FILE"
cat $REPORT_FILE

脚本中每个检查项都独立成段,告警阈值可根据实际环境调整。注意THRESHOLD_DISK=80和THRESHOLD_CPU=5这两个变量,如果你的服务器磁盘较小或CPU核心数少,可以适当降低阈值。脚本最后用cat输出报告内容,方便在终端直接查看,同时文件保存在/tmp目录下。

脚本部署与权限设置

脚本写好后,上传到服务器的/opt/scripts/目录。这个目录需要手动创建,/opt是存放第三方软件包的标准路径,把运维脚本集中放在这里便于管理。执行以下命令完成初始部署:

# 创建脚本目录
sudo mkdir -p /opt/scripts

# 将脚本文件移动到目录中
sudo mv security_check.sh /opt/scripts/

# 赋予执行权限
sudo chmod +x /opt/scripts/security_check.sh

# 修改所有者为root,防止被篡改
sudo chown root:root /opt/scripts/security_check.sh
sudo chmod 700 /opt/scripts/security_check.sh

权限设为700意味着只有root用户可以读取和执行这个脚本,因为脚本中涉及系统敏感信息,普通用户不应有访问权限。如果你的运维团队有多个管理员,可以创建一个专门的用户组,把权限改为750。

配置定时任务实现自动化

手动执行脚本只能算半自动化,真正的自动化需要配合cron定时任务。建议每天执行一次,时间选在业务低峰期,比如凌晨3点。编辑root用户的crontab:

sudo crontab -e

添加以下行:

0 3 * * * /bin/bash /opt/scripts/security_check.sh > /dev/null 2>&1

这行配置表示每天凌晨3点整执行脚本。> /dev/null 2>&1是把标准输出和错误输出都丢弃,因为脚本已经自己写了报告文件,不需要cron再发邮件。如果你希望cron执行后把报告内容通过系统邮件发送,可以改为:

0 3 * * * /bin/bash /opt/scripts/security_check.sh | mail -s "服务器巡检报告 - $(hostname)" admin@example.com

但这种方式依赖服务器上的邮件发送组件配置正确。更推荐的做法是结合企业微信、钉钉或飞书的Webhook机器人,把报告内容推送到即时通讯工具。

报告推送与告警集成

巡检报告生成在/tmp目录下,如果不主动查看,等于白做。最实用的推送方式是通过Webhook把报告摘要发到企业微信或钉钉群。在脚本末尾追加一段推送逻辑,利用curl发送POST请求。

以企业微信机器人为例,先在群聊中添加机器人获取Webhook地址,然后在脚本的cat $REPORT_FILE之前加入:

# 企业微信机器人推送
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的机器人key"

# 生成摘要信息
SUMMARY="## ${HOSTNAME} 巡检报告\n"
SUMMARY="${SUMMARY}时间: ${CHECK_TIME}\n"
SUMMARY="${SUMMARY}磁盘使用率最高: ${DISK_ALERT}%\n"
SUMMARY="${SUMMARY}暴力破解尝试: ${FAILED_COUNT}次\n"

# 发送消息
curl -s -X POST "$WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{
        \"msgtype\": \"markdown\",
        \"markdown\": {
            \"content\": \"${SUMMARY}\"
        }
    }" > /dev/null

钉钉和飞书的接口格式类似,只是JSON结构稍有不同。推送内容建议只发摘要,详细报告仍然保留在服务器上,需要时再登录查看。如果巡检中发现严重问题,比如磁盘使用率超过90%或发现未知监听端口,可以在脚本中增加紧急告警逻辑,立即推送高优先级消息。

多服务器批量部署方案

如果你管理着几十台Ubuntu服务器,逐台手动部署脚本显然不现实。可以用Ansible来实现批量部署。先准备一个简单的playbook文件deploy_check.yml:

---
- name: 部署安全巡检脚本
  hosts: all
  become: yes
  tasks:
    - name: 创建脚本目录
      file:
        path: /opt/scripts
        state: directory
        mode: '0755'
    
    - name: 上传巡检脚本
      copy:
        src: ./security_check.sh
        dest: /opt/scripts/security_check.sh
        mode: '0700'
        owner: root
        group: root
    
    - name: 配置定时任务
      cron:
        name: "安全巡检"
        minute: "0"
        hour: "3"
        job: "/bin/bash /opt/scripts/security_check.sh > /dev/null 2>&1"
        user: root

执行命令ansible-playbook -i inventory deploy_check.yml即可一次性完成所有服务器的部署。inventory文件里列出所有目标服务器的IP和登录凭据。如果服务器数量较多,建议分批执行,避免同时生成报告造成网络拥堵。

脚本维护与持续优化

部署完成不代表一劳永逸。安全巡检脚本需要随着业务变化持续迭代。比如新上线了Redis服务,就要在服务检查列表里加上redis-server;发现某类攻击特征明显,就要在日志分析里增加对应的匹配规则。建议把巡检脚本纳入版本管理,用Git跟踪每次修改,这样回滚和分发都方便。

另外要注意脚本本身的性能消耗。巡检脚本运行时会有短暂的CPU和IO开销,在配置较低的云服务器上,如果巡检时间恰好和业务高峰重叠,可能会造成影响。可以通过nice -n 19降低脚本进程优先级,或者在cron里把执行时间调整到凌晨4点以后。如果服务器上有监控系统如Prometheus,巡检脚本的部分检查项可以和监控指标互补,避免重复劳动。

日志文件轮转也要考虑到。脚本生成的报告文件会随时间累积,可以在脚本开头加一段清理逻辑,自动删除7天前的旧报告:

# 清理7天前的旧报告
find /tmp -name "security_report_*.md" -mtime +7 -delete

这样/tmp目录不会因为报告堆积而占满空间。整套方案从脚本编写、权限加固、定时调度、告警推送到批量部署,形成了一个完整的闭环。你只需要在首次部署时花半小时配置好,后续每天花一分钟扫一眼推送消息,就能对服务器安全状态心中有数。