简介:面向运维安全人员的Windows与Linux基线核查脚本资源包,用于帮助企业IT、安全运维及等保合规建设人员快速完成系统安全配置检查。包内整合了Windows与Linux两套核查脚本、配套基线配置文档、配置文件、结果导出报表以及常见报错处理指南,覆盖账户策略、口令复杂度、服务端口、日志审计、远程访问控制、会话超时等关键核查项。压缩包共8个文件,以docx文档、ps1/sh脚本、cfg/txt配置说明和csv结果表格为主,整体仅约182KB,无需复杂部署,适合在服务器维护窗口快速执行。已有1171人学习下载,适合需要规范化开展主机安全自查、合规整改、应急排查或内部审计的运维与安全工程师;脚本输出结果可保存为结构化表格,便于后续整改追踪和证据留档。无论是单机巡检还是批量评估,都能快速定位风险项,并辅助生成整改依据。 做运维这些年,我每年都要经历好几轮的服务器安全检查。系统上线前要查、季度巡检要查、合规审计前更要查,查的内容翻来覆去就是那些东西:服务器密码策略设没设、SSH登录允不允许root、关键文件权限对不对、审计日志开没开。一台两台手工看一眼还行,几十台上百台服务器要是也靠一台台ssh上去敲命令,一天时间眨眼就没了,还容易漏。
后来我干脆把平时检查的项固化成了一套脚本,一条命令跑完,直接输出 PASS / FAIL / WARN 的结果清单。这套脚本分 Windows 和 Linux 两套实现,覆盖了目前绝大多数服务器的基线核查场景。这篇文章我把整个脚本从设计思路到具体实现原原本本拆开讲,包含完整可用的代码片段、我在实际落地过程中踩过的坑、以及针对误报漏报问题的处理经验,给同样被服务器核查折腾的运维朋友一个可以直接抄作业的参考。
1. 整体设计与实现思路
1.1 基线核查到底在查什么
基线核查本质上就是拿服务器的实际配置去对比一个安全基准,看看哪里不达标。这个“基准”在企业实际场景里通常来自两部分:一是行业通用的 CIS Benchmark,二是企业根据自身安全要求自定义的规范。核心检查范围基本固定在下面这几块:
- 身份鉴别:密码长度、复杂度、有效期、账户锁定阈值、远程登录限制
- 访问控制:用户权限、关键文件属主和权限位、远程管理服务开放范围
- 安全审计:审计服务是否开启、日志保留策略、登录事件是否记录
- 入侵防范:开启的服务、监听的端口、危险内核参数、共享目录
- 资源与防护:防火墙状态、磁盘剩余空间(某些场景下作为 WARN 项)
理解了这五个方向,脚本的检查项就有章可循了。我最初的版本只覆盖了密码策略和 SSH 配置,后来按这五类扩充到了三十多个检查项,才算真正满足核查需要。
1.2 为什么选择脚本方案而不是上平台
有人可能会问,现在市面上堡垒机、配置管理平台、商业漏扫工具都有类似的基线核查模块,为什么还要自己写脚本?我的体会是,在两种场景下脚本方案无可替代:一是服务器数量不大但分布散、临时性核查需求多的环境,为查一次基线专门搭一套平台纯属杀鸡用牛刀;二是等保测评、项目验收这类需要提供原始核查数据的场景,对方往往要求看到每条检查项的“期望值 vs 实际值”,自己跑出来的脚本输出恰恰是最好用的佐证材料。
而且脚本方案轻量、透明、可控。不依赖远程代理、不需要在被查机器上装任何软件,改起来也方便,今天想要加一条检查项,改几行代码就能部署。我现在的做法是把它集成到了 Jenkins 的定时任务里,每个月自动跑一轮,输出结果归档,审计要数据时直接拿报告。
1.3 双平台技术选型:Bash 和 PowerShell
既然要同时覆盖 Windows 和 Linux,脚本语言我就分别选了 Bash 和 PowerShell。Linux 上的开发者基本都熟练 Bash,做配置读取、文本解析非常顺手;Windows 上 PowerShell 能直接调用系统接口,从注册表、安全策略、事件日志里取数据都比传统的 bat 脚本靠谱太多。
两套脚本分工明确,但都遵循同一个约定:每条检查项输出统一格式,包含主机名、检查项编号、检查项名称、期望值、实际值和结果,方便后续统一汇总。Linux 脚本输出到标准输出,Windows 脚本输出到文本文件(后续解释原因),但字段结构保持一致,这样我拿到线上几十台机器的结果后,用 Excel 打开筛选 FAIL 项就能迅速定位。
2. 核心检查项拆解与实现细节
2.1 Linux 侧检查项的筛选与优先级
Linux 侧我最终保留了三十多个检查项,按“必须修复”和“建议关注”两个级别分类。必须修复类的典型代表是密码策略、SSH 配置、关键文件权限;建议关注类的包括内核参数、登录超时设置、闲置会话处理。
这里我重点说两个容易被忽略但实际非常重要的检查项。第一个是 /etc/passwd 中 UID 为 0 的非 root 账户,很多管理人员不清楚 UID 0 意味着超级用户权限,只知道看用户名;脚本必须把awk -F: '$3==0{print $1}' /etc/passwd的结果拉出来人工核对。第二个是空密码账户,awk -F: '($2==""){print $1}' /etc/shadow这条命令执行结果如果非空,说明存在无密码账户,这比密码简单更危险百倍。
SSH 配置检查时还要注意区分“主配置值”和“实际生效值”。sshd_config 里主配置会被 /etc/ssh/sshd_config.d/ 目录下的文件覆盖,脚本如果只grep主配置文件很容易误判。我的处理办法是先用sshd -T导出实际生效配置再解析,虽然执行稍慢,但数据可靠。
2.2 Windows 侧检查项的读取方式
Windows 的基线核查比 Linux 要绕一点,很多配置项没有直接的命令行查看入口。我摸索出了一套组合读取方案:
- 密码策略和账户锁定策略:用
secedit /export /cfg导出安全模板,再解析 INF 文件中[System Access]段的字段 - 审核策略:同样从安全模板中解析
[Event Audit]段 - 防火墙状态:
Get-NetFirewallProfile读取三种网络配置文件的启用状态 - 共享目录:
Get-SmbShare筛选非默认共享 - 注册表加固项:直接读注册表对应键值,比如 RestrictAnonymous、LocalAccountTokenFilterPolicy、LM hash 级别
- 屏幕保护锁定:读注册表 HKCU 和 HKLM 下的 ScreenSaveActive、ScreenSaverIsSecure
微软官方文档里每一个配置项在注册表里的路径都能查到,但比较分散。我在脚本里把常用加固项的注册表路径统一维护成了一个哈希表,需要新增检查项时往表里加一行就行,代码结构非常清晰。
2.3 输出格式设计:让结果一眼看懂
核查脚本的输出如果不规范化,几十台机器的结果汇总起来就是一场灾难。我统一约定的输出格式如下:
hostname|check_id|check_name|expected|actual|result|advice每条检查项占一行,字段之间用竖线分隔。result只有三种取值:PASS 表示通过、FAIL 表示不通过、WARN 表示无法自动判断需要人工复核。advice字段是修复建议,比如密码有效期超标的建议命令是chage -M 90 <user>,这样拿到 FAIL 结果后可以直接复制命令去修复,不用再翻知识库。
Linux 下我直接用echo输出到这个格式的文件;Windows 下 PowerShell 输出中文时如果直接管道重定向,容易遇到编码问题,所以我统一用Out-File -Encoding utf8写入文件,实测下来乱码问题基本消除。
3. 实操过程:从标准到脚本的完整落地
3.1 把合规要求翻译成可执行的检查命令
写脚本时最难的不是写代码,而是把安全标准里的条款翻译成一条条可执行的判断逻辑。CIS Benchmark 里写“确保密码最大有效期不超过90天”,脚本里的对应实现就是:
#!/bin/bash # 检查密码最大有效期 pass_max_age=$(grep -E '^PASS_MAX_DAYS' /etc/login.defs | awk '{print $2}') if [ -z "$pass_max_age" ]; then echo "$(hostname)|PASS_MAX_AGE|密码最大有效期|<=90|未设置|FAIL|建议在/etc/login.defs中设置PASS_MAX_DAYS 90" elif [ "$pass_max_age" -le 90 ]; then echo "$(hostname)|PASS_MAX_AGE|密码最大有效期|<=90|$pass_max_age|PASS|" else echo "$(hostname)|PASS_MAX_AGE|密码最大有效期|<=90|$pass_max_age|FAIL|建议执行: chage -M 90 <每个用户>" fi但这里有个细节很多人会忽略:/etc/login.defs 里的 PASS_MAX_DAYS 只对新建用户生效,存量用户的密码有效期存在 /etc/shadow 中。所以只检查 login.defs 并不能反映真实风险。我的脚本会在检查完 login.defs 后,再遍历 /etc/shadow 中的用户,找出密码超期未改的账户,两者结合才算完整。
3.2 Linux 核心脚本:SSH、权限、审计服务一次跑完
SSH 配置是 Linux 基线里权重最高的部分,因为 SSH 是远程管理的主要入口,也是最常被爆破攻击的目标。我的脚本中 SSH 相关检查项包括:
#!/bin/bash echo "==== SSH 安全配置核查 ====" # 使用sshd -T获取实际生效配置,避免被配置文件覆盖导致误判 sshd_effective=$(sshd -T 2>/dev/null) # 检查是否允许root直接登录 permit_root=$(echo "$sshd_effective" | grep -E '^permitrootlogin' | awk '{print $2}') if [ "$permit_root" = "no" ]; then echo "$(hostname)|SSH_ROOT|禁止root直接登录|no|$permit_root|PASS|" else echo "$(hostname)|SSH_ROOT|禁止root直接登录|no|$permit_root|FAIL|建议修改/etc/ssh/sshd_config: PermitRootLogin no" fi # 检查是否允许密码登录 password_auth=$(echo "$sshd_effective" | grep -E '^passwordauthentication' | awk '{print $2}') if [ "$password_auth" = "no" ]; then echo "$(hostname)|SSH_PWD|禁止密码登录|no|$password_auth|PASS|" else echo "$(hostname)|SSH_PWD|禁止密码登录|no|$password_auth|FAIL|建议配置密钥登录后关闭密码认证" fi # 检查最大认证尝试次数 max_tries=$(echo "$sshd_effective" | grep -E '^maxauthtries' | awk '{print $2}') if [ -n "$max_tries" ] && [ "$max_tries" -le 4 ]; then echo "$(hostname)|SSH_TRIES|最大认证尝试次数|<=4|$max_tries|PASS|" else echo "$(hostname)|SSH_TRIES|最大认证尝试次数|<=4|${max_tries:-未设置}|FAIL|建议设置MaxAuthTries 4" fi关键文件权限检查相对简单,但对结果的判断要细致。比如 /etc/shadow 的权限,CIS 标准要求 0000 或者仅 root 可读写(600/640 存在争议),不同发行版默认值不一样,直接写死某个值会导致大量误报。我的做法是先生成默认文件权限基线表,第一次运行时把服务器实际权限存为基线,后续检查只在和基线不一致时报 FAIL,减少无意义的告警。
审计服务检查也需要区分两种情况:CentOS 7 及以上的系统用systemctl is-active auditd,Ubuntu 18.04 之前则通过service auditd status查看。脚本里用systemctl优先、service兜底的方式,兼容性会好很多。
3.3 Windows 核心脚本:PowerShell 搞定策略读取
Windows 侧的核心逻辑围绕安全模板导出。我用 secedit 导出配置后,用 PowerShell 解析其中的关键字段:
# 导出当前安全策略 $tempDir = "$env:TEMP\secpol" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null secedit /export /cfg "$tempDir\secpol.inf" | Out-Null # 读取密码策略相关字段 $content = Get-Content "$tempDir\secpol.inf" $maxPwdAge = ($content | Select-String '^MaximumPasswordAge\s*=').ToString().Split('=')[1].Trim() # secedit中的值是天数,但以负数形式存储 $maxPwdAgeDays = [Math]::Abs([int]$maxPwdAge) if ($maxPwdAgeDays -le 90) { Write-Output "$env:COMPUTERNAME|PWD_AGE|密码最大有效期|<=90|$maxPwdAgeDays|PASS|" } else { Write-Output "$env:COMPUTERNAME|PWD_AGE|密码最大有效期|<=90|$maxPwdAgeDays|FAIL|建议通过secpol.msc设置密码最长使用期限为90天" }secedit 导出的格式有一个比较坑的地方:MaximumPasswordAge 的值用的是负秒数,不是直接的天数。PowerShell 里需要转成绝对值再除以一天的秒数(86400 秒),实际操作时我换算过一次之后就觉得还好,但第一次拿到负数确实容易懵。
注册表加固项是 Windows 基线里检查效率最高的部分,一条命令读一个值,批量处理非常快。比如禁用匿名枚举 SAM 账户:
$restrictAnonymous = (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RestrictAnonymous").RestrictAnonymous if ($restrictAnonymous -eq 1) { Write-Output "$env:COMPUTERNAME|RESTRICT_ANON|禁止匿名枚举SAM账户|1|$restrictAnonymous|PASS|" } else { Write-Output "$env:COMPUTERNAME|RESTRICT_ANON|禁止匿名枚举SAM账户|1|$restrictAnonymous|FAIL|建议设置RestrictAnonymous为1" }Windows 侧脚本有一个天然限制:PowerShell 的权限模型。如果当前终端不是管理员权限,很多注册表键值读不到、secedit 导出也会失败。我建议统一用管理员身份执行,或者在脚本开头加一段自检逻辑,检测到非管理员权限就直接退出并给出提示。
3.4 批量执行与结果汇总
拿单台机器跑完脚本只是第一步,实际工作中面对的是几十上百台机器。Linux 侧我建议直接用 Ansible 批量分发执行,或者写一个简单的 for 循环通过 SSH 远程执行:
#!/bin/bash # 批量在远程Linux服务器上执行核查脚本 for host in $(cat server_list.txt); do echo "===== $host =====" ssh "$host" 'bash -s' < check_linux.sh >> result_all.txt doneWindows 侧如果那批机器都加入了域,可以直接用 PowerShell 远程会话(WinRM)批量调用;不在域里的话,退而求其次用计划任务把脚本推下去再回传结果文件。我个人的经验是 WinRM 在企业环境里经常被安全策略禁用,最省事的反而是让运维同事登录上去双击跑一遍,耗时增加不多,但胜在稳定可靠。
结果汇总上,输出文件统一是带分隔符的文本,我通常会写一个合并脚本,把所有机器结果汇总后生成一个 CSV,再按检查项编号做透视表,FAIL 项一眼就能列出哪些机器哪些配置不达标,直接在修复建议列复制命令去执行。
4. 常见问题与排错实录
4.1 权限不足导致的误报
我最初在 Linux 上测试脚本时,用普通用户身份执行,结果所有跟 /etc/shadow 相关的检查项全部报 FAIL,比如空密码账户检查读不到 shadow 内容就认为是空密码,完全错误。后来在脚本开头加了 UID 检查,非 root 用户直接提示并以不同模式运行——能查的项继续查,必须 root 权限的项标记为 WARN,避免误伤。
Windows 上也踩过类似的坑。PowerShell 执行策略默认可能是 Restricted,脚本文件根本跑不起来;部分注册表路径在 32 位 PowerShell 和 64 位 PowerShell 下重定向行为不同,很容易读到不存在的键值。建议统一用 64 位管理员 PowerShell 执行。
# 脚本开头自检 if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "请以管理员身份运行此脚本" -ForegroundColor Red exit 1 }4.2 中文系统的编码与显示问题
Windows 中文系统上使用 Out-File 输出中文检查项名称,如果不指定编码,默认可能是 GBK 编码,传到 Linux 汇总时乱码得没法看。我统一在脚本里指定Out-File -Encoding utf8,并且所有检查项名称避免使用生僻汉字,尽量用标准术语。
Linux 侧如果系统 locale 不是 UTF-8,脚本里含中文的输出也会出问题。所以我在 Linux 脚本里限制了所有输出字段用英文和数字,只在 advice 列保留部分中文修复提示,并且用export LANG=C在脚本开头固定语言环境。
4.3 发行版差异导致的命令不兼容
这是 Linux 基线脚本最大的坑。CentOS、Ubuntu、Debian、SUSE 的命令路径和工具集都有差异,比如:
| 检查项 | CentOS/RHEL | Ubuntu/Debian |
|---|---|---|
| 查看开放端口 | ss -tlnp | ss -tlnp 或 netstat |
| 审计服务状态 | systemctl is-active auditd | service auditd status |
| 用户密码状态 | passwd -S user | passwd -S user 或 chage -l user |
| 防火墙 | firewalld | ufw |
我的经验是脚本里做一层命令探测,优先使用ss和systemctl,如果命令不存在就回退到netstat和service。另外不同发行版对sshd -T的输出字段大小写处理稍有不同,最好统一转小写再匹配。
4.4 误报的另一个来源:服务端点和解释器差异
我遇到过最隐蔽的问题是某些服务器上安装了多个版本的 Python 或 Bash,脚本开头如果写了#!/bin/bash,但系统默认 bash 版本过低,某些语法(比如[[ ]])解析异常,导致脚本中途退出。后来我在脚本里规避了高级语法,统一用 POSIX 兼容的写法,或者在 Shebang 里指定绝对路径/usr/bin/env bash。
Windows 上类似的坑是 PowerShell 版本差异。Windows Server 2012 自带 PowerShell 4.0,而 Server 2019 是 5.1,部分 cmdlet 参数名不同。脚本里我尽量用兼容性的写法,比如用Get-ItemProperty替代Get-ItemPropertyValue(后者是 PS 5.1 才有的),保证低版本系统也能跑。
4.5 结果复核与人工判断的边界
脚本输出 WARN 的项,设计初衷就是提醒人工复核,不能直接下结论。比如 UID 0 的非 root 账户,可能是厂商预置的管理员账号,也可能是被篡改的后门,脚本只能把现象摆出来,判断需要结合业务背景。再比如开放的端口,某些端口是数据库或中间件必须监听的,脚本的 WARN 只是提示“这里有个端口”,是不是风险完全取决于业务。
我的处理办法是在脚本里维护一个“已知白名单”配置文件,把业务必须监听的端口、必须存在的服务账号提前写好,检查时自动过滤。这样既保留了 WARN 的提示价值,又避免每次都要人工核对一大堆无关紧要的告警。这套思路在用了一段时间后,整个核查流程的噪音下降得非常明显。
最后再分享一个实际使用中的技巧:这套脚本跑出来的结果,最好在一开始就让各个业务线的技术负责人先过目一遍。因为他们最清楚某个服务器的特殊配置是有意为之还是历史遗留问题。脚本工具负责把问题暴露出来,定义和决策还是在人。把流程理顺之后,基线核查才能真正从负担变成运维安全体系里一个有价值的环节。
本文还有配套的精品资源,点击获取