Skip to content

17|系统初始化基线

运维管几十台机器时会发现一个麻烦:每台机器状态都不一样,有的时区对、有的不对,有的装了某工具、有的没装,日志时间对不上、监控采集报错。两台机器出问题想对比,发现环境差异太大没法比。

解决办法是给每台新机器定一个统一标准——主机名、时区、时间同步、软件源、SSH 加固、防火墙、内核参数……一套配齐,让任意一台机器经过初始化后状态一致。这套统一标准就是初始化基线。

本篇把前面讲过的各项配置整合成一个可直接用的初始化脚本,再补充基线漂移检测和批量推送。

一、基线分类与覆盖范围

按职责分,初始化基线通常覆盖以下分类。不同公司会把安全审计、账号集中管理、监控 Agent 接入也纳入基线,这里覆盖操作系统层面的核心项:

分类配置项解决什么问题章节引用
主机身份hostname、/etc/hosts批量管理、日志识别、集群节点解析第 1 讲
网络解析DNS、NSS 顺序软件安装、域名访问、内网服务解析第 12 讲第 13 讲
时间同步时区、chrony、硬件时钟日志排序、证书校验、集群一致性第 1 讲
软件环境软件源、EPEL、基础工具安装与排障命令立即可用第 9 讲
登录访问SSH、sudo、密钥、PAM远程管理入口安全第 16 讲
网络策略iptables / firewalld、ipset控制端口暴露范围第 15 讲
内核参数sysctl、模块加载网络栈调优、安全收紧、特性启用第 16 讲
资源限制ulimit、systemd Limit、cgroup文件句柄、进程数、内存上限第 7 讲
存储基础swap、fstab、独立挂载重启后挂载稳定、关键目录隔离第 8 讲
日志审计journald 持久化、logrotate、auditd保留操作和故障现场第 11 讲第 16 讲

二、基线脚本的设计原则

写初始化基线脚本之前,先确立几个原则——这些原则决定脚本能否在生产环境长期可用:

1. 幂等性

幂等指脚本可以反复执行,每次执行后系统状态相同,不会因为重复执行而出错。生产场景中,基线脚本经常需要在已经部分初始化的机器上跑第二次、第三次——做修补、补漏、应用新规范。每个动作之前先检查"目标状态是否已经达到",才执行:

bash
# 非幂等(每次都执行,审计日志反复刷)
timedatectl set-timezone Asia/Shanghai

# 幂等(状态不对才执行)
current_tz=$(timedatectl show -p Timezone --value)
if [ "$current_tz" != "Asia/Shanghai" ]; then
    timedatectl set-timezone Asia/Shanghai
fi

2. 可追溯

每个动作输出日志,记录"做了什么、原因、时间戳"。事后追溯某台机器为什么是这个状态,从日志就能复盘:

bash
log() {
    echo "[$(date '+%F %T')] $*" | tee -a /var/log/init-baseline.log
}

log "[INFO] 设置时区为 Asia/Shanghai (原值: $current_tz)"

3. 失败立即停止

set -euo pipefail 让脚本在任意命令失败时立即退出。基线脚本不能"前面失败了后面还继续硬装"——失败时立刻停下,人工介入决定是修复还是回滚:

bash
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

4. 可参数化

主机名、IP、内网网段、NTP 服务器、SSH 端口、外发邮件地址……所有可变项作为参数或环境变量传入,不要硬编码:

bash
HOSTNAME="${1:?usage: $0 <hostname>}"
NTP_SERVER="${NTP_SERVER:-ntp.aliyun.com}"
SSH_ALLOW_CIDR="${SSH_ALLOW_CIDR:-10.0.0.0/8}"

5. 分模块

基线脚本随系统演进会越来越长,按功能拆成模块,单独可调用。某次只需要重做时间配置时,运行对应模块就够,不需要跑整个基线:

