服务器安全基线核查脚本实战:Linux与Windows检查项设计与排错

📅 发布时间:2026/9/7 15:48:49
服务器安全基线核查脚本实战:Linux与Windows检查项设计与排错
简介面向运维安全人员的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: $30{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 auditdUbuntu 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 LANGC在脚本开头固定语言环境。4.3 发行版差异导致的命令不兼容这是 Linux 基线脚本最大的坑。CentOS、Ubuntu、Debian、SUSE 的命令路径和工具集都有差异比如检查项CentOS/RHELUbuntu/Debian查看开放端口ss -tlnpss -tlnp 或 netstat审计服务状态systemctl is-active auditdservice auditd status用户密码状态passwd -S userpasswd -S user 或 chage -l user防火墙firewalldufw我的经验是脚本里做一层命令探测优先使用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 的提示价值又避免每次都要人工核对一大堆无关紧要的告警。这套思路在用了一段时间后整个核查流程的噪音下降得非常明显。最后再分享一个实际使用中的技巧这套脚本跑出来的结果最好在一开始就让各个业务线的技术负责人先过目一遍。因为他们最清楚某个服务器的特殊配置是有意为之还是历史遗留问题。脚本工具负责把问题暴露出来定义和决策还是在人。把流程理顺之后基线核查才能真正从负担变成运维安全体系里一个有价值的环节。本文还有配套的精品资源点击获取