简介:面向运维与安全审计人员的Windows/Linux基线核查脚本资源,用于快速检查主机安全配置项是否符合合规基线,覆盖系统账号权限、口令策略、登录限制、服务端口开放、日志审计、补丁更新等常见核查场景。压缩包共8个文件、182KB,含PowerShell与Shell双平台核查脚本、Windows/Linux基线配置说明文档、config.cfg自定义检查项配置、CSV格式核查结果样例以及README与报错处理说明;用户可参照基线文档在config.cfg中调整检查项,再运行对应平台脚本,一键生成核查结果,便于整改留档与等保测评准备。脚本支持自定义检查项,可灵活适配不同业务环境的安全基线要求;随包附带的CSV样例和报错说明能帮助快速理解输出格式并排查执行异常。已有1171人学习下载,资源体量精简但体系完整,适合需要批量开展主机安全自检、交付安全基线报告或准备等保合规测评的运维工程师与安全工程师使用。 像“这台服务器到底符不符合安全要求”这类问题,很多运维团队都遇到过。等保评测临近、客户安全审计发来表格、或者自己刚接手一批老服务器时,都需要一份能快速摸清系统安全底数的“体检报告”。Windows和Linux两套系统的基线核查,难点从来不是检查项本身有多深奥,而是如何在最短时间内覆盖足够多的检查点,并且让人一眼就能看懂结果。我写过一个用于日常巡检和合规自查的基线核查脚本,覆盖两套主流系统的账户策略、认证安全、系统配置、权限设置和日志审计几个维度,这篇文章就把设计和落地过程中的细节拆开讲清楚。
1. 基线核查脚本到底在查什么:合规需求与自查场景
先说清楚一个概念:所谓“基线”,就是一套双方约定的安全最低标准。对于运营团队来说,这个标准通常来自等保二级或三级要求、CIS(Center for Internet Security)Benchmark,或者是企业自己沉淀下来的加固规范。不管标准来源是哪一套,落到服务器上最终都会变成一条条可核查的配置项,例如密码最长使用期限是多少天、是否禁止Root直接远程登录、审计策略是否开启等等。
有同事问过我:这些检查项手动敲命令也能看,为什么要写脚本?真实原因是效率和数据一致性。十几台、几十台服务器如果靠人肉逐台核查,不仅耗时,还容易因为命令输入差异、状态判断差异得到不一致的结果。尤其当检查项细到几十上百条时,脚本批量执行的价值就会非常明显。
脚本设计之初我先确定了要核查的四大维度:
- 账户与认证安全:包括空密码账户、特权账户、口令策略、登录限制。
- 系统服务与网络暴露:包括不必要的服务、监听端口、远程登录方式。
- 文件与目录权限:包括关键系统文件的属主、属组和权限位。
- 审计与日志策略:包括系统审计是否开启、日志留存周期、登录事件记录情况。
这四个维度基本覆盖了等保和CIS高频核查项。建议初次做基线工具的同学不要贪多,先把这四个维度做扎实,再逐步往中间件、数据库方向扩展。
2. Linux基线核查的核心检查项与设计逻辑
Linux端我选择了Bash脚本实现,主流程遵循“逐项采集、逐项判定、统一输出”的模式。每个检查项封装成一个函数,方便单测也方便后续增删查检项。脚本整体只依赖系统自带的命令,不额外安装软件包,这在离线环境和最小化安装的系统上特别重要。
2.1 用户与特权账户检测:空口令和UID为0的账户是重中之重
check_empty_password() { empty_users=$(awk -F: '($2 == "") {print $1}' /etc/shadow 2>/dev/null) if [ -n "$empty_users" ]; then echo "[FAIL] 发现空口令账户: $empty_users" else echo "[PASS] 未发现空口令账户" fi } check_uid_zero_accounts() { uid_zero=$(awk -F: '($3 == 0) {print $1}' /etc/passwd) if [ "$uid_zero" = "root" ]; then echo "[PASS] 仅root用户UID为0" else echo "[FAIL] 发现非root但UID为0的账户: $uid_zero" fi }空口令账户意味着完全可以不用口令直接登录,危害程度就不用多说了。/etc/shadow中第二字段如果为空,代表这个账户没有口令。之所以用2>/dev/null把报错吞掉,是因为某些容器环境没有shadow文件或者读取受限,宁可当作未知处理也不要让脚本报错中断。
UID为0的账户拥有与root完全相同的权限,这是Linux提权路径里最隐蔽的一种。很多后门程序会添加一个uid=0的普通用户名来混淆视野。这个检查项同时也是CIS Benchmark中的高频项,实现也非常轻量。
其它账户相关检查还包括:
- 锁定或删除不活跃账户(
passwd -S查看状态); - 验证
/etc/passwd中是否有重复UID; - 检查
wheel或sudo组成员是否最小化。
2.2 SSH安全基线:远程登录权限必须收敛
check_ssh_root_login() { permit_root=$(grep -E '^PermitRootLogin' /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}') if [ -z "$permit_root" ]; then echo "[WARN] 未显式配置PermitRootLogin,SSH默认允许root登录,建议显式设置为no" elif [ "$permit_root" = "no" ]; then echo "[PASS] SSH已禁止root直接登录" else echo "[FAIL] SSH允许root直接登录: PermitRootLogin $permit_root" fi }SSH配置是Linux基线里最容易出问题的部分,也是最容易被甲方安全工程师挑出毛病的地方。PermitRootLogin这一项在多数发行版中默认值是prohibit-password,即允许使用密钥登录。不同基线标准对这个值的判定逻辑也有差异,CIS要求是no,而很多企业内部标准允许prohibit-password。脚本设计时要支持通过配置文件自定义期望值,否则换一套基线标准就要改一次代码。
所以我额外做了个配置化处理:把期望值放到baseline_config数组里,检查时读取该配置而不是写死。这样一个脚本在不同客户现场就能通过不同配置适配不同的合规标准。这类经验也被我沉淀到了之后的版本里。
SSH相关的补充检查项:
Protocol是否为2(SSHv1已被淘汰多年);MaxAuthTries是否限制在4次以内;ClientAliveInterval和ClientAliveCountMax是否设置了空闲超时;- 是否配置了
AllowUsers或AllowGroups做白名单限制; - sshd_config中是否引用了不存在的用户或路径。
这些项用grep加一些前置条件判断就能完成,但要注意sshd_config支持include机制,主配置里的某些配置会被子配置覆盖。严谨的做法是用sshd -T导出运行时生效配置,再对生效配置做判断。后来我在v2版本中改成了这种方式,兼容性好了很多。
2.3 系统配置与文件权限:挖出“裸奔”的系统
Linux系统安全配置检查项比较零散,我挑选了几个最能反映系统加固水平的检查项放进脚本。
umask设置:
check_umask() { umask_value=$(grep -E '^\s*UMASK' /etc/login.defs 2>/dev/null | awk '{print $2}') if [ "$umask_value" = "027" ] || [ "$umask_value" = "077" ]; then echo "[PASS] umask设置为 $umask_value" else echo "[FAIL] umask设置为 $umask_value,建议027或077" fi }/etc/login.defs中的UMASK决定了新创建文件的默认权限。027或077能确保新文件不会默认被组内其他用户和其他用户可写。很多tmp目录沦陷、文件被篡改的案例,追根溯源都是umask配置过于宽松。
核心转储、时间同步、补丁更新:
check_core_dump() { core_pattern=$(cat /proc/sys/kernel/core_pattern 2>/dev/null) if [ "$core_pattern" = "core" ]; then echo "[FAIL] 核心转储文件使用默认core命名,建议设置为core.%e.%p" else echo "[PASS] core_pattern已设置: $core_pattern" fi }核心转储(core dump)设置成core或/core/tmp/core之类,存在堆信息被其他用户读取的可能。设置成core.%e.%p这种带执行文件名和PID的格式,既方便排查又降低信息泄露风险。
关键文件权限核查:
check_passwd_perms() { stat_output=$(stat -c "%a %U %G" /etc/passwd 2>/dev/null) echo "[INFO] /etc/passwd 权限与属主: $stat_output" # 判定逻辑:权限位不得为666/777之类,属主必须为root }/etc/passwd、/etc/shadow、/etc/gshadow、/etc/sshd_config、/etc/grub.conf这类文件是攻击者重点关注的对象,权限和属主稍有异常就要引起警觉。stat命令可以快速拿到权限位和属主属组,比ls -l解析输出更稳定。
2.4 Linux检查项的输出规范与退出码设计
输出规范直接关系到后续报告生成的便利性。我给每个检查项都规范了固定格式,便于后续用脚本批量分析:
[时间戳] [主机名] [检查项编号] [结果] [详情] [2025-04-15 10:30:01] [web-server-01] [LNX-001] [FAIL] SSH允许root直接登录: PermitRootLogin yes同时,脚本在最末尾根据FAIL/ WARN/ PASS的数量生成退出码:只要存在FAIL,退出码为1,WARN单独处理为2,全部通过为0。这样对接Zabbix、Prometheus或者自研巡检平台时,可以直接通过退出码判断是否触发告警。
输出支持-o json参数是我后来追加的,因为后期接CMDB或做可视化报表时,JSON比纯文本好解析得多。建议做脚本时一开始就考虑机器可读的输出格式,不然后面改造成本很高。
3. Windows基线核查:PowerShell方案中的策略读取与判断
Windows端的核查脚本我采用PowerShell实现。和Linux不同,Windows配置大量集中在注册表和本地安全策略里,PowerShell原生支持对命令和WMI/CIM对象进行操作,几乎不需要额外的依赖。实现过程中也踩了几个明显的坑,比如本地安全策略的读取方式、以及PowerShell版本的兼容性问题。
3.1 账户策略核查:密码与锁定策略必须读取生效值
function Get-AccountPolicy { $seceditOutput = secedit /export /cfg "$env:TEMP\secpol.cfg" | Out-Null $policy = Get-Content "$env:TEMP\secpol.cfg" $minLen = ($policy | Select-String 'MinimumPasswordLength').ToString().Split('=')[1].Trim() $maxAge = ($policy | Select-String 'MaximumPasswordAge').ToString().Split('=')[1].Trim() $minAge = ($policy | Select-String 'MinimumPasswordAge').ToString().Split('=')[1].Trim() $lockoutThreshold = ($policy | Select-String 'LockoutBadCount').ToString().Split('=')[1].Trim() $lockoutDuration = ($policy | Select-String 'LockoutDuration').ToString().Split('=')[1].Trim() $result = @{ MinPasswordLength = $minLen MaxPasswordAge = $maxAge MinPasswordAge = $minAge LockoutThreshold = $lockoutThreshold LockoutDuration = $lockoutDuration } Remove-Item "$env:TEMP\secpol.cfg" -Force return $result }这一步是Windows基线核查里最典型的做法:通过secedit /export把本地安全策略导出到临时文件,再解析关键字段。需要注意/cfg指定的路径不能是中文或带空格目录,否则导出可能失败或乱码。曾经在中文版Windows Server上导出的cfg文件默认采用系统编码,PowerShell直接读取会乱码,后来统一用Get-Content -Encoding Default读取旧编码文件解决了这个问题。
密码策略部分基线项通常是:
- 密码最小长度不小于8位;
- 密码最长使用期限不大于90天;
- 密码最短使用期限不少于1天;
- 账户锁定阈值不大于5次;
- 锁定时间不少于30分钟。
3.2 安全选项与审计策略:抓出最容易踩线的配置
安全选项核查集中在注册表HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System下面,主要包括:
function Get-SecurityOptions { $systemPolicyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" $options = @{ DontDisplayLastUserName = (Get-ItemProperty -Path $systemPolicyPath -Name "DontDisplayLastUserName" -ErrorAction SilentlyContinue).DontDisplayLastUserName LegalNoticeCaption = (Get-ItemProperty -Path $systemPolicyPath -Name "LegalNoticeCaption" -ErrorAction SilentlyContinue).LegalNoticeCaption LegalNoticeText = (Get-ItemProperty -Path $systemPolicyPath -Name "LegalNoticeText" -ErrorAction SilentlyContinue).LegalNoticeText LimitBlankPasswordUse = (Get-ItemProperty -Path $systemPolicyPath -Name "LimitBlankPasswordUse" -ErrorAction SilentlyContinue).LimitBlankPasswordUse } return $options }这几个安全选项对应等保条款非常直接。DontDisplayLastUserName为1时不显示上次登录用户名,防止本机登录用户信息泄露;LimitBlankPasswordUse为1时禁止使用空密码的账户进行网络登录,这个和Linux空口令检查是同一个思路,只是实现完全不同。
审计策略核查我用auditpol /get /subcategory:*来抓取所有子类别的当前配置,判断关键的几个子类别是否开启了成功和失败审计:
function Get-AuditPolicy { $auditLines = auditpol /get /subcategory:"登录","账户登录","对象访问","进程创建" /r 2>$null $parsed = $auditLines | ConvertFrom-Csv foreach ($item in $parsed) { [PSCustomObject]@{ Category = $item.'子系统' Subcategory = $item.'子类别' SuccessIncluded = $item.'成功事件' FailureIncluded = $item.'失败事件' } } }用auditpol /get /r加上CSV解析,比从注册表读取审计策略值可靠得多,因为审计策略的注册表值在不同系统版本上格式并不统一。这个函数输出的结构化对象,也方便后续通过Export-Csv直接落盘成报告。
3.3 共享与会话防护:容易被忽略却经常违规的检查项
Windows基线核查中还有一类高频检查项和网络暴露相关:
- 共享目录:
Get-SmbShare查看是否存在非默认共享,特别是C$、ADMIN$这类管理共享是否被不必要地开放; - Guest账户状态:
Get-LocalUser -Name Guest | Select-Object Enabled确认Guest账户未启用; - 防火墙状态:
Get-NetFirewallProfile | Select-Object Name, Enabled三个网络配置文件必须都处于启用状态; - 远程桌面会话超时:注册表
HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services下的MaxIdleTime值,确保空闲会话能及时断开。
其中防火墙状态是很容易被忽略的坑。许多应用在安装时会把防火墙对应的域配置文件或专用配置文件临时关闭,导致服务器暴露在不受控网络里。脚本每次运行都会检查三个配置文件是否都启用才算通过,这个逻辑要写死,不给弹性的空间。因为防火墙作为系统级别的出入口管控,一旦某个配置文件的策略被关闭,审计人员一定会给你记一条中危。
共享目录检查还要区分默认管理共享和业务共享。默认管理共享是有盘符路径的共享(C$、D$、IPC$),通常不建议脚本直接判定为FAIL,因为系统正常运行需要少量管理共享存在。但如果有额外创建的、非默认的共享,就需要列出共享名和路径,让运维人员判断是否业务需要。这个弹性很关键,否则一台正常的文件服务器会天天被脚本标红。
4. 脚本输出的自动分析、日报表和后续整改协同
核查脚本做出来后,面临的下一个问题就是:怎么把几十台服务器生成的结果汇总成一份有指导意义的报告。手工打开每台服务器的输出文件显然违背了“自动化”的初衷,所以我写了一个汇总分析模块,把运行结果统一收集后,按“严重/高危/中危/低危/通过”五级归类,输出成Markdown或CSV格式的日报。
这一步的输入协议,就是开头提到的“统一输出格式”。每台服务器执行脚本后生成一份JSON文件,文件名包含服务器IP和日期:
{ "host": "192.168.1.101", "platform": "linux", "timestamp": "2025-04-15T10:30:01+08:00", "results": [ {"id": "LNX-001", "level": "FAIL", "name": "SSH root login", "detail": "PermitRootLogin yes"} ] }汇总脚本读取所有主机的JSON文件后,就会执行下面三个动作:
- 统计每台服务器的合规率,生成排名表格;
- 统计所有服务器上FAIL项出现的频次,找出“共性漏洞”,比如连续三台服务器都开了root远程登录,说明是镜像或模板层面的问题;
- 按风险级别生成整改任务列表,方便分派给对应的系统负责人。
共性漏洞分析这一点,实际用处远比想象中要大。因为运维团队往往使用固定的镜像模板批量开通服务器,模板里如果有一个配置不对,会导致新开通的几十台全部违规。脚本能不能快速暴露这种“模板级”问题,直接决定了整改的工作量。做过大规模服务器交付的团队应该都深有体会——人肉逐台修正几十台服务器的时间,足够重新构建一个安全基线模板了。
日报生成后,还涉及后续的动作闭环。基线核查本质上是一个持续检查和整改的过程,单单暴露问题还不够,需要和工单系统或者运维平台的CI/CD流程联动。这个问题可以从两个角度来落地,一是脚本提供“修复建议”字段,在输出报告的同时自动推荐对应的修复命令;二是把高危项的整改动作通过运维平台的任务系统下发,由负责人确认后执行。
5. 踩过的坑:兼容性、转义与误报处理
脚本写完之后,真正的考验才开始。只有放到各种型号、各种发行版、各种安全软件共存的环境里跑过一轮,才知道基线核查脚本也会遇到各种奇怪情况。下面几个坑是我自己在实际使用中最常遇到的,分享出来希望读者少走弯路。
5.1 Bash脚本在不同发行版上的兼容性
Linux端的脚本我一共在CentOS 7、CentOS 8 Stream、Ubuntu 20.04、Ubuntu 22.04、Debian 11、openEuler 22.03上执行过。最典型的差异有三个:
grep的正则支持不一样:CentOS 7自带的grep对\s转义支持不完整,Ubuntu上的grep -P可以,CentOS上就必须用[[:space:]]这种POSIX字符类,否则匹配结果不对;- 用户组命令
groupmems在Ubuntu上不是默认安装的,检查sudo组成员时要么用getent group sudo替代,要么先探测命令是否存在; /etc/login.defs中字段的缩进和注释风格在不同发行版差异很大,解析时不能太依赖固定模板,最好先用grep -vE '^#|^$'过滤掉注释和空行。
还有一类特殊环境是容器内的检查。在Docker容器里跑脚本时,systemctl命令是不存在的,很多服务状态查询函数会直接报错。我后来在脚本开头做了运行环境探测,如果是容器环境就跳过init相关检查项,只在报告中标注“容器环境跳过”。否则每次容器巡检都会刷出一堆FAIL,很容易把真正的问题淹没。
5.2 Windows脚本的PowerShell版本与执行策略
Windows端的坑主要集中在PowerShell版本和执行策略上。Windows Server 2012默认的PowerShell是4.0,一些新语法(如三元运算符?:)在4.0里不可用,所以脚本要尽量使用兼容性较好的语法。另外,有些安全加固会默认开启受限执行策略(Restricted),脚本运行时会被拦截,需要在交付说明里提前告知客户执行Set-ExecutionPolicy -Scope Process Bypass的方式来临时绕过,而不是修改系统全局策略。
secedit导出策略时还会遇到一个问题:导出的cfg文件用记事本打开是乱码。这个主要是因为本地安全策略采用UTF-16编码存储,直接用Get-Content读取时乱码,需要加上-Encoding Unicode参数。第一次遇到这个问题时我还以为脚本解析失败了,后来确认是PowerShell读取时没指定编码。这个细节看起来小,遇到一次就知道多重要了。
5.3 误报排查:基线脚本的置信度管理
做基线核查最尴尬的场景,是自己脚本报出的FAIL经人工复核后其实是正常状态。这类误报如果频繁出现,会导致运维人员对脚本结果失去信任,最终又回到人肉核查的老路上去。
拿SSH检查举例,早期版本直接grep '^PermitRootLogin' /etc/ssh/sshd_config判断,如果整行被注释掉,grep的结果为空,脚本判WARN。但后来发现有些服务器在/etc/ssh/sshd_config.d/目录下有子配置文件,主配置里没写,子配置里却设置了PermitRootLogin no。后来改用sshd -T导出运行时生效配置后才彻底解决。
umask检查也有类似的误报场景。我最初检查/etc/login.defs中的UMASK值,发现有些机器实际生效的umask是022,和文件里写的不一致。深入排查后才知道Shell的启动文件(/etc/profile、/etc/bashrc)里也设置了umask,最终生效的是启动脚本中的值。这让我意识到,任何配置的核查都应该以“运行时生效值”为准,而不是以默认配置文件为准。为此我还专门在Linux端增加了一个“系统则”检查,确保启动脚本里的umask设置和login.defs保持一致,防止配置“两张皮”。
Windows端也有类似问题,注册表里许多安全选项键值不会立即生效,需要重启或者刷新策略后才会应用。我在报告输出中专门注明“配置已修改,需待策略刷新后复测”这种状态,而不是直接标FAIL或PASS,这样执行整改的同事就不会产生困惑。
5.4 执行性能与超时控制
最后一点是关于脚本运行的速度控制。Linux端几十个检查项全跑一遍通常几秒到十几秒完成,Windows端由于要secedit /export再解析,耗时较长,有时达到三四十秒。在批量执行场景下,需要给脚本设置合理的超时时间。我通常会在调度侧给每个脚本留足五分钟的上限,然后在脚本内部为单台主机的执行时间做计时,超过三分钟的部分会在日志中记录。
给脚本加超时还有一层保护意义:基线核查必须被设计为只读操作,绝不能因为脚本自身的死循环或其他原因导致生产系统出现额外负载。为此Linux脚本中所有耗时命令都加了timeout前缀,例如timeout 5 sshd -T,确保即使在异常环境下单条命令也不会卡死整个巡检流程。Windows端则用PowerShell的Start-Job配合Wait-Job -Timeout来约束执行时间,虽然会产生少量额外系统开销,但带来的稳定性收益非常明显。
6. 把核查动作沉淀成常态化机制
脚本本身就是一次性的检查工具,真正有价值的是让它变成一种常态化机制。在多个项目里实践之后,我坚持一个原则:基线核查必须与周期、责任人、整改动作绑定,否则三个月后合规率一定会回落到整改前的水平。
我建议的落地方式是先建一台跑调度的跳板机,每天固定时间段批量拉取所有服务器的核查结果,汇总成报表推送到群或工单系统。核查频率可以根据风险等级调节:核心业务系统每天查一次,非核心的每三天查一次。每次巡检发现的高危项进入整改流程,由负责人确认后在系统侧执行修复,修复后再次触发核查确认结果。
脚本本身可以持续演进,比如结合CMDB自动发现新上线服务器并自动纳入核查范围、对基线项按需裁剪生成不同规格的“合规套餐”,以及通过API对接外部报告平台统一管理。这些都是脚本能力之外,但能极大提升安全治理效率的周边工作。
最后再分享一个个人经验:基线核查脚本的技术实现并不复杂,难点在于让检查结果真正被团队接受并转化为整改行为。所以从最开始设计输出格式的时候,就要想到使用报告的人——运维人员关心的是能不能快速定位到问题服务器和方法,安全负责人关心的是整体合规率和风险趋势,把这些角色的需求提前考虑到脚本设计里,比事后做一堆分析工具要好用得多。
本文还有配套的精品资源,点击获取