bash
init-baseline.sh         # 主入口
  modules/
    01-hostname.sh
    02-time.sh
    03-network.sh
    04-software.sh
    05-ssh.sh
    06-firewall.sh
    07-sysctl.sh
    08-limits.sh
    09-storage.sh
    10-logging.sh

三、完整的初始化基线脚本

下面是一份生产可用的基线脚本骨架,以 RHEL 9 / Rocky 9 为目标,实际部署时根据公司需求调整。

bash
#!/bin/bash
# /usr/local/sbin/init-baseline.sh
# 系统初始化基线,幂等执行
#
# 使用:
#   init-baseline.sh <hostname>
#
# 环境变量:
#   TIMEZONE       — 时区,默认 Asia/Shanghai
#   NTP_SERVERS    — NTP 服务器列表(空格分隔),默认公网
#   SSH_PORT       — SSH 端口,默认 22
#   SSH_ALLOW_CIDR — 允许 SSH 的网段,默认 10.0.0.0/8
#   ENABLE_SWAP    — 是否启用 swap,默认 false(K8s 节点必须关闭)

set -euo pipefail
IFS=$'\n\t'

# === 参数与默认值 ===
HOSTNAME="${1:?Usage: $0 <hostname>}"
TIMEZONE="${TIMEZONE:-Asia/Shanghai}"
NTP_SERVERS="${NTP_SERVERS:-ntp.aliyun.com time1.cloud.tencent.com}"
SSH_PORT="${SSH_PORT:-22}"
SSH_ALLOW_CIDR="${SSH_ALLOW_CIDR:-10.0.0.0/8}"
ENABLE_SWAP="${ENABLE_SWAP:-false}"

LOG_FILE="/var/log/init-baseline.log"
mkdir -p "$(dirname "$LOG_FILE")"

log() {
    echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"
}

require_root() {
    if [ "$EUID" -ne 0 ]; then
        echo "必须以 root 身份运行" >&2
        exit 1
    fi
}

require_root
log "=== 开始基线初始化 ==="

# === 1. 主机名 ===
log "[1/10] 主机名配置"
current_hostname=$(hostname)
if [ "$current_hostname" != "$HOSTNAME" ]; then
    hostnamectl set-hostname "$HOSTNAME"
    log "  主机名: $current_hostname -> $HOSTNAME"
fi
# 同步 /etc/hosts
if ! grep -q "^127\.0\.1\.1\s*$HOSTNAME" /etc/hosts; then
    sed -i '/^127\.0\.1\.1/d' /etc/hosts
    echo "127.0.1.1   $HOSTNAME" >> /etc/hosts
    log "  /etc/hosts 已同步"
fi

# === 1b. 云主机防止主机名被 cloud-init 重置 ===
# 部分云厂商的 cloud-init 会在每次重启时按元数据重置主机名,覆盖手工设置。
# 表现是 hostnamectl set-hostname 改完当时生效,重启后又变回云厂商默认名。
if [ -f /etc/cloud/cloud.cfg ]; then
    if ! grep -q "^preserve_hostname: true" /etc/cloud/cloud.cfg.d/99-disable-hostname.cfg 2>/dev/null; then
        cat > /etc/cloud/cloud.cfg.d/99-disable-hostname.cfg <<'EOF'
preserve_hostname: true
EOF
        log "  cloud-init hostname 模块已禁用"
    fi
fi

# === 2. 时区与时间同步 ===
log "[2/10] 时区与时间同步"
current_tz=$(timedatectl show -p Timezone --value)
if [ "$current_tz" != "$TIMEZONE" ]; then
    timedatectl set-timezone "$TIMEZONE"
    log "  时区: $current_tz -> $TIMEZONE"
fi

# 安装 chrony
if ! command -v chronyc &>/dev/null; then
    if command -v yum &>/dev/null; then
        yum install -y chrony
    elif command -v apt &>/dev/null; then
        apt update && apt install -y chrony
    fi
fi

