资讯动态

基于Fabric的Linux服务器合规聚合器设计与落地

发布时间:2026/10/21 18:22:48 来源:尧图企业网站定制
手里管着几十台Linux服务器最头疼的不是服务部署而是安全基线能不能统一。今天聊的这套东西核心是用Fabric把安全加固和合规检查攒成一个聚合器来用统一跑、统一收、统一出报告。这篇文章就来拆解一下这个“统一合规性聚合器”是怎么一步步设计和落地的适合正在被安全审计折腾的运维同学也适合想给服务器批量做安全基线加固的朋友。我先把话说在前头Fabric只是一个远程执行工具它本身不懂什么是安全真正干活的是我们写的检查项和加固动作。聚合器的价值在于把分散在几十台机器上的检查结果汇总成一张可读性很强的总表让你知道哪台机器哪一项没过以及为什么没过。围绕这个目标下面的内容会从方案选型讲起把检查项设计、Fabric脚本实现、结果聚合与报表输出以及我实际踩过的坑全部摊开说。1. 方案选型为什么是Fabric来做合规聚合1.1 现状痛点合规检查为什么难落地很多团队的现状是安全基线文档写了一大堆但落到服务器上每台机器的状态千差万别。有人手动改过SSH配置有人装完系统就没动过防火墙有人系统里还跑着弱密码账户。审计一来拿不出全量的合规报告只能在每台机器上敲一遍命令手动比对基线要求效率低得让人头皮发麻。我之前接触过的一个场景里审计要求提供全部服务器的SSH安全配置、密码策略、文件权限等检查结果时间只有三天。当时唯一能想到的“自动化”就是写一堆shell脚本然后一台一台ssh进去执行再把输出手工汇总到Excel里。这种方式的痛苦在于脚本本身不统一某些机器执行报错也不知道检查项和输出格式没有标准化统计的时候要人工反复核对最致命的是脚本执行完没有留痕审计问起来说不清是哪天跑的、用的哪个版本。事实上这类工作如果靠手动本质上是不可能持续做下去的。一两台机器还能照顾到几十台上百台之后合规检查就沦为了应付式抽查。所以做这个聚合器的第一个需求其实很朴素把检查动作、执行范围、结果收集三件事标准化让合规状态变成随时可查询、可量化的数据而不是存在于某台机器上的临时文件里。1.2 Fabric的特点与选型理由选工具的时候我对比过Ansible、Puppet这类配置管理工具也考虑过直接写一套基于paramiko的脚本。Ansible很强大它自带的模块确实能省不少事但有一个问题引入Ansible意味着要维护一套playbook工程对于“只想快速把合规检查和加固跑起来”的场景来说有点杀鸡用牛刀。Puppet和Chef这种偏向持续收敛状态的方案更适合作为长期配置管理平台初期建设成本和运维成本都不低。直接写paramiko脚本连接管理、命令执行、错误处理都要自己造轮子轮子造完基础功能才刚能用也谈不上舒服。Fabric正好卡在中间它本质上是一个基于SSH的远程命令执行库用Python写脚本比shell脚本好维护得多。Fabric 2.x的API设计得很清爽核心就是Connection和run这两个概念。它不尝试替你管理服务器状态只负责把你给的命令稳定地送到目标机器上执行然后把结果带回来。这种“只做执行”的定位恰恰符合我需要的设计——合规检查的核心逻辑本来就应该由我们来定义工具只需要保证执行和结果返回的稳定性。另外还有一点Fabric直接构建在paramiko之上底层SSH的host key管理、连接复用、sftp这些都已经处理好了不需要重复踩一遍ssh连接的那些坑。对于团队里主要是Python背景的同学来说用Fabric写出来的代码可读性和可维护性远高于一堆shell脚本。1.3 整体架构设计整个聚合器的结构是这样的一台控制机作为执行入口上面跑Fabric脚本若干台Linux服务器作为被管对象控制机通过SSH连接到这些机器。控制机上还有一个结果存储层我用的是本地SQLite因为轻量、单文件、不需要额外部署服务。Fabric脚本执行完检查项后会把结果写入SQLite再由聚合模块从数据库里读出所有主机的结果生成统计报表。控制机 ├── fabric脚本检查与加固 ├── sqlite_db检查结果存储 └── report模块聚合与输出 │ │ SSH ▼ 服务器1 服务器2 服务器3 ...这个架构的好处在于被管端不需要安装任何额外的agent只要控制机能ssh过去有执行命令的权限就能纳入检查范围。对于很多暂时不方便装agent的存量环境来说这是最友好的接入方式。同时因为SQLite里存的是标准化后的结构化结果后续不管是制作HTML报告、导出CSV还是接进监控平台的告警逻辑都很容易扩展。2. 合规基线与检查项设计2.1 检查项分类体系做合规聚合器最核心的工作其实是定义“怎么样算合规”。这个定义必须既贴合安全基线的通用要求又能体现自己环境里的实际控制项。我参考公开的安全基线要求再结合自己环境里的实际控制项把检查项分成了六类系统基本信息、用户与认证、SSH安全、网络与防火墙、文件系统权限、内核安全参数、审计与日志。每类下面又拆出具体的检查项每个检查项包含唯一标识、检查命令、预期通过结果、加固命令、超时时间。到这里可能有人会问为什么要把“系统基本信息”也放进来。原因很简单聚合报告里如果只有合规项的通过与否审计老师看的时候看得很累他会想确认这套检查到底是在什么系统版本上跑的。把内核版本、操作系统发行版这类信息作为“信息类”检查项放进去报告的可信度会高很多。2.2 核心检查项落地命令直接上干货以下是我实际用到的部分核心检查项清单这些命令都在CentOS 7/8以及Rocky Linux上实测过。检查项ID检查内容检查命令预期通过sys_kernel内核版本uname -r输出非空ssh_protocolSSH协议版本grep -E ^Protocol /etc/ssh/sshd_config包含Protocol 2ssh_root_login禁止Root直接登录grep -E ^PermitRootLogin /etc/ssh/sshd_configPermitRootLogin nossh_password_auth密码认证策略grep -E ^PasswordAuthentication /etc/ssh/sshd_configPasswordAuthentication nopass_max_days密码最大有效期grep -E ^PASS_MAX_DAYS /etc/login.defsPASS_MAX_DAYS 90pass_min_len密码最小长度grep -E ^PASS_MIN_LEN /etc/login.defsPASS_MIN_LEN 8file_passwd_permpasswd文件权限stat -c %a /etc/passwd644file_shadow_permshadow文件权限stat -c %a /etc/shadow000 或 600sysctl_ip_forward禁止IP转发sysctl -n net.ipv4.ip_forward0sysctl_syn_cookies启用SYN Cookiessysctl -n net.ipv4.tcp_syncookies1auditd_status审计服务状态systemctl is-active auditdactivefirewalld_status防火墙状态systemctl is-active firewalldactive这里有个设计细节值得说一下每条命令都是针对“单点事实”的查询输出最好只有一行这样比对预期值时逻辑最简单。避免在一条命令里做太复杂的组合判断否则结果解析的时候会有很多边缘情况。如果某个配置项需要多条命令综合判断我会拆成多个检查项各自独立记录结果。比如SSH安全就拆了协议版本、root登录、密码认证、端口等好几个检查项。2.3 检查项与加固动作的对应关系检查只是第一步发现问题之后要能改。聚合器里的每个检查项都配了一个可选的加固动作Fabric脚本执行检查后发现不合规可以提示用户也可以在指定模式下自动执行加固。这里必须强调一个原则加固动作默认不自动执行除非用户显式传入--apply-fix参数。因为安全加固里的很多操作比如禁用密码登录、重启sshd是有业务影响的挂掉一个ssh连接就麻烦了。加固动作的写法也很讲究。比如修改/etc/login.defs里的密码策略不能简单echo覆盖整个文件而是用sed -i只替换关键行并且先备份原文件到/root/bak/目录。同样的原则适用于所有涉及配置文件修改的加固项先备份再修改最后重启服务。这个习惯在批量场景里能救命尤其是当你同时加固几十台机器的时候。3. Fabric实战批量检查与自动加固3.1 环境准备与连接配置Fabric 2.x的安装很简单一条pip命令就搞定pip install fabric连接配置我建议统一放在一个配置文件里维护而不是写死在代码中。我是用YAML文件存主机列表和连接参数格式类似下面这样servers: - host: 192.168.10.21 user: ops port: 22 - host: 192.168.10.22 user: ops key_filename: ~/.ssh/id_rsa在Python里用yaml.safe_load读进来再逐个创建Connection对象。之所以用YAML而不是直接写在Python里是为了让后续增删服务器不用改代码只改配置文件对不熟悉Python的同事也更友好。创建连接时有个小点需要注意Fabric 2.x里connect_kwargs要传入key_filename而不是直接把密钥路径放在Connection的初始化参数里稍微容易踩空。from fabric import Connection def get_connection(server_info): return Connection( hostserver_info[host], userserver_info[user], portserver_info.get(port, 22), connect_kwargs{ key_filename: server_info.get(key_filename, ~/.ssh/id_rsa), }, )3.2 批量采集脚本实现检查项的采集逻辑其实很直白遍历所有检查项对每个检查项执行对应的命令比对预期值记录结果。关键要让Fabric的run能从容面对命令失败的情况比如某条命令返回非零退出码我们不希望整个脚本直接崩溃而是要把失败本身记录下来。from fabric import Connection def run_item_check(conn, item, timeout10): 执行单个检查项返回标准化结果 try: result conn.run( item[cmd], warnTrue, hideTrue, timeouttimeout ) output result.stdout.strip() status PASS if item[pass] in output.lower() else FAIL return { host: conn.host, item_id: item[id], item_name: item[name], status: status, output: output, expected: item[pass], } except Exception as exc: return { host: conn.host, item_id: item[id], item_name: item[name], status: ERROR, output: str(exc), expected: item[pass], }warnTrue表示命令返回非零时不抛异常hideTrue则抑制命令本身的输出避免控制台被大量原始输出刷屏。timeout参数很重要后面我会在坑点部分详细展开当目标机器负载异常或SSH响应变慢时没有超时限制的检查可能卡住整个流程。遍历主机和检查项的代码长这样def collect_all(servers, check_items): results [] for srv in servers: conn get_connection(srv) for item in check_items: results.append(run_item_check(conn, item)) conn.close() return results这段代码还可以改进比如用ThreadPoolExecutor做并行连接控制在10个左右并发整体检查时间能大幅缩短。并发千万不要拉得太高否则大量同时建立SSH连接对目标机器的sshd可能造成压力SSH连接还会失败。3.3 加固任务的幂等实现加固脚本和检查脚本的差异不只是“把配置改了就行”更重要的是要设计成幂等同一台机器脚本跑两遍结果应该一致不会因为重复执行产生副作用。也就是说如果一次加固后目标已经是期望状态再次执行时不应当报错也不应当重复产生垃圾配置。比如禁用root直接登录这个动作我会这么写def fix_ssh_root_login(conn): conn.run(mkdir -p /root/bak, warnTrue) conn.run(cp /etc/ssh/sshd_config /root/bak/sshd_config.bak, warnTrue) conn.run( sed -i s/^#*PermitRootLogin.*/PermitRootLogin no/ /etc/ssh/sshd_config, warnTrue, ) conn.run(systemctl restart sshd, warnTrue)这里sed -i的正则同时匹配了“被注释的行”和“已经存在的配置行”不论原来是什么状态执行后都变成PermitRootLogin no这就满足了幂等要求。但如果原文件里这一行被注释掉了sed匹配后会把注释也替换掉目标行生效重启sshd后也不会报错。测试阶段可以先在不重要的机器上跑两遍确认结果一致再全量执行。配置密码策略那类加固也可以用类似方式def fix_password_policy(conn): conn.run(sed -i s/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/ /etc/login.defs, warnTrue) conn.run(sed -i s/^PASS_MIN_LEN.*/PASS_MIN_LEN 8/ /etc/login.defs, warnTrue)注意如果原文件里没有PASS_MIN_LEN这一行sed就找不到替换目标这时候还需要追加行所以在加固前要检查一下文件现有状态或者用一个更稳的做法先用grep判断再决定执行sed还是echo追加。3.4 并行执行提升效率几十台服务器串行执行检查每台哪怕只有15个检查项每项平均3秒算下来就要半小时以上。加上网络延迟会更慢。并行是这个工具落到实际生产环境默认要考虑的事情。我用了concurrent.futures.ThreadPoolExecutor设置最大线程数max_workers10。每台主机作为一个任务单元内部仍然顺序执行该主机的所有检查项。这样既控制了并发总量又让单台主机内的检查结果按顺序排列后续聚合时不会搞乱脉络。from concurrent.futures import ThreadPoolExecutor, as_completed def run_one_host(server, check_items): conn get_connection(server) results [] try: for item in check_items: results.append(run_item_check(conn, item)) finally: conn.close() return results def collect_parallel(servers, check_items): all_results [] with ThreadPoolExecutor(max_workers10) as executor: future_map {executor.submit(run_one_host, srv, check_items): srv for srv in servers} for future in as_completed(future_map): all_results.extend(future.result()) return all_results用as_completed的好处是哪台机器先返回就先处理哪个结果不需要等最慢的那个跑完。出错的任务通过future.result()抛出的异常捕获后记录到该主机的ERROR结果里不影响其他主机的检查。这个实现看起来简单但很实用。4. 合规聚合与报告输出4.1 检查结果数据模型采集出来的数据结构是扁平的列表每一条是一个检查项在某台机器上的结果。聚合之前先把这些数据持久化到SQLite。表结构其实不复杂按“一次检查任务一个检查项一台主机”的粒度来存CREATE TABLE check_results ( task_id TEXT, host TEXT, item_id TEXT, item_name TEXT, status TEXT, output TEXT, expected TEXT, check_time TIMESTAMP );加一个task_id字段每条记录属于哪一次检查任务一目了然。这样可以保留多次检查的历史数据后续做趋势分析时能看出某台机器的合规状态是在变好还是变差不会等到审计才发现一堆问题堆积到最后一刻。实际跑的时候我会在脚本开头生成一个类似20240615_1530的task_id带上时间和序号保证可唯一标识。4.2 聚合统计实现聚合的逻辑其实不复杂但对展示要求比较高。我需要从数据库里读出所有结果按主机分组、按检查项分类然后产生三个层次的视图全局合规率、每台主机的合规率、每个检查项的通过率。import sqlite3 def aggregate_stats(task_id): conn sqlite3.connect(compliance.db) cur conn.cursor() cur.execute( SELECT status, COUNT(*) FROM check_results WHERE task_id ? GROUP BY status , (task_id,)) rows cur.fetchall() stats {total: 0, pass: 0, fail: 0, error: 0} for status, cnt in rows: stats[total] cnt stats[status.lower()] cnt conn.close() return stats更完整的聚合会按主机和检查项分组各查一次。我习惯生成三个结果集一个总览统计、一个主机维度明细表、一个检查项维度明细表。这三个结果集分别对应报告的前段、中段和后段审计的人看起来结构很清晰。4.3 报告输出与展示有了结构化的结果输出报告就只是个模板问题。我在实现中选择了两种输出CSV和HTML。CSV适合给别人继续做数据处理HTML适合直接打开看。HTML报告我用一个简单的Python脚本渲染出一个自包含页面不依赖外网CDN公司内网离线环境也能正常打开这个点在实际使用中相当重要。HTML报告的核心部分是一个红色/绿色状态汇总表每行是一台主机每列是一个检查项单元格显示PASS或FAIL。整块表格用颜色区分红色醒目绿色安心。全局合规率用一个大数字显示在报告顶部一眼就知道整体状况。def generate_html_report(stats, host_rows, task_id): html [] html.append(!DOCTYPE htmlhtmlheadmeta charsetutf-8) html.append(titleCompliance Report %s/title/headbody % task_id) html.append(h1合规性检查报告 %s/h1 % task_id) html.append(p全局合规率: %.1f%%/p % (100.0 * stats[pass] / stats[total])) # 渲染主机明细表 ... html.append(/body/html) return \n.join(html)报告生成后脚本会把CSV和HTML都保存到reports/目录下文件名带上task_id。这样每次检查的结果都被留存审计来的时候直接说出“这是某月某日全量检查的报告”可信度完全不一样。5. 踩坑实录与排查经验5.1 连接与权限类问题第一批遇到的问题是SSH连接失败。一种典型场景是控制机的known_hosts里没有目标主机的指纹连接时会交互式询问是否信任而Fabric脚本是非交互的直接抛异常。解决办法是在connect_kwargs里设置disabled_algorithms或者传入known_hosts参数更简单的做法是在初始化连接时设置connect_kwargs{known_hosts: ~/.ssh/known_hosts}让paramiko自动读取。还有一种做法是主动用conn.open()测试连接如果失败就记录到ERROR结果不让整台主机中断。权限问题是另一个大头。很多检查项要读/etc/shadow、要执行sysctl -w、要重启服务普通用户跑不了。我在设计上要求控制机使用一个具有sudo权限的账号但sudo执行也分情况有的环境sudo需要输入密码这就麻烦了。Fabric里可以这样处理conn.run(sudo cat /etc/shadow, ptyTrue, warnTrue)如果sudo不需要密码直接能通过如果sudo需要密码pytTrue会进入交互模式但脚本里没有交互能力所以还是要靠配置sudo免密。一个务实的做法是提前在所有被管机上配置好指定账号的sudo免密权限这是批量运维工具的基础设施也算不上额外负担。5.2 检查结果不准确问题检查项的结果不准很多时候是命令输出格式的问题。比如grep查配置项如果文件里同时存在“被注释的行”和“生效的行”只有grep会把你想要的那行也匹配出来需要结合grep -E ^PermitRootLogin这种只匹配行首的写法才能避开注释行。但如果原配置用的不是行首空格顶格而是前面有缩进可能需要再多加判断。这类细节在做检查项时要多加验证不能想当然。另一个典型问题是sysctl -n的输出末尾会带换行strip之后没问题但如果某些配置项的输出有多个值或需要精确匹配就不能简单用“包含”判断了。我在数据模型里预留了output和expected字段就是为了让排查的人能一眼看到实际输出和预期值定位问题时不需要重新登录机器去验证。5.3 加固回滚与安全护栏加固操作里最不让人放心的是“改坏了怎么办”。我在设计加固模块时强制给每个修改配置的加固动作增加了备份步骤备份文件统一放在/root/bak/文件名带上主机名和时间戳。回滚脚本也写好了按主机执行cp /root/bak/sshd_config.bak /etc/ssh/sshd_config systemctl restart sshd就能恢复到加固前状态。还有一个安全护栏必须强调加固项默认不执行只有显式传参才执行。我甚至建议在脚本入口处打印警告要求操作者二次确认。我实盘里一次不小心把“禁用密码登录”的加固项直接跑到了批量环境里幸好只是测试服务器但也足够吓出一身冷汗。从那以后我把加固执行拆成了独立脚本只允许传入指定的目标主机列表不能跑到所有主机上这算是一个吃过的亏换来的经验。另外加固过程中如果某一台机器的sshd重启失败要马上停止该批次中后续对同一类配置的修改防止多台机器同时陷入不可用状态。我用一个简单的状态标记来实现某台机器加固后反馈失败就把它标记出来跳过这台机器剩下的加固动作等手动介入。写到最后的一些体会合规检查这种事情做一次容易难的是每次审计来之前都能快速给出准确的结果。我这套基于Fabric的聚合器最实际的价值不是技术多花哨而是把检查流程固定下来了主机列表在YAML里维护检查项在Python里维护结果全部进SQLite报告一键生成历史记录随时可查。整个过程不再依赖某个人临时敲命令也不怕人员变动带走脑子里那些执行细节。有一点我很想分享给正在做同类事情的同学不要一上来就追求把检查项做得非常多非常细先把体系跑通比什么都重要。先做10个最关键的检查项全流程跑通让审计看到报告雏形再根据实际需求慢慢扩展检查项。功能是长出来的不是一次设计出来的。这个聚合器我用到现在检查项已经从第一批的十几个迭代到了几十个每一步迭代都是因为现实有需要而不是为了显得“功能全面”。安全加固这事儿最后拼的还是执行的可重复性和结果的可追溯性。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