Linux 安全基线实战:CIS Benchmark 一键核查脚本
文章目录
一、为什么要做 Linux 安全基线
服务器被入侵,很少是因为「零日漏洞」,更多是因为:
- SSH 允许 root 密码登录
- 密码策略形同虚设
- 审计未开启、日志无人看
- 内核参数未加固,转发/源路由仍开着
这些都属于配置问题,不是补丁问题。CIS(Center for Internet Security)Benchmark 把这类检查项标准化了,是等保、ISO 27001、SOC2 等场景里最常用的 Linux 基线参考之一。
目标很明确:把“凭经验巡检”变成“可重复、可审计、可落地”的一键核查。
二、CIS Benchmark 怎么用(先对齐预期)
CIS 按发行版拆分,例如:
| 文档 | 适用对象 |
|---|---|
| CIS Ubuntu Linux 22.04 LTS Benchmark | Ubuntu 22.04 |
| CIS Red Hat Enterprise Linux 8 Benchmark | RHEL / Rocky / Alma 8 |
| CIS Amazon Linux 2 Benchmark | Amazon Linux 2 |
每条控制项通常包含:
- Profile:Level 1(通用加固)/ Level 2(高安全,可能影响兼容性)
- Description:要防什么
- Rationale:为什么要做
- Audit:怎么查
- Remediation:怎么改
官方工具有 CIS-CAT,但企业环境里常见诉求是:
- 内网离线可跑
- 输出中文可读报告
- 可嵌入 Ansible / Cron / CI
- 不依赖商业授权
所以本文用 Bash 一键核查脚本:覆盖高频 CIS 控制点,适合日常巡检与验收前自查。它不是 CIS-CAT 的完整替代,而是可落地的基线引擎雏形。
三、脚本设计原则
做基线脚本,建议坚持这几条:
- 只读核查:默认不改系统,避免“查着查着把生产改挂了”
- 可解释:每项输出
PASS / FAIL / WARN / MANUAL,并附证据命令结果 - 可扩展:一条检查 = 一个函数,便于按发行版增删
- 可汇总:最后给出通过率与高风险清单
- 可归档:同时落盘文本报告与 JSON,方便二次处理
状态约定:
| 状态 | 含义 |
|---|---|
PASS |
符合基线 |
FAIL |
不符合,建议整改 |
WARN |
部分符合或依赖环境 |
MANUAL |
需人工确认(如物理安全、组织流程) |
N/A |
当前系统不适用 |
四、一键核查脚本(可直接落地)
把下面内容保存为 cis_linux_baseline_check.sh,在目标机以 root 或具备足够权限的账号执行。
说明:脚本面向通用 Linux(systemd + OpenSSH),以 CIS Level 1 常见项为主;不同发行版路径/服务名可能略有差异,可按现场微调。
#!/usr/bin/env bash
# cis_linux_baseline_check.sh
# 《网络安全实战》- Linux CIS 风格安全基线一键核查(只读)
# 适用:Ubuntu / Debian / RHEL / Rocky / Alma / CentOS Stream(systemd)
set -euo pipefail
VERSION="1.0.0"
TS="$(date +%Y%m%d_%H%M%S)"
HOST="$(hostname -f 2>/dev/null || hostname)"
OUT_DIR="${OUT_DIR:-./cis_reports}"
REPORT_TXT="${OUT_DIR}/cis_${HOST}_${TS}.txt"
REPORT_JSON="${OUT_DIR}/cis_${HOST}_${TS}.json"
PASS_N=0; FAIL_N=0; WARN_N=0; MANUAL_N=0; NA_N=0
RESULTS=()
mkdir -p "$OUT_DIR"
color() {
local c="$1"; shift
if [[ -t 1 ]]; then
case "$c" in
green) printf "\033[32m%s\033[0m" "$*" ;;
red) printf "\033[31m%s\033[0m" "$*" ;;
yellow) printf "\033[33m%s\033[0m" "$*" ;;
blue) printf "\033[34m%s\033[0m" "$*" ;;
*) printf "%s" "$*" ;;
esac
else
printf "%s" "$*"
fi
}
record() {
# record ID TITLE STATUS DETAIL
local id="$1" title="$2" status="$3" detail="$4"
case "$status" in
PASS) PASS_N=$((PASS_N+1)); s_col=green ;;
FAIL) FAIL_N=$((FAIL_N+1)); s_col=red ;;
WARN) WARN_N=$((WARN_N+1)); s_col=yellow ;;
MANUAL) MANUAL_N=$((MANUAL_N+1)); s_col=blue ;;
N/A) NA_N=$((NA_N+1)); s_col=blue ;;
*) s_col=blue ;;
esac
printf "[%s] %-8s %s\n" "$id" "$(color "$s_col" "$status")" "$title"
[[ -n "$detail" ]] && printf " └─ %s\n" "$detail"
RESULTS+=("$(printf '%s|%s|%s|%s' "$id" "$title" "$status" "$detail")")
}
have_cmd() { command -v "$1" >/dev/null 2>&1; }
# ---------- 检查项 ----------
check_filesystem_tmp() {
# CIS: Ensure /tmp is configured (常见要求:独立挂载 + nosuid,nodev,noexec 中至少部分)
local opts
if findmnt -n /tmp >/dev/null 2>&1; then
opts="$(findmnt -n -o OPTIONS /tmp)"
if echo "$opts" | grep -Eq 'nosuid' && echo "$opts" | grep -Eq 'nodev'; then
record "1.1.2" "/tmp 挂载加固" "PASS" "options=$opts"
else
record "1.1.2" "/tmp 挂载加固" "FAIL" "建议含 nosuid,nodev(,noexec);当前=$opts"
fi
else
record "1.1.2" "/tmp 挂载加固" "WARN" "/tmp 未独立挂载,建议拆分并加挂载选项"
fi
}
check_sticky_bit_world_writable() {
# CIS: Ensure sticky bit is set on all world-writable directories
local bad
bad="$(df --local -P 2>/dev/null | awk 'NR!=1 {print $6}' | xargs -I{} find {} -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null | head -n 20 || true)"
if [[ -z "$bad" ]]; then
record "1.1.23" "全局可写目录 sticky bit" "PASS" "未发现缺失 sticky bit 的 world-writable 目录(抽样扫描)"
else
record "1.1.23" "全局可写目录 sticky bit" "FAIL" "示例: $(echo "$bad" | tr '\n' ' ')"
fi
}
check_aslr() {
local v
v="$(sysctl -n kernel.randomize_va_space 2>/dev/null || echo missing)"
if [[ "$v" == "2" ]]; then
record "1.5.2" "ASLR (kernel.randomize_va_space)" "PASS" "value=$v"
else
record "1.5.2" "ASLR (kernel.randomize_va_space)" "FAIL" "期望=2,当前=$v"
fi
}
check_core_dumps() {
local soft hard
soft="$(ulimit -S -c 2>/dev/null || echo '?')"
hard="$(ulimit -H -c 2>/dev/null || echo '?')"
if [[ "$soft" == "0" && "$hard" == "0" ]]; then
record "1.5.1" "限制 core dump" "PASS" "ulimit -c soft=$soft hard=$hard"
else
record "1.5.1" "限制 core dump" "WARN" "当前 soft=$soft hard=$hard;建议在 limits.conf 设 * hard core 0"
fi
}
check_ssh_root_login() {
local v
if ! have_cmd sshd; then
record "5.2.10" "SSH PermitRootLogin" "N/A" "未安装 sshd"
return
fi
v="$(sshd -T 2>/dev/null | awk '/^permitrootlogin /{print $2; exit}')"
if [[ "$v" == "no" || "$v" == "prohibit-password" || "$v" == "without-password" ]]; then
record "5.2.10" "SSH PermitRootLogin" "PASS" "permitrootlogin=$v"
else
record "5.2.10" "SSH PermitRootLogin" "FAIL" "当前=$v;建议 no 或 prohibit-password"
fi
}
check_ssh_password_auth() {
local v
if ! have_cmd sshd; then
record "5.2.11" "SSH PasswordAuthentication" "N/A" "未安装 sshd"
return
fi
v="$(sshd -T 2>/dev/null | awk '/^passwordauthentication /{print $2; exit}')"
if [[ "$v" == "no" ]]; then
record "5.2.11" "SSH PasswordAuthentication" "PASS" "passwordauthentication=no(密钥登录)"
else
record "5.2.11" "SSH PasswordAuthentication" "WARN" "当前=$v;高安全环境建议关闭密码登录"
fi
}
check_ssh_max_auth_tries() {
local v
if ! have_cmd sshd; then
record "5.2.7" "SSH MaxAuthTries" "N/A" "未安装 sshd"
return
fi
v="$(sshd -T 2>/dev/null | awk '/^maxauthtries /{print $2; exit}')"
if [[ -n "$v" && "$v" -le 4 ]]; then
record "5.2.7" "SSH MaxAuthTries" "PASS" "maxauthtries=$v"
else
record "5.2.7" "SSH MaxAuthTries" "FAIL" "当前=${v:-unknown};建议 <=4"
fi
}
check_passwd_max_days() {
local v
v="$(awk -F: '/^PASS_MAX_DAYS/{print $2; exit}' /etc/login.defs 2>/dev/null | tr -d '[:space:]')"
if [[ -n "$v" && "$v" -le 365 && "$v" -gt 0 ]]; then
record "5.4.1.1" "密码最长使用期限 PASS_MAX_DAYS" "PASS" "PASS_MAX_DAYS=$v"
else
record "5.4.1.1" "密码最长使用期限 PASS_MAX_DAYS" "FAIL" "当前=${v:-unset};常见建议 90~365"
fi
}
check_umask() {
# 简化:看 /etc/login.defs 与 bashrc 常见配置
local v
v="$(awk '/^UMASK/{print $2; exit}' /etc/login.defs 2>/dev/null | tr -d '[:space:]')"
if [[ "$v" == "027" || "$v" == "077" ]]; then
record "5.4.3" "默认 umask" "PASS" "login.defs UMASK=$v"
else
record "5.4.3" "默认 umask" "WARN" "login.defs UMASK=${v:-unset};CIS 常建议 027"
fi
}
check_firewall() {
if have_cmd ufw && ufw status 2>/dev/null | grep -qi 'Status: active'; then
record "3.5" "主机防火墙" "PASS" "ufw active"
elif have_cmd firewall-cmd && firewall-cmd --state 2>/dev/null | grep -qi running; then
record "3.5" "主机防火墙" "PASS" "firewalld running"
elif have_cmd iptables && iptables -L -n 2>/dev/null | grep -q 'Chain'; then
record "3.5" "主机防火墙" "WARN" "检测到 iptables,请确认默认策略与入站规则"
else
record "3.5" "主机防火墙" "FAIL" "未检测到启用中的主机防火墙"
fi
}
check_ip_forward() {
local v
v="$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo missing)"
if [[ "$v" == "0" ]]; then
record "3.1.1" "关闭 IPv4 转发" "PASS" "net.ipv4.ip_forward=0"
else
# 路由器/K8s 节点可能必须开启
record "3.1.1" "关闭 IPv4 转发" "WARN" "当前=$v;非路由节点建议为 0"
fi
}
check_rp_filter() {
local all def
all="$(sysctl -n net.ipv4.conf.all.rp_filter 2>/dev/null || echo missing)"
def="$(sysctl -n net.ipv4.conf.default.rp_filter 2>/dev/null || echo missing)"
if [[ "$all" == "1" && "$def" == "1" ]]; then
record "3.2.7" "反向路径过滤 rp_filter" "PASS" "all=$all default=$def"
else
record "3.2.7" "反向路径过滤 rp_filter" "FAIL" "期望均为1;当前 all=$all default=$def"
fi
}
check_auditd() {
if systemctl is-active --quiet auditd 2>/dev/null; then
record "4.1.1" "auditd 服务" "PASS" "auditd is active"
elif have_cmd auditctl; then
record "4.1.1" "auditd 服务" "FAIL" "安装了 audit 工具但服务未运行"
else
record "4.1.1" "auditd 服务" "FAIL" "未安装/未运行 auditd"
fi
}
check_chrony_ntp() {
if systemctl is-active --quiet chronyd 2>/dev/null || systemctl is-active --quiet chrony 2>/dev/null; then
record "2.2.1" "时间同步" "PASS" "chrony active"
elif systemctl is-active --quiet systemd-timesyncd 2>/dev/null; then
record "2.2.1" "时间同步" "PASS" "systemd-timesyncd active"
elif systemctl is-active --quiet ntpd 2>/dev/null; then
record "2.2.1" "时间同步" "PASS" "ntpd active"
else
record "2.2.1" "时间同步" "FAIL" "未检测到启用中的时间同步服务"
fi
}
check_unattended_upgrades_hint() {
record "1.9" "补丁与更新策略" "MANUAL" "请确认自动安全更新/补丁窗口已纳入运维流程"
}
# ---------- 主流程 ----------
main() {
{
echo "=============================================="
echo " CIS Linux Baseline Check v${VERSION}"
echo " Host : $HOST"
echo " Date : $(date '+%F %T %Z')"
echo " Kernel: $(uname -srm)"
echo " OS : $(grep -E '^PRETTY_NAME=' /etc/os-release 2>/dev/null | cut -d= -f2- | tr -d '"')"
echo " Mode : READ-ONLY"
echo "=============================================="
echo
} | tee "$REPORT_TXT"
{
check_filesystem_tmp
check_sticky_bit_world_writable
check_aslr
check_core_dumps
check_chrony_ntp
check_ip_forward
check_rp_filter
check_firewall
check_auditd
check_ssh_root_login
check_ssh_password_auth
check_ssh_max_auth_tries
check_passwd_max_days
check_umask
check_unattended_upgrades_hint
} | tee -a "$REPORT_TXT"
local total=$((PASS_N+FAIL_N+WARN_N+MANUAL_N+NA_N))
local score=0
if (( total - MANUAL_N - NA_N > 0 )); then
score=$(( PASS_N * 100 / (total - MANUAL_N - NA_N) ))
fi
{
echo
echo "-------------- SUMMARY --------------"
echo " PASS=$PASS_N FAIL=$FAIL_N WARN=$WARN_N MANUAL=$MANUAL_N N/A=$NA_N"
echo " Score(PASS/(total-MANUAL-N/A)) = ${score}%"
echo " Report: $REPORT_TXT"
echo "-------------------------------------"
} | tee -a "$REPORT_TXT"
# JSON
{
echo "{"
echo " \"host\": \"$HOST\","
echo " \"timestamp\": \"$TS\","
echo " \"version\": \"$VERSION\","
echo " \"summary\": {\"pass\":$PASS_N,\"fail\":$FAIL_N,\"warn\":$WARN_N,\"manual\":$MANUAL_N,\"na\":$NA_N,\"score\":$score},"
echo " \"checks\": ["
local i=0
for row in "${RESULTS[@]}"; do
IFS='|' read -r id title status detail <<<"$row"
detail="${detail//\"/\\\"}"
title="${title//\"/\\\"}"
((i++)) && echo ","
printf ' {"id":"%s","title":"%s","status":"%s","detail":"%s"}' "$id" "$title" "$status" "$detail"
done
echo
echo " ]"
echo "}"
} > "$REPORT_JSON"
echo " JSON : $REPORT_JSON"
# FAIL 数作为退出码上限(便于 CI)
if (( FAIL_N > 0 )); then exit 2; else exit 0; fi
}
main "$@"
五、怎么跑
chmod +x cis_linux_baseline_check.sh
sudo ./cis_linux_baseline_check.sh
输出示例(节选):
[5.2.10] PASS SSH PermitRootLogin
└─ permitrootlogin=prohibit-password
[5.2.11] WARN SSH PasswordAuthentication
└─ 当前=yes;高安全环境建议关闭密码登录
[4.1.1] FAIL auditd 服务
└─ 未安装/未运行 auditd
-------------- SUMMARY --------------
PASS=9 FAIL=3 WARN=2 MANUAL=1 N/A=0
Score(...)= 64%
报告目录默认:./cis_reports/
.txt:给人看.json:给平台/SIEM/工单系统吃
CI 场景可把退出码 2(存在 FAIL)当作流水线失败条件。
六、从“查出问题”到“改到位”
核查只是第一步。常见 FAIL 的快速整改参考:
1)禁止 SSH root 密码登录
# /etc/ssh/sshd_config.d/99-cis.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
MaxAuthTries 4
systemctl reload sshd || systemctl reload ssh
2)开启 auditd(RHEL 系示例)
dnf install -y audit audit-libs
systemctl enable --now auditd
3)rp_filter / ASLR
cat >/etc/sysctl.d/99-cis-sysctl.conf <<'EOF'
kernel.randomize_va_space = 2
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.ip_forward = 0
EOF
sysctl --system
4)防火墙(Ubuntu)
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable
整改原则:先备份配置 → 灰度一台 → 再批量推送。K8s 节点、NAT 网关等角色对 ip_forward 有业务依赖,不要一刀切。
七、进阶:从“脚本”到“基线平台”
单机脚本够用后,可以按这个路径升级:
- 规则外置:把检查项写成 YAML(id / title / audit_cmd / expect / severity)
- 发行版适配层:Ubuntu 用 ufw,RHEL 用 firewalld;自动选择
- 与编排联动:Ansible 先
check再remediate,并保留 diff - 对标正式标准:关键控制项与 CIS Benchmark 章节号一一映射,验收时好说话
- 持续化:开机后首次、每周 Cron、镜像构建流水线各跑一遍
若合规要求严格,最终仍建议用官方 CIS-CAT 或经过认证的扫描器出正式报告;本文脚本适合作为日常运营层的第一道防线。
八、实战清单(建议你按这个顺序落地)
- 选准发行版对应的 CIS Benchmark PDF,标出 Level 1 必做项
- 在一台“金样机”上跑通一键核查脚本
- 把 FAIL 项拆成变更单,区分“立即改 / 需评估 / 可接受风险”
- 整改后复扫,分数稳定后再做成镜像或 Kickstart/cloud-init 模板
- 新机器入网必须先过基线,再挂业务
结语:Linux 安全基线不是一次“扫完就完”的运动,而是把 CIS 这类共识规则,固化成可执行、可度量、可回归的工程能力。
一键核查脚本的价值在于:
- 降低巡检成本
- 统一团队对“什么叫安全”的标准
- 为自动化整改提供输入
先跑起来,再逐项对齐官方 Benchmark——这比空谈“全面合规”更接近真实的安全实战。
更多推荐



所有评论(0)