ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

服务器安全基线核查实战:Linux与Windows自动化检查脚本设计

2026/9/7 17:40:03 拓冰建站 浏览量
服务器安全基线核查实战:Linux与Windows自动化检查脚本设计 简介面向运维与安全审计人员的Windows/Linux基线核查脚本资源用于快速检查主机安全配置项是否符合合规基线覆盖系统账号权限、口令策略、登录限制、服务端口开放、日志审计、补丁更新等常见核查场景。压缩包共8个文件、182KB含PowerShell与Shell双平台核查脚本、Windows/Linux基线配置说明文档、config.cfg自定义检查项配置、CSV格式核查结果样例以及README与报错处理说明用户可参照基线文档在config.cfg中调整检查项再运行对应平台脚本一键生成核查结果便于整改留档与等保测评准备。脚本支持自定义检查项可灵活适配不同业务环境的安全基线要求随包附带的CSV样例和报错说明能帮助快速理解输出格式并排查执行异常。已有1171人学习下载资源体量精简但体系完整适合需要批量开展主机安全自检、交付安全基线报告或准备等保合规测评的运维工程师与安全工程师使用。 像“这台服务器到底符不符合安全要求”这类问题很多运维团队都遇到过。等保评测临近、客户安全审计发来表格、或者自己刚接手一批老服务器时都需要一份能快速摸清系统安全底数的“体检报告”。Windows和Linux两套系统的基线核查难点从来不是检查项本身有多深奥而是如何在最短时间内覆盖足够多的检查点并且让人一眼就能看懂结果。我写过一个用于日常巡检和合规自查的基线核查脚本覆盖两套主流系统的账户策略、认证安全、系统配置、权限设置和日志审计几个维度这篇文章就把设计和落地过程中的细节拆开讲清楚。1. 基线核查脚本到底在查什么合规需求与自查场景先说清楚一个概念所谓“基线”就是一套双方约定的安全最低标准。对于运营团队来说这个标准通常来自等保二级或三级要求、CISCenter for Internet SecurityBenchmark或者是企业自己沉淀下来的加固规范。不管标准来源是哪一套落到服务器上最终都会变成一条条可核查的配置项例如密码最长使用期限是多少天、是否禁止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提权路径里最隐蔽的一种。很多后门程序会添加一个uid0的普通用户名来混淆视野。这个检查项同时也是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] 未显式配置PermitRootLoginSSH默认允许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是否为2SSHv1已被淘汰多年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退出码为1WARN单独处理为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:0108: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对接外部报告平台统一管理。这些都是脚本能力之外但能极大提升安全治理效率的周边工作。最后再分享一个个人经验基线核查脚本的技术实现并不复杂难点在于让检查结果真正被团队接受并转化为整改行为。所以从最开始设计输出格式的时候就要想到使用报告的人——运维人员关心的是能不能快速定位到问题服务器和方法安全负责人关心的是整体合规率和风险趋势把这些角色的需求提前考虑到脚本设计里比事后做一堆分析工具要好用得多。本文还有配套的精品资源点击获取