安全基线这件事,甲方安全岗几乎都做过,但大部分都做成了"一张没人看的 Excel"。
典型的样子:安全部门花两周整理出一份几百行的配置清单,发给运维,要求自查整改;运维看一眼,回一句"太多了,哪些必须改";安全部门说"都是必须的";然后就没有然后了。半年后审计来查,发现清单躺在共享盘里,没人动过。
我在几个项目里踩过这个坑,后来把基线从"文档"做成了"流水线":一份分级清单 + 一套自动核查脚本 + 一条例外审批通道 + 一个接进整改台账的出口。核查覆盖率从靠人肉抽查的 20% 左右,做到全量自动跑,配置类问题从"季度清理"变成"每周出报告"。
这篇把整条流水线拆开写,包括可以直接跑的核查脚本。
一、基线为什么总是落不了地
先想清楚失败的原因,不然做多少版清单都一样。
我总结下来是四个错位:
| 错位 | 表现 | 结果 |
|---|---|---|
| 粒度错位 | 一条基线写成一本书("操作系统应安全配置") | 运维无法执行 |
| 刚性错位 | 所有条目都标"必须",没有例外机制 | 遇到业务冲突就整份放弃 |
| 验证错位 | 只发清单不验证,靠自查自报 | 报上来的全是"已整改" |
| 归属错位 | 安全部门写、运维改、没人签字 | 出问题互相推 |
其中刚性错位是最致命的。
安全基线本质上是"默认拒绝"的配置集合,而业务系统几乎总有一些"合理的不合规"——老系统必须开 telnet、某中间件必须用弱加密算法对接旧设备、某服务必须以 root 跑。如果你不给这些情况留正规出口,运维就会整体不执行,因为按清单来一定会影响业务。
所以第一件事不是写清单,是给清单分级,并给例外留通道。
二、基线分级:三层结构,只有第一层不可协商
我把所有基线条目分成三层,这个结构直接决定了后面所有流程怎么跑。
| 层级 | 定义 | 数量占比 | 例外可能 | 违反后果 |
|---|---|---|---|---|
| L1 红线 | 违反即构成可直接利用的高危风险 | 约 15% | 不允许例外 | 立即整改,24 小时内 |
| L2 标准 | 业界通行的安全配置要求 | 约 65% | 需审批,有替代措施 | 30 天内,或走例外 |
| L3 建议 | 加固项,收益明确但非必需 | 约 20% | 可自主决定 | 纳入季度计划 |
L1 的判断标准很简单:这一条如果没做,攻击者能不能直接进来或者直接拿走数据。比如:
- 数据库对公网开放
- 存在空口令或默认口令账号
- SSH 允许 root 直接登录 + 密码认证
- 未授权的远程管理端口暴露(3389/22/5900)
- Web 目录可列目录且含备份文件
- 应用存在已知的未修复高危组件(在野利用)
这些不给例外。如果业务确实冲突,方案是改架构,不是签例外。
L2 才是主体,也是容易扯皮的部分。L3 用来给运维"送分",做完显得有进展,实际风险不大——这个设计对推动整体配合度很有效。
三、清单本身怎么长:按"可验证"来写
一条基线写得对不对,只有一个标准:能不能用一条命令或者一次检查判断出"符合/不符合"。
反例:
操作系统应进行安全加固,关闭不必要的服务和端口。
正例(可直接验证):
| 编号 | 检查项 | 判定方法 | 合格标准 | 层级 | ||
|---|---|---|---|---|---|---|
| LNX-001 | SSH 禁止 root 登录 | `sshd -T \ | grep permitrootlogin` | 输出permitrootlogin no | L1 | |
| LNX-002 | SSH 禁止密码认证 | `sshd -T \ | grep passwordauthentication` | 输出no | L2 | |
| LNX-003 | 口令最小长度 | grep -E '^minlen' /etc/security/pwquality.conf | ≥ 10 | L2 | ||
| LNX-004 | 口令复杂度 | `grep -E '^(minclass | dcredit | ucredit)' /etc/security/pwquality.conf` | 四类字符中至少三类 | L2 |
| LNX-005 | 登录失败锁定 | grep deny /etc/security/faillock.conf | deny ≤ 5 | L2 | ||
| LNX-006 | 审计服务运行 | systemctl is-active auditd | active | L1 | ||
| LNX-007 | 关键目录权限 | stat -c '%a' /etc/shadow | 000 或 640 以下 | L1 | ||
| LNX-008 | 空口令账号 | awk -F: '($2==""){print $1}' /etc/shadow | 无输出 | L1 | ||
| LNX-009 | 内核参数-禁止 IP 转发 | sysctl net.ipv4.ip_forward | 0(非路由设备) | L3 | ||
| LNX-010 | 时间同步 | `timedatectl status \ | grep synchronized` | yes | L2 |
这个表的价值在于:每一条都能被脚本直接判定,不需要人去理解、判断、解释。
写清单时容易犯的一个错是写"应该"、"建议"、"适当"这类模糊词。一旦出现这类词,这条基线就等于没写——因为没人能验证,也没人会执行。
四、自动化核查:把清单变成脚本
清单定了,下一步是让它自动跑。架构很简单:
整条流水线分四层,每一层的输入输出都必须固定下来,否则脚本就跑不起来:
- 采集层:各主机执行核查脚本,输出统一的 JSON(每条带 check_id、结果、证据)
- 比对层:与基线定义逐条比对,生成"符合 / 不符合 / 不适用"三种结论
- 汇总层:按系统、按责任人聚合,出 HTML 或 Excel 报告
- 闭环层:不符合项自动写入整改台账,进入整改流程
关键在于采集层要足够轻:不装 agent、不依赖特定平台、能批量下发。这一点很重要——很多企业的基线核查做不下去,就是因为要求装一套重型工具,而运维不愿意在几百台机器上装东西。
我用的方式是最原始的:一段 bash 脚本,通过已有的运维通道(ansible / 堡垒机 / 批量执行)推下去跑,输出 JSON 回来。
下面这段可以直接用,覆盖上面表里的主要检查项:
#!/bin/bash # baseline_check.sh - Linux 基线核查,输出 JSON # 用法: ./baseline_check.sh > /tmp/baseline_$(hostname).json out() { printf '%s\n' "$1"; } j() { printf ' "%s": {"value": "%s", "level": "%s"},\n' "$1" "$2" "$3"; } echo "{" echo " \"host\": \"$(hostname)\"," echo " \"time\": \"$(date -Iseconds)\"," echo " \"items\": {" # LNX-001 SSH root 登录 v=$(sshd -T 2>/dev/null | awk '/^permitrootlogin/{print $2}') j "LNX-001" "${v:-unknown}" "L1" # LNX-002 SSH 密码认证 v=$(sshd -T 2>/dev/null | awk '/^passwordauthentication/{print $2}') j "LNX-002" "${v:-unknown}" "L2" # LNX-003 口令最小长度 v=$(grep -E '^\s*minlen' /etc/security/pwquality.conf 2>/dev/null | awk -F= '{print $2}' | tr -d ' ') j "LNX-003" "${v:-unset}" "L2" # LNX-005 登录失败锁定 v=$(grep -E '^\s*deny' /etc/security/faillock.conf 2>/dev/null | awk -F= '{print $2}' | tr -d ' ') j "LNX-005" "${v:-unset}" "L2" # LNX-006 审计服务 v=$(systemctl is-active auditd 2>/dev/null) j "LNX-006" "${v:-inactive}" "L1" # LNX-007 /etc/shadow 权限 v=$(stat -c '%a' /etc/shadow 2>/dev/null) j "LNX-007" "${v:-unknown}" "L1" # LNX-008 空口令账号 v=$(awk -F: '($2==""){print $1}' /etc/shadow 2>/dev/null | tr '\n' ',' ) j "LNX-008" "${v:-none}" "L1" # LNX-009 IP 转发 v=$(sysctl -n net.ipv4.ip_forward 2>/dev/null) j "LNX-009" "${v:-unknown}" "L3" # LNX-010 时间同步 v=$(timedatectl status 2>/dev/null | awk -F: '/synchronized/{gsub(/ /,"",$2);print $2}') j "LNX-010" "${v:-unknown}" "L2" # 附加:监听在 0.0.0.0 的高危端口 v=$(ss -lntp 2>/dev/null | awk 'NR>1 && $4 ~ /0\.0\.0\.0:(3306|6379|9200|27017)/{print $4}' | tr '\n' ',') j "LNX-011" "${v:-none}" "L1" echo " }" echo "}"输出长得像这样,正好能被下游解析:
{ "host": "app-node-01", "time": "2026-03-25T09:12:33+08:00", "items": { "LNX-001": {"value": "no", "level": "L1"}, "LNX-002": {"value": "yes", "level": "L2"}, "LNX-008": {"value": "none", "level": "L1"} } }汇总端用一个短脚本把结果转成"不符合项清单",直接当整改台账用:
import json, glob RULES = { # 编号 -> (合格判定函数, 说明) "LNX-001": (lambda v: v == "no", "SSH 允许 root 直接登录"), "LNX-002": (lambda v: v == "no", "SSH 允许密码认证"), "LNX-003": (lambda v: v.isdigit() and int(v) >= 10, "口令最小长度不足"), "LNX-006": (lambda v: v == "active", "审计服务未运行"), "LNX-007": (lambda v: v in ("0", "000", "640"), "/etc/shadow 权限过宽"), "LNX-008": (lambda v: v == "none", "存在空口令账号"), "LNX-010": (lambda v: v == "yes", "未启用时间同步"), } rows = [] for f in glob.glob("/tmp/baseline_*.json"): d = json.load(open(f, encoding="utf-8")) for code, item in d["items"].items(): check, desc = RULES.get(code, (None, None)) if not check: continue if not check(item["value"]): rows.append({ "host": d["host"], "code": code, "level": item["level"], "actual": item["value"], "issue": desc, }) rows.sort(key=lambda x: (x["level"], x["host"])) for r in rows: print(f'[{r["level"]}] {r["host"]:20s} {r["code"]} {r["issue"]} (当前值: {r["actual"]})') print(f"\n共 {len(rows)} 项不符合")跑完直接得到一份可以贴进整改通知的清单,按等级和主机排好序。
Windows 侧不用写脚本,系统自带两个工具就够:
:: 导出当前安全策略,与基线 .inf 比对 secedit /export /cfg C:\tmp\current.inf :: 查看本地密码策略实际生效值 net accounts :: 导出组策略结果集,看最终生效配置 gpresult /h C:\tmp\gpresult.html /f把基线做成一个.inf模板,用secedit /configure /db secedit.sdb /cfg baseline.inf就能批量套用。这套东西在域环境里走组策略更省事,非域机器才用 secedit。
五、例外管理:给"改不了"一条正规路
这是整套机制能不能长期跑的关键。
没有例外通道,运维只有两个选择:要么硬改(可能引发故障),要么不理(风险悬空)。有例外通道,就变成第三个选择:走流程、留痕、加补偿措施。
例外单我设计成一张表,五个必填项:
| 字段 | 要求 | 示例 |
|---|---|---|
| 基线编号 | 精确到条目 | LNX-002 |
| 系统/主机 | 明确责任范围 | 旧版 ERP 应用节点 ×2 |
| 无法整改原因 | 必须是客观约束,不能是"没时间" | 对接的旧设备仅支持密码认证 |
| 补偿措施 | 必须可验证 | 仅允许内网 10.0.0.0/8 访问,已加 ACL |
| 有效期 + 复核时间 | 最长 6 个月,到期自动回到待整改 | 至 2026-09-30,9 月复核 |
规则有三条:
- L1 不允许例外,只允许"临时缓解 + 限期整改",且必须业务负责人签字
- 例外必须有补偿措施,没有补偿措施的不批
- 例外到期自动复活,不允许"永久例外"
第 3 条最容易被绕过——很多单位的例外单签完就再没人看。我的做法是把例外单和台账打通,到期日到点自动回到"待整改"状态,跟着周报一起推。
六、接进整改闭环,别做两套流程
基线核查出来的不符合项,本质上就是漏洞。所以不要单独建一套"基线整改流程",直接并入漏洞整改的同一张台账。
字段设计上只要多两列:
| 字段 | 说明 |
|---|---|
| 来源 | 漏扫/基线核查/人工发现/外部通报 |
| 基线编号 | 基线类问题填写,其他为空 |
这样带来三个好处:
- 领导看一张表就知道整体风险,不用听你解释两套体系
- 整改推动的话术、分级时限、复测流程全部复用
- 同一个系统的漏洞和基线问题能一起排期,减少反复打扰业务方
关于整改推动的完整方法(责任矩阵、话术库、七步 SOP),我另外写过一篇,这里不重复展开,两篇配合看就是一条完整链路。
七、怎么衡量基线做得好不好
不要用"核查了多少台"这种过程指标,用下面四个:
| 指标 | 定义 | 参考目标 |
|---|---|---|
| 红线清零率 | L1 不符合项已闭环 / 总数 | 100%,任何时点都不该有存量 |
| 核查覆盖率 | 已纳入自动核查的主机 / 资产总数 | 核心系统 100%,整体 ≥ 90% |
| 平均闭环时长 | 从核查出问题到复测通过 | L1 ≤ 3 天,L2 ≤ 30 天 |
| 例外占比 | 有效期内的例外条目 / 总条目 | < 8%,超过说明基线定得不合理 |
第四个指标最有诊断价值。例外占比长期高于 10%,通常不是运维不配合,而是基线本身脱离实际——要么定得太严(照抄等保条款但没考虑业务),要么没分层(该是 L3 的写成 L1)。
这时候该改的是基线,不是加压。
八、二十条最常被忽略的加固点
最后给一份"补漏清单",都是实际核查里高频出现、但容易被写的清单漏掉的:
- 数据库监听地址是
0.0.0.0而不是127.0.0.1或内网地址 - Redis 无密码且对公网开放(这个组合出了太多事故)
- Elasticsearch 9200 端口无认证暴露
- Tomcat 管理后台
/manager/html用默认口令 - Jenkins 未授权访问(
/script可直接执行命令) - Nacos / Eureka / Consul 等注册中心面板裸奔
- Spring Boot Actuator 的
/env、/heapdump对外可访问 - 应用日志目录全局可读,含数据库连接串明文口令
- 备份文件(
.bak、.sql、.zip)留在 Web 目录 - 版本管理目录
.git/.svn可被下载 - 中间件示例应用未删除(
/examples/、/docs/) - 云主机安全组放通
0.0.0.0/0的 22/3389 - 对象存储桶允许匿名列举
- 内部 API 未做鉴权,仅靠网关路由隔离
- 运维账号共用,无个人实名账号
- 数据库账号权限过大(业务账号带 DBA 权限)
- 定时任务以 root 跑业务脚本
- 日志未集中采集,本地日志保留不足 6 个月
- 证书使用自签名或已过期,未做有效期监控
- 双因素认证未覆盖 VPN、堡垒机、云控制台
这 20 条不需要一次性全推。我的建议是先把第 1、2、3、5 条做成自动扫描,因为它们的高危率极高、修复成本极低(改个配置、加个密码),投入产出比最好。
九、落地顺序建议
如果你现在从零开始,别一上来就写完整清单。按这个顺序走:
- 第一周:只定 L1 红线,不超过 20 条,全部选"能被一条命令验证"的
- 第二周:写核查脚本,先在 3~5 台非核心机器上跑通,修掉脚本误报
- 第三周:全量跑一次,出一份《当前基线现状报告》给领导——这一份报告是后面所有推动的凭据
- 第四周:把不符合项按系统分组,走整改流程,先清 L1
- 第二个月:补 L2 清单,接进例会和周报,形成固定节奏
- 第三个月:上例外审批,把"改不了"的正规化
不要跳过第 3 步。没有一份"现状报告",你就没有推动的抓手,所有要求都会被当成"安全部门又来找事"。
十、写在最后
安全基线是甲方安全里最不性感、但收益最确定的一块。
它不像应急响应那样有故事性,也不像攻防演练那样有戏剧性,但它决定了你的安全水位下限——真实世界里绝大多数被拿下的系统,不是被高级 0day 打穿的,而是因为一个没改的弱口令、一个没关的端口、一个没权限控制的接口。
把基线做成流水线,等于把"靠人盯"变成"靠机制跑"。这件事做完,你才有余力去做那些更需要判断力的活。
基线跑起来之后,往下接的两步:一是把不符合项变成整改任务,方法在漏洞整改推动实操;二是把基线条目对应到测评条款,写法在等保2.0定级备案与差距分析。主机被打穿了的情况,处置顺序看应急响应复盘。
有具体场景想聊的(比如你们环境里某类设备怎么核查),评论区留,我看到会回。