# 配置 NTP 源
CHRONY_CONF="/etc/chrony.conf"
[ -f /etc/chrony/chrony.conf ] && CHRONY_CONF="/etc/chrony/chrony.conf"
if ! grep -q "^server $(echo $NTP_SERVERS | awk '{print $1}')" "$CHRONY_CONF"; then
    cp -a "$CHRONY_CONF" "$CHRONY_CONF.bak.$(date +%s)"
    sed -i '/^server\|^pool/d' "$CHRONY_CONF"
    for ntp in $NTP_SERVERS; do
        echo "server $ntp iburst" >> "$CHRONY_CONF"
    done
    log "  NTP 源已更新: $NTP_SERVERS"
fi

# 启动 chrony(RHEL chronyd / Debian chrony)
for svc in chronyd chrony; do
    if systemctl list-unit-files "$svc.service" &>/dev/null; then
        systemctl enable --now "$svc"
        log "  $svc 已启用"
        break
    fi
done

# === 3. 软件源与基础工具 ===
log "[3/10] 软件源与基础工具"
if command -v yum &>/dev/null; then
    yum install -y epel-release 2>/dev/null || true
    yum install -y vim curl wget tar unzip lsof net-tools bind-utils \
                    bash-completion tcpdump traceroute mtr \
                    htop iotop sysstat psmisc nc 2>/dev/null || true
elif command -v apt &>/dev/null; then
    apt update
    apt install -y vim curl wget tar unzip lsof net-tools dnsutils \
                    bash-completion tcpdump traceroute mtr \
                    htop iotop sysstat psmisc netcat-openbsd 2>/dev/null || true
fi
log "  基础工具已安装"

# === 4. journald 持久化 ===
log "[4/10] journald 持久化"
if [ ! -d /var/log/journal ]; then
    mkdir -p /var/log/journal
    systemctl restart systemd-journald
    log "  journald 持久化已启用"
fi

# 限制 journald 大小
JOURNALD_CONF="/etc/systemd/journald.conf"
if ! grep -q "^SystemMaxUse=" "$JOURNALD_CONF"; then
    sed -i 's/^#SystemMaxUse=.*/SystemMaxUse=1G/' "$JOURNALD_CONF"
    sed -i 's/^#SystemMaxFileSize=.*/SystemMaxFileSize=100M/' "$JOURNALD_CONF"
    systemctl restart systemd-journald
    log "  journald 大小限制 1G,单文件 100M"
fi

# === 5. SSH 加固 ===
log "[5/10] SSH 加固"
SSHD_CONF="/etc/ssh/sshd_config"
cp -a "$SSHD_CONF" "$SSHD_CONF.bak.$(date +%s)"

ssh_set() {
    local key="$1" value="$2"
    if grep -qE "^#?\s*$key\s" "$SSHD_CONF"; then
        sed -i "s|^#\?\s*$key\s.*|$key $value|" "$SSHD_CONF"
    else
        echo "$key $value" >> "$SSHD_CONF"
    fi
}

ssh_set "Port" "$SSH_PORT"
ssh_set "PermitRootLogin" "no"
ssh_set "PasswordAuthentication" "no"
ssh_set "PubkeyAuthentication" "yes"
ssh_set "PermitEmptyPasswords" "no"
ssh_set "MaxAuthTries" "3"
ssh_set "LoginGraceTime" "30"
ssh_set "ClientAliveInterval" "300"
ssh_set "ClientAliveCountMax" "2"
ssh_set "X11Forwarding" "no"
ssh_set "AllowAgentForwarding" "no"
ssh_set "LogLevel" "VERBOSE"

# 测试 + reload
if sshd -t; then
    systemctl reload sshd
    log "  SSH 配置已应用并 reload"
else
    log "  [ERROR] SSH 配置语法错误,已保留备份,中止"
    exit 1
fi

# === 6. 防火墙 ===
log "[6/10] 防火墙"
# 关闭 firewalld(若选用直接管理 iptables 的方案)
if systemctl is-active --quiet firewalld 2>/dev/null; then
    systemctl stop firewalld
    systemctl disable firewalld
    systemctl mask firewalld
    log "  firewalld 已禁用"
