Appearance
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
fi2. 可追溯
每个动作输出日志,记录"做了什么、原因、时间戳"。事后追溯某台机器为什么是这个状态,从日志就能复盘:
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
fiK8s 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.promPrometheus 告警规则:
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.ymlvalidate 参数让 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.htmlOpenSCAP 输出每条检查项的"通过/失败/不适用"状态,失败项给出具体修复建议。
等保 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 --checkaide --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每次执行合法变更(useradd、passwd、改 SSH 配置)后,必须更新 AIDE 基线,否则下次 --check 会一直报告这些变更:
bash
aide --init
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gzAIDE 数据库本身要保护起来——如果攻击者能改 AIDE 数据库,完整性监控就失效了。常见做法是把数据库放到只读介质或独立的审计服务器上。
九、基线变更的发布流程
基线本身随时间演进——新漏洞要求加新参数、内核新版本需要适配、业务需求要求开新端口。基线变更需要正式发布流程,不能某个运维心血来潮就改了。
合理的流程:
- 变更提案 — 描述要改什么、为什么改、影响哪些机器
- 测试环境验证 — 在 staging 环境应用新基线,跑业务回归
- 变更评审 — 安全、运维、业务方共同评审
- 灰度发布 — 先在小规模(几台)机器应用,观察 1-2 周
- 全量推送 — 通过 Ansible 等工具推送到所有机器
- 漂移检测同步更新 — 把新基线作为漂移检测的对照
基线版本写入 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 重建规则 |