fi

# 基础 iptables 规则(此处仅为骨架,生产应当从模板加载)
if ! iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then
    iptables -P INPUT ACCEPT
    iptables -F INPUT
    iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    iptables -A INPUT -i lo -j ACCEPT
    iptables -A INPUT -p icmp -j ACCEPT
    iptables -A INPUT -p tcp --dport "$SSH_PORT" -s "$SSH_ALLOW_CIDR" -j ACCEPT
    iptables -A INPUT -j DROP

    # 持久化
    if command -v iptables-save &>/dev/null; then
        if [ -d /etc/sysconfig ]; then
            iptables-save > /etc/sysconfig/iptables
        elif [ -d /etc/iptables ]; then
            iptables-save > /etc/iptables/rules.v4
        fi
    fi
    log "  iptables 基础规则已应用"
fi

# === 7. 内核参数 sysctl ===
log "[7/10] 内核参数"
cat > /etc/sysctl.d/99-baseline.conf <<'EOF'
# 网络层加固
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1

# TCP 性能调优
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535

# conntrack
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30

# 内存与 swap
vm.swappiness = 10
vm.overcommit_memory = 1

# 文件系统
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 8192

# 内核安全
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.yama.ptrace_scope = 1
EOF
sysctl --system >/dev/null
log "  sysctl 参数已应用"

# === 8. 资源限制 ===
log "[8/10] 资源限制"
cat > /etc/security/limits.d/99-baseline.conf <<'EOF'
* soft nofile 65535
* hard nofile 65535
* soft nproc  65535
* hard nproc  65535
root soft nofile 65535
root hard nofile 65535
EOF

# systemd 全局 limits
mkdir -p /etc/systemd/system.conf.d
cat > /etc/systemd/system.conf.d/limits.conf <<'EOF'
[Manager]
DefaultLimitNOFILE=65535
DefaultLimitNPROC=65535
EOF

systemctl daemon-reload
log "  ulimit 与 systemd limit 已应用"

# === 9. swap 控制 ===
log "[9/10] swap 配置"
if [ "$ENABLE_SWAP" = "false" ]; then
    # K8s 节点要求关闭 swap
    swapoff -a
    sed -i 's|^\(/.*swap.*\)$|#\1|' /etc/fstab
    log "  swap 已关闭"
fi

# === 10. 时间同步状态检查 ===
log "[10/10] 验证基线"
sleep 5  # 让 chrony 有时间同步
log "  $(timedatectl | grep 'System clock synchronized' || echo '时间同步状态未知')"
log "  $(hostnamectl --static)"
log "  SSH 监听: $(ss -lntp | grep ":$SSH_PORT" || echo '未监听!')"

log "=== 基线初始化完成 ==="

调用方式

bash
# 单台机器
bash /usr/local/sbin/init-baseline.sh web-01

# 指定 NTP 服务器
NTP_SERVERS="ntp1.internal.example.com ntp2.internal.example.com" \
    bash init-baseline.sh web-01

# K8s 节点(关闭 swap)
ENABLE_SWAP=false bash init-baseline.sh k8s-node-01

# SSH 改非标端口 + 限制更窄网段
SSH_PORT=22022 SSH_ALLOW_CIDR=10.10.0.0/16 \
    bash init-baseline.sh bastion-01

四、K8s 节点专属基线

Kubernetes 节点对基础环境有几个额外要求,在通用基线之上叠加:

bash
#!/bin/bash
# /usr/local/sbin/init-k8s-node.sh
# K8s 节点专用基线,在通用基线之后执行

set -euo pipefail

# === 1. 必须关闭 swap ===
swapoff -a
sed -i 's|^\(/.*swap.*\)$|#\1|' /etc/fstab

# === 2. 加载必需内核模块 ===
cat > /etc/modules-load.d/k8s.conf <<'EOF'
br_netfilter
overlay
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF

modprobe br_netfilter
modprobe overlay
modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack

# === 3. K8s 必需的 sysctl 参数 ===
cat > /etc/sysctl.d/99-kubernetes.conf <<'EOF'
# bridge 流量经过 iptables
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1

# 启用转发(K8s Pod 间通信必需)
net.ipv4.ip_forward = 1

# K8s 大集群需要更高 conntrack 上限
net.netfilter.nf_conntrack_max = 2097152

# inotify(K8s 节点上 Pod 多时容易耗尽)
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 524288

# 文件句柄
fs.file-max = 2097152
EOF
sysctl --system

# === 4. cgroup v2 检查(K8s 1.25+ 推荐使用 cgroup v2) ===
if [ "$(stat -fc %T /sys/fs/cgroup/)" != "cgroup2fs" ]; then
    echo "[WARN] 当前未使用 cgroup v2,建议升级"
    # 启用 cgroup v2 需要 grub 配置 systemd.unified_cgroup_hierarchy=1
fi

# === 5. 关闭 SELinux(或设为 Permissive) ===
# K8s 早期版本要求关闭 SELinux,新版本可保留
if [ "$(getenforce)" = "Enforcing" ]; then
    setenforce 0
    sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
fi

K8s 1.27+ 已经支持 SELinux 启用状态运行,但需要使用支持 SELinux 的 CRI(containerd 1.7+)并正确配置策略。如果遇到问题,临时切到 Permissive 排查,长期推荐保留 SELinux 启用。

五、基线漂移检测

基线只在装机时执行一次是不够的——服务器运行过程中,人工操作、应急修复、临时变更会让实际状态逐渐偏离基线。漂移检测是基线管理的另一半:定期对比当前状态与基线,发现偏差自动告警或修复

简易漂移检测脚本

把关键配置项的当前值与基线对比:

bash
#!/bin/bash
# /usr/local/sbin/check-baseline.sh
# 基线漂移检测,只读,定期 cron 执行

set -uo pipefail

BASELINE_FILE="/etc/baseline/expected.txt"
mkdir -p "$(dirname "$BASELINE_FILE")"

# 提取当前关键配置项的值
current_state() {
    {
        echo "TIMEZONE=$(timedatectl show -p Timezone --value)"
        echo "SSH_PORT=$(grep -E '^Port ' /etc/ssh/sshd_config | awk '{print $2}')"
        echo "SSH_PERMIT_ROOT=$(grep -E '^PermitRootLogin' /etc/ssh/sshd_config | awk '{print $2}')"
        echo "SSH_PASSWORD_AUTH=$(grep -E '^PasswordAuthentication' /etc/ssh/sshd_config | awk '{print $2}')"
        echo "IP_FORWARD=$(sysctl -n net.ipv4.ip_forward)"
        echo "SWAPPINESS=$(sysctl -n vm.swappiness)"
        echo "SWAP_ON=$([ "$(swapon --show | wc -l)" -gt 0 ] && echo true || echo false)"
        echo "FIREWALLD=$(systemctl is-active firewalld 2>/dev/null || echo inactive)"
        echo "SELINUX=$(getenforce 2>/dev/null || echo none)"
        echo "JOURNALD_PERSISTENT=$([ -d /var/log/journal ] && echo true || echo false)"
        echo "ULIMIT_NOFILE=$(grep -hE '^\* hard nofile' /etc/security/limits.d/*.conf 2>/dev/null | awk '{print $4}' | sort -u | head -1)"
    } | sort
}

if [ ! -f "$BASELINE_FILE" ]; then
    echo "首次运行,生成基线"
    current_state > "$BASELINE_FILE"
    exit 0
fi

drift=$(diff <(current_state) "$BASELINE_FILE")
if [ -n "$drift" ]; then
    echo "检测到基线漂移:"
    echo "$drift"
    exit 1
fi
echo "基线一致"

加到 cron 每天检查:

cron
0 6 * * * /usr/local/sbin/check-baseline.sh >> /var/log/baseline-drift.log 2>&1

漂移检测的输出可以接入告警系统——发现漂移直接通知运维。

把基线检查集成到监控系统

更彻底的方案是把基线检查指标暴露给 Prometheus,通过现有监控告警:

bash
# /usr/local/sbin/baseline_exporter.sh
# 输出 Prometheus 文本格式
cat <<EOF
# HELP baseline_drift_total 检测到的基线漂移项数
# TYPE baseline_drift_total gauge
baseline_drift_total $(diff <(current_state) "$BASELINE_FILE" 2>/dev/null | grep -c '^[<>]' || echo 0)
EOF

让 node_exporter 通过 textfile collector 采集这个指标:

bash
# 输出到 textfile collector 目录
/usr/local/sbin/baseline_exporter.sh > /var/lib/node_exporter/textfile_collector/baseline.prom

Prometheus 告警规则:

yaml
- alert: BaselineDrift
  expr: baseline_drift_total > 0
  for: 1h
  annotations:
    summary: "{{ $labels.instance }} 检测到基线漂移"

六、批量推送基线

基线脚本写好后,如何把它推到几十几百台机器并保持长期一致是真正的工程问题。主流方案:

Ansible(轻量、无 agent)

yaml
# baseline.yml
- hosts: all
  become: yes
  tasks:
    - name: 设置时区
      timezone:
        name: Asia/Shanghai

    - name: 安装 chrony
      package:
        name: chrony
        state: present

    - name: 启用 chrony
      systemd:
        name: "{{ 'chronyd' if ansible_os_family == 'RedHat' else 'chrony' }}"
        state: started
        enabled: yes

    - name: 部署 SSH 配置
      template:
        src: sshd_config.j2
        dest: /etc/ssh/sshd_config
        validate: '/usr/sbin/sshd -t -f %s'
      notify: reload sshd

    # ... 其他基线项

执行:

bash
ansible-playbook -i hosts baseline.yml

validate 参数让 Ansible 在写入前先检查配置语法,语法错才不会写出错误文件——比手工 shell 脚本可靠得多。

SaltStack / Puppet / Chef

各有适用场景,核心都是"声明期望状态,工具确保达到该状态"。SaltStack 用 state 文件,Puppet 用 manifest,Chef 用 recipe。

自动化运维平台(更上层)

公司规模大、机器多、变更频繁时,通常会建自动化运维平台:

  • 资产管理:每台机器属于哪个业务、什么角色、应用哪套基线
  • 基线版本管理:基线随安全规范更新,所有机器跟随新基线
  • 变更审计:谁、什么时候、改了什么基线项

七、CIS Benchmark 与等保对照

公司有合规要求时(等保 2.0、PCI DSS、SOC 2、HIPAA),基线需要对照具体标准条款。CIS(Center for Internet Security)发布的 Linux Benchmark 是行业最广泛使用的参考。

CIS Benchmark 的层级

CIS 把检查项分为两个 Profile:

  • Level 1 — 基础加固,对系统功能影响最小。生产环境通常都要求满足
  • Level 2 — 高安全加固,可能影响某些功能,适合高合规要求场景

自动化对照工具

bash
# OpenSCAP(Red Hat 主导)
yum install openscap-scanner scap-security-guide -y

# 扫描本机对照 CIS Benchmark
oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis \
    --results /tmp/cis-results.xml \
    --report /tmp/cis-report.html \
    /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

# 看 HTML 报告
firefox /tmp/cis-report.html

OpenSCAP 输出每条检查项的"通过/失败/不适用"状态,失败项给出具体修复建议。

等保 2.0 的核心条款

等保对 Linux 主机的常见要求映射到基线项:

等保条款基线动作
身份鉴别(三级)SSH 禁密码、PAM 失败锁定、密码复杂度策略
访问控制(三级)sudo 最小权限、关闭不必要服务、文件权限收紧
安全审计(三级)auditd 启用、journald 持久化、日志保留 6 个月以上
入侵防范(三级)iptables 默认拒绝、fail2ban、漏洞扫描
恶意代码防范ClamAV、rkhunter 定期扫描
数据完整性AIDE 文件完整性监控

完整的等保实施需要整套体系(包括人员、流程、应急响应),操作系统层面的基线只是其中一部分。

八、AIDE:文件完整性监控

基线还有一层是关键系统文件的完整性——/etc/passwd/etc/shadow/etc/ssh/sshd_config/usr/bin/* 这些文件不应该被随意修改。AIDE 计算文件的哈希值作为基线,定期对比发现篡改。

bash
# 安装
yum install aide -y
apt install aide -y

# 第一次执行:生成基线数据库
aide --init
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz

# 定期检查(通常通过 cron 每天执行)
aide --check

aide --check 输出哪些文件被新增、删除、修改:

Added entries:
-----
f++++++++++++++++: /tmp/suspicious-file

Changed entries:
-----
f   ...c.....: /etc/passwd

Detailed information about changes:
---------------------------
File: /etc/passwd
  Mtime    : 2026-06-20 18:00:00              | 2026-06-23 11:00:00
  Ctime    : 2026-06-20 18:00:00              | 2026-06-23 11:00:00
  MD5      : xxxxxxxxxx                       | yyyyyyyyyy

每次执行合法变更(useraddpasswd、改 SSH 配置)后,必须更新 AIDE 基线,否则下次 --check 会一直报告这些变更:

bash
aide --init
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz

AIDE 数据库本身要保护起来——如果攻击者能改 AIDE 数据库,完整性监控就失效了。常见做法是把数据库放到只读介质或独立的审计服务器上。

九、基线变更的发布流程

基线本身随时间演进——新漏洞要求加新参数、内核新版本需要适配、业务需求要求开新端口。基线变更需要正式发布流程,不能某个运维心血来潮就改了。

合理的流程:

  1. 变更提案 — 描述要改什么、为什么改、影响哪些机器
  2. 测试环境验证 — 在 staging 环境应用新基线,跑业务回归
  3. 变更评审 — 安全、运维、业务方共同评审
  4. 灰度发布 — 先在小规模(几台)机器应用,观察 1-2 周
  5. 全量推送 — 通过 Ansible 等工具推送到所有机器
  6. 漂移检测同步更新 — 把新基线作为漂移检测的对照

基线版本写入 Git,每次变更都有 commit、有 review、可追溯。这是基线管理的工程化基础。

十、易踩的陷阱速查

现象根因处理
基线脚本第二次跑报错没做幂等性检查,某些操作重复执行后失败每个动作前判断目标状态是否已达到
改完 ulimit 但 systemd 服务还是老值systemd 服务不读 /etc/security/limits.conf同时配 systemd unit 的 LimitNOFILE 或 /etc/systemd/system.conf 的 DefaultLimitNOFILE
sysctl 改了重启后失效改的是 /proc/sys/...(运行时),没写入 /etc/sysctl.d/持久化配置写到 /etc/sysctl.d/99-*.conf
K8s 节点 sysctl bridge-nf-call-iptables 不生效该参数依赖 br_netfilter 模块加载先 modprobe br_netfilter,再 sysctl
journald 持久化设了但日志还是丢/var/log/journal 目录不存在,只配 Storage=persistent 无效先 mkdir /var/log/journal,再 restart journald
Ansible 部署 sshd_config 后服务起不来template 渲染出语法错误template 必须配 validate '/usr/sbin/sshd -t -f %s'
基线脚本在某台机器卡住yum/apt 等命令在没网络时挂起加 timeout,失败时立即退出而不是等待
漂移检测告警但实际没改过系统包升级或内核更新触发了配置文件重写包升级后主动跑 baseline 修复 + 更新基线
CIS 扫描显示一堆 Level 2 失败Level 2 对生产可能影响过大只追求 Level 1 通过,Level 2 按业务需要选择性应用
关闭 firewalld 后 Docker 网络坏Docker 依赖 iptables 规则,firewalld 重启或停止可能干扰mask firewalld 后,重启 Docker 重建规则