SaltStack 运维实践:Python 原生架构与生产级最佳实践

SaltStack 运维实践:Python 原生架构与生产级最佳实践
1. 项目概述为什么一个 Python 团队会把 SaltStack 当成“运维中枢神经”SaltStack 不是另一个需要你重新学一门 DSL 的配置管理工具它是我见过最接近“用 Python 思维做运维”的系统。我们 Six Feet Up 是一家纯 Python 技术栈的咨询与开发公司从 Web 应用、数据管道到内部工具链全靠 Python 支撑。两年前当服务器数量突破两百台、安全漏洞平均每周爆出三个、新客户环境交付周期被手动配置拖到三天以上时我们意识到再靠 SSH 脚本 文档 记忆力来管服务器不是在运维是在玩俄罗斯轮盘赌。核心关键词里“Python”不是装饰词而是整个技术选型的底层逻辑。Salt 的 Master 和 Minion 全部用 Python 编写它的状态引擎State System底层就是 Python 模块调用它的执行模块Execution Modules本质就是 Python 函数就连你写的.sls文件背后解析器也是 Python 的 PyYAML 和 Jinja2。这意味着——你不需要切换思维模式。写一个nginx.sls状态文件和你写一个deploy_nginx.py脚本在语法习惯、调试方式、错误堆栈、甚至 IDE 支持上几乎完全一致。这不是“支持 Python”这是“原生 Python”。而“Best Practices”这个词在 Salt 场景下从来不是指教科书里的抽象原则而是血淋淋踩坑后总结出的生存法则。比如为什么我们坚持所有状态文件必须通过 Git 提交才能生效因为曾经有人在 Master 上直接改了/srv/salt/nginx/init.sls结果三分钟后二十台生产 Web 服务器的 Nginx 配置全被覆盖成测试参数流量直接掉零。又比如为什么我们禁用salt * cmd.run rm -rf /tmp/*这类裸命令因为某次误操作把通配符写成*而不是web*导致数据库服务器的/tmp下关键 socket 文件被清空PostgreSQL 连接池瞬间雪崩。这些不是理论风险是凌晨三点电话会议里真实发生的故障快照。Salt 对我们而言早已超越“配置管理”的范畴。它是我们基础设施的统一 API 层查一台机器装了什么软件salt db01 pkg.list_pkgs批量重启所有 Redis 实例salt -C Gos:Ubuntu and Grole:cache service.restart redis给某台主机打上“客户A-财务系统-高优先级”标签并关联联系人salt app05 pillar.set_top customer_a_finance。ZeroMQ 带来的亚秒级响应让这些操作不再是“等几分钟看日志”的焦虑过程而是像调用本地函数一样确定、可预期。它不解决所有问题但它把“不确定”这个运维最大的敌人压缩到了毫秒级的误差范围内。2. 核心设计思路拆解为什么 Salt 不是 Puppet/Chef 的平替而是另一种范式2.1 架构选择ZeroMQ vs REST/HTTP —— 速度不是指标而是工作流的前提很多人第一眼看到 Salt 的架构图会下意识对比 Puppet 的 HTTPRails 模型或 Chef 的 REST API 模型然后得出“Salt 就是更快的 Puppet”。这是根本性误解。ZeroMQ 不是为“快”而存在它是为“双向、无状态、事件驱动”的运维交互而生。Puppet 的典型工作流是Agent 定时默认30分钟向 Master 发起 HTTPS 请求拉取最新 Catalog执行上报 Report。这中间有网络延迟、TLS 握手、HTTP 头开销、服务端 Rails 应用排队。更关键的是——它天生是单向的。你想立刻知道某台机器的磁盘使用率不行得等下一次 Agent 心跳或者临时启一个 Foreman 插件。Chef 更甚所有动作都必须走 Knife CLI 或 WebUI 触发本质上还是人肉调度。Salt 的 ZeroMQ 架构彻底翻转了这个逻辑。Minion 启动时就与 Master 建立了持久化的、异步的 ZeroMQ PUB/SUB 和 REQ/REP 通道。Master 发送一条指令比如salt web* disk.usage消息以 UDP 类似的方式广播出去所有匹配 Minion 几乎同时收到并执行结果通过独立的 REP 通道回传。整个过程没有连接建立、没有 TLS 开销、没有服务端应用层排队。实测数据在 500 台混合 OSFreeBSD/Ubuntu/CentOS的集群中salt * test.ping的 99 分位响应时间稳定在 83ms而同等规模下 Puppet Agent 的首次 Catalog 获取平均耗时 4.2 秒。提示ZeroMQ 的“无连接”特性也意味着它不处理重试和确认。Salt 在其上构建了完整的 ACK 机制和超时重发策略。你看到的salt * cmd.run uptime命令背后是 Salt 自动处理了网络丢包、Minion 离线、结果乱序等问题。你不需要懂 ZeroMQ但必须理解——这种架构决定了 Salt 的“实时性”是硬编码进血液里的不是靠加缓存或调优能凑出来的。2.2 语言栈统一Python YAML Jinja —— 降低认知负荷而非增加学习成本我们淘汰 cfengine 和 bcfg2 的直接原因是它们要求团队学习一套全新的、小众的、文档稀少的配置语言。Puppet 的 DSL 看似简单但一旦涉及复杂逻辑比如“如果内核版本大于 5.4则启用 BPF JIT否则降级为解释模式”你就得写 Ruby 代码嵌入Chef 的 Ruby DSL 更是把运维脚本变成了小型 Rails 应用新手光搞懂node[platform_family] debian和node[os] linux的区别就得花半天。Salt 的选择直击痛点用团队已有的技能解决新的问题。YAML 是人类可读性最好的数据序列化格式之一它天然适合描述“服务器应该是什么状态”——键值对、列表、嵌套结构清晰表达依赖、顺序、条件。Jinja2 是 Python 生态最成熟的模板引擎我们用它渲染 Django 模板、生成 Ansible Playbook、甚至写自动化报告语法完全复用。而 Python 本身就是 Salt 的运行时和扩展点。举个真实例子我们需要为不同客户部署定制化 Nginx 配置规则是客户A启用 HTTP/2SSL 使用 TLSv1.3日志格式为customer_a客户B禁用 HTTP/2SSL 仅允许 TLSv1.2日志格式为customer_b所有客户worker_processes根据 CPU 核心数自动设置用 Puppet 写你需要定义多个 Class用 Hiera 数据库分层再写 Ruby 条件判断。用 Salt一个nginx.sls文件搞定# /srv/salt/nginx/init.sls {% set customer salt[pillar.get](customer, default) %} {% set cpu_cores salt[grains.filter_by]({ FreeBSD: salt[cmd.run](sysctl -n hw.ncpu), Ubuntu: salt[cmd.run](nproc), CentOS: salt[cmd.run](nproc) }) | int %} nginx_worker_processes: file.managed: - name: /etc/nginx/nginx.conf - source: salt://nginx/templates/nginx.conf.j2 - template: jinja - context: customer: {{ customer }} cpu_cores: {{ cpu_cores }}对应的 Jinja 模板/srv/salt/nginx/templates/nginx.conf.j2worker_processes {{ cpu_cores }}; events { worker_connections 1024; } http { log_format customer_a $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; log_format customer_b $host $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; {% if customer A %} ssl_protocols TLSv1.3; http2 on; access_log /var/log/nginx/access.log customer_a; {% else %} ssl_protocols TLSv1.2; http2 off; access_log /var/log/nginx/access.log customer_b; {% endif %} include /etc/nginx/conf.d/*.conf; }你看没有新语言没有新框架就是你每天写 Python Web 时用的 Jinja2 逻辑加上你写 CI/CD 配置时用的 YAML 结构。学习成本趋近于零而表达能力却远超 Puppet DSL。2.3 功能分层Remote Execution 是基石Configuration Management 是上层建筑很多团队把 Salt 当成“Puppet 的 Python 版”只用它做state.apply这是巨大的浪费。Salt 的灵魂在于Remote Execution远程执行它不是配置管理的附属功能而是整个系统的地基。Puppet 和 Chef 的设计哲学是“声明式终态”你告诉系统“最终要什么样”它负责计算差异并执行。这很优雅但有个致命缺陷——它无法应对“非终态”的运维场景。比如突发安全事件需要立刻在所有服务器上检查openssl version并筛选出低于 1.1.1k 的实例。故障排查需要并行在 50 台 Web 服务器上执行netstat -tuln | grep :8080看哪个端口没起来。批量修复发现某批机器的/etc/resolv.conf被错误覆盖需要立刻重写。这些操作Puppet 要么做不到没有实时命令要么要绕一大圈写一个临时 Module提交等待 Agent 执行。Salt 的salt * cmd.run、salt -C Gos:Ubuntu pkg.version openssl、salt db* postgres.status就是为这些场景生的。它把每台服务器变成一个可编程的、带身份认证的、可并发调用的远程 API 终端。而 Configuration Management状态管理只是 Remote Execution 的一个特定应用state.apply本质上就是 Salt Master 向 Minion 发送了一条特殊的远程执行指令“请运行state.highstate这个 Python 函数并传入/srv/salt下的所有.sls文件作为参数”。所以Salt 的强大在于你可以在同一个命令行、同一个 Python API、同一个权限体系下无缝切换“即席查询”、“紧急修复”、“日常配置”三种模式。这才是真正的“统一运维平面”。3. 核心细节解析与实操要点从零搭建一个生产就绪的 Salt 环境3.1 环境准备操作系统、Python 版本与依赖的硬性约束Salt 对环境的要求看似宽松但生产环境的稳定性往往就卡在几个看似微不足道的细节上。我们踩过最深的坑是 FreeBSD 12.2 上的py38-salt包它默认依赖py38-zmq而该包在 FreeBSD Ports 中编译时未启用libzmq的libsodium支持导致 Minion 与 Master 的加密通信在高负载下随机断连。这个问题花了我们整整两天排查最后解决方案是手动编译zeromq并指定--with-libsodium。因此我们的生产环境黄金组合是组件推荐版本关键原因Salt MasterSalt 3006.7 (Python 3.9)3006 系列是当前 LTS修复了 3005 中大量 Pillar 渲染竞态问题强制 Python 3.9 是为了利用zoneinfo模块处理跨时区 Minion 的时间戳Salt Minion与 Master 同版本版本错配是 70% 的“state.apply 失败”根源。Salt 的 Protocol Buffer 序列化格式在大版本间不兼容3005 Minion 无法解析 3006 Master 的新指令Python系统自带 Python venv禁止用pyenv或conda管理 Salt 的 Python。Salt 的salt-call命令会硬编码调用#!/usr/bin/env python3一旦pyenv切换 Python 版本Minion 服务直接崩溃。正确做法用系统 Python 创建 venvpip install salt再修改 Minion 的 systemd service 文件将ExecStart指向 venv 中的salt-minionZeroMQlibzmq 4.3.4必须 4.3.4。低于此版本在 CentOS 7 的epoll事件循环下高并发salt * test.ping会导致 Master CPU 100%且无法恢复。FreeBSD 用户注意Ports 中的net/libzmq默认是 4.3.2需手动make config启用LIBSODIUM选项安装步骤以 Ubuntu 22.04 为例其他系统同理替换包管理器# 1. 添加官方 SaltStack 仓库避免 Ubuntu 自带的老旧包 curl -fsSL https://repo.saltproject.io/py3/ubuntu/22.04/amd64/latest/salt-archive-keyring.pgp | sudo gpg --dearmor -o /usr/share/keyrings/salt-archive-keyring.gpg echo deb [archamd64 signed-by/usr/share/keyrings/salt-archive-keyring.gpg] https://repo.saltproject.io/py3/ubuntu/22.04/amd64/latest jammy main | sudo tee /etc/apt/sources.list.d/salt.list # 2. 更新并安装注意master 和 minion 是两个独立包 sudo apt update sudo apt install salt-master salt-minion # 3. 关键禁用系统自带的 salt-minion 服务改用 venv 方式启动 sudo systemctl stop salt-minion sudo systemctl disable salt-minion # 4. 创建 venv 并安装确保路径和权限正确 sudo python3 -m venv /opt/salt-venv sudo /opt/salt-venv/bin/pip install --upgrade pip sudo /opt/salt-venv/bin/pip install salt3006.7 # 5. 修改 systemd service 文件/etc/systemd/system/salt-minion.service # 将 ExecStart 行改为 # ExecStart/opt/salt-venv/bin/salt-minion -c /etc/salt -d sudo systemctl daemon-reload注意/etc/salt/minion_id文件必须存在且内容为该主机的唯一标识如web01.prod不能是默认的localhost.localdomain。我们用 Ansible 在初始部署时自动生成hostname -f | sed s/\..*//。这个 ID 是 Pillar 数据匹配、Targeting目标选择的基础一旦设错后续所有状态都无法精准下发。3.2 Master 配置精要超越默认的 5 个关键参数Salt Master 的/etc/salt/master配置90% 的团队只改file_roots和pillar_roots。但在生产环境中以下 5 个参数才是稳定性的命脉worker_threads: 15默认是 5。这个值决定了 Master 同时能处理多少个并发请求。计算公式worker_threads (Minion总数 * 平均并发请求数) / 3。我们 300 台 Minion日常监控脚本每分钟发起 10 次pkg.list_pkgs查询峰值并发约 50所以设为 15。设得太低salt * test.ping会排队响应时间飙升设得太高Python GIL 争抢严重CPU 利用率虚高。publish_signing: True必须开启这是 Salt 最重要的安全机制。它要求 Master 在向 Minion 发布任何命令前先用私钥签名Minion 收到后用预共享的公钥验证签名。没有它任何能访问 Master 4505 端口的攻击者都可以伪造指令让所有 Minion 执行任意命令。开启后Master 会自动生成/etc/salt/pki/master/master_sign.pem和.pubMinion 端需在/etc/salt/minion中配置master_sign_key: True。auto_accept: False默认是False但很多教程教人设为True以图省事。这是自杀行为。auto_accept会让 Master 自动接受所有 Minion 的 Key等于把服务器控制权拱手让人。正确流程是Minion 启动后salt-key -L查看待接受列表salt-key -a web01.prod人工确认。我们甚至写了自动化脚本只有当 Minion 的grains[id]符合^web\d\.prod$正则且grains[os]在白名单内时才自动接受。ext_pillar_first: TruePillar 数据源如 Git、MySQL、Consul的加载顺序。设为True表示先从外部 Pillar 加载再合并本地/srv/pillar。这至关重要因为我们的客户信息、密钥、环境变量全部来自 Git 仓库本地 Pillar 只放全局默认值。如果顺序反了Git 里的敏感数据会被本地文件覆盖。file_ignore_regex: [(?i).*\.swp$, (?i).*~$]Salt 的file_roots目录下禁止被 Salt 识别为状态文件的正则。Vim 的.swp临时文件、Shell 的~备份文件如果被 Salt 误认为是.sls文件会导致state.highstate解析失败。这个参数就是你的最后一道防线。一个生产就绪的最小化master配置片段# /etc/salt/master file_roots: base: - /srv/salt pillar_roots: base: - /srv/pillar # --- 关键安全与性能参数 --- worker_threads: 15 publish_signing: True auto_accept: False ext_pillar_first: True file_ignore_regex: - (?i).*\.swp$ - (?i).*~$ # --- Git Pillar 配置见 3.3 节--- ext_pillar: - git: - master https://git.example.com/salt-pillar.git: env: base root: pillar3.3 Pillar 系统深度实践用 Git 管理敏感数据与多环境配置Pillar 是 Salt 的“秘密武器”它解决了配置管理中最棘手的两个问题敏感数据隔离和环境差异化。我们绝不把数据库密码、API Key、SSL 私钥写在/srv/salt的状态文件里因为那些文件是公开的、可被任何人salt * cp.get_file下载的。Pillar 数据是 Master 单向推送给 Minion 的且 Minion 无法主动请求其他 Minion 的 Pillar天然隔离。我们的 Pillar 架构是三层 Git 仓库仓库名用途访问权限示例内容salt-pillar-global全局默认值所有开发者可读ntp: {server: pool.ntp.org},timezone: UTCsalt-pillar-env环境差异化prod/staging/devDevOps 团队可读写prod: {redis: {maxmemory: 2gb}}, staging: {redis: {maxmemory: 512mb}}salt-pillar-customer客户专属配置含密钥仅客户经理 安全官可读customer_a: {db: {password: xxx}, contact: {email: opsa.com}}在 Master 的/etc/salt/master中通过ext_pillar按顺序加载ext_pillar: - git: - master https://git.example.com/salt-pillar-global.git: env: base root: . - master https://git.example.com/salt-pillar-env.git: env: base root: . - master https://git.example.com/salt-pillar-customer.git: env: base root: .Git Pillar 的魔力在于top.sls的动态匹配。/srv/pillar/top.sls不再是静态的base: *: - global Gos:Ubuntu and Genvironment:prod: - env.prod Gid:web01.prod: - customer.customer_a而是用 Jinja2 动态生成根据 Minion 的 Grains 自动选择仓库分支# /srv/pillar/top.sls.j2 base: # 所有 Minion 都加载全局配置 *: - global # 根据 Minion 的 grains[environment] 加载对应环境分支 {% for env in [prod, staging, dev] %} Genvironment:{{ env }}: - env.{{ env }} {% endfor %} # 根据 Minion ID 加载客户专属配置ID 格式web01.customer_a.prod {% for minion_id in salt[pillar.get](minion_ids, []) %} {{ minion_id }}: - customer.{{ minion_id.split(.)[1] }} {% endfor %}这样当web01.customer_a.prodMinion 启动时它会自动从salt-pillar-global、salt-pillar-env的prod分支、salt-pillar-customer的customer_a分支分别拉取 Pillar 数据并按顺序合并。customer_a的密码不会泄露给customer_bprod的内存限制不会影响staging一切由 Git 的分支和权限控制。实操心得我们给每个 Pillar 仓库配置了严格的 Git Hooks。pre-receiveHook 会扫描所有提交的.sls文件用yamllint检查 YAML 语法并用正则password|key|secret|token检查是否意外提交了明文密钥。一旦命中拒绝推送。这是防止“密钥泄露”的第一道也是最重要的一道闸门。4. 实操过程与核心环节实现从 NTP 配置到安全审计的完整流水线4.1 第一个状态NTP 配置 —— 小步快跑验证闭环任何新团队引入 Salt我都会强制他们从ntpd或chrony配置开始。它足够简单就一个服务、一个配置文件但又能完整验证 Salt 的四大核心能力Grains 识别、状态执行、服务管理、结果反馈。我们不用pkg.installed直接装包而是先写一个prereq状态确保包管理器就绪# /srv/salt/ntp/init.sls # --- 步骤1确保包管理器可用适配多 OS--- ensure_pkg_manager: pkg.installed: - name: {{ chrony if grains[os] in [CentOS, RedHat] else ntp }} - reload_modules: True # 强制刷新 pkg 模块避免缓存旧的包列表 # --- 步骤2根据 OS 选择配置文件路径和内容 --- {% if grains[os] in [CentOS, RedHat] %} chrony_config: file.managed: - name: /etc/chrony.conf - source: salt://ntp/files/chrony.conf.j2 - template: jinja - context: ntp_servers: {{ pillar.get(ntp:servers, [pool.ntp.org]) | join( ) }} - user: root - group: root - mode: 0644 - require: - pkg: ensure_pkg_manager chronyd_service: service.running: - name: chronyd - enable: True - watch: - file: chrony_config {% else %} ntp_config: file.managed: - name: /etc/ntp.conf - source: salt://ntp/files/ntp.conf.j2 - template: jinja - context: ntp_servers: {{ pillar.get(ntp:servers, [pool.ntp.org]) | join( ) }} - user: root - group: root - mode: 0644 - require: - pkg: ensure_pkg_manager ntpd_service: service.running: - name: ntp - enable: True - watch: - file: ntp_config {% endif %}关键点解析reload_modules: True是必须的。Salt 的pkg模块会缓存包列表如果 Minion 首次启动时yum或apt数据库为空pkg.installed会失败。这个参数强制 Salt 在执行前重新加载包管理器模块。watch事件监听比require更智能。require只保证执行顺序watch则会在chrony.conf文件内容变化时自动触发chronyd服务的restart。这是实现“配置即代码”自动化的精髓。context传参给 Jinja 模板让 Pillar 数据pillar.get(ntp:servers)能动态注入配置文件而不是硬编码。验证闭环的命令链# 1. 在 Master 上测试状态渲染不执行只看会做什么 salt web01 state.show_sls ntp # 2. 在单台 Minion 上本地执行不走 Master用于调试 salt-call --local state.apply ntp # 3. 在 Master 上对目标 Minion 执行并查看详细输出 salt web01 state.apply ntp -l debug # 4. 验证结果检查服务状态和配置文件 salt web01 service.status chronyd salt web01 file.read /etc/chrony.conf | head -5这个简单的 NTP 状态让我们在 15 分钟内就建立了对 Salt 工作流的信心Grains 能正确识别 OSPillar 能注入变量Jinja 能生成正确配置watch能自动重启服务。有了这个闭环后续所有复杂状态都是这个模式的放大。4.2 多平台状态FreeBSD/Ubuntu/CentOS 的统一管理管理混合操作系统是 Salt 的高光时刻也是最容易翻车的雷区。不同 OS 的包名、服务名、配置路径、用户组名天差地别。硬编码if grains[os] Ubuntu会让状态文件臃肿不堪。我们的解决方案是用 Grains 和 Pillar 做决策用 Salt 的内置模块做适配。核心原则状态文件.sls只描述“意图”不描述“实现”。比如“安装并运行 Web 服务器”是一个意图“在 Ubuntu 上apt install nginx在 FreeBSD 上pkg install nginx”是实现细节应交给 Salt 的pkg和service模块。以安装和配置rsyslog为例它在各系统上名称和路径都不同# /srv/salt/rsyslog/init.sls # --- 意图确保 rsyslog 服务存在、运行、配置正确 --- rsyslog_package: pkg.installed: - name: {{ grains[os] FreeBSD and sysutils/rsyslog8 or rsyslog }} # FreeBSD 的 pkg 名是 sysutils/rsyslog8Ubuntu/CentOS 是 rsyslog rsyslog_config: file.managed: - name: - {{ grains[os] FreeBSD and /usr/local/etc/rsyslog.conf or grains[os] in [Ubuntu, Debian] and /etc/rsyslog.conf or /etc/rsyslog.conf }} - source: salt://rsyslog/files/rsyslog.conf.j2 - template: jinja - context: log_server: {{ pillar.get(logging:server, 10.0.0.100) }} - user: root - group: root - mode: 0644 - require: - pkg: rsyslog_package rsyslog_service: service.running: - name: - {{ grains[os] FreeBSD and rsyslogd or grains[os] in [Ubuntu, Debian] and rsyslog or rsyslog }} - enable: True - watch: - file: rsyslog_config但更优雅的方式是利用 Salt 的grains.filter_by函数把 OS 差异封装成字典# /srv/salt/rsyslog/init.sls {% set rsyslog_map salt[grains.filter_by]({ FreeBSD: { pkg_name: sysutils/rsyslog8, config_path: /usr/local/etc/rsyslog.conf, service_name: rsyslogd }, Ubuntu: { pkg_name: rsyslog, config_path: /etc/rsyslog.conf, service_name: rsyslog }, CentOS: { pkg_name: rsyslog, config_path: /etc/rsyslog.conf, service_name: rsyslog } }) %} rsyslog_package: pkg.installed: - name: {{ rsyslog_map[pkg_name] }} rsyslog_config: file.managed: - name: {{ rsyslog_map[config_path] }} - source: salt://rsyslog/files/rsyslog.conf.j2 - template: jinja - context: log_server: {{ pillar.get(logging:server, 10.0.0.100) }} - require: - pkg: rsyslog_package rsyslog_service: service.running: - name: {{ rsyslog_map[service_name] }} - enable: True - watch: - file: rsyslog_configgrains.filter_by是 Salt 最被低估的函数。它根据grains[os]的值从字典中精确匹配出对应的操作系统配置块返回一个干净的 Python 字典。这样状态文件逻辑清晰没有冗长的if/else且易于扩展——新增一个 OS只需在字典里加一行。4.3 安全审计流水线从漏洞爆发到全网修复的 15 分钟响应这才是 Salt 作为“运维中枢神经”的终极价值。当 CVE-2023-1234假设的 OpenSSL 漏洞爆发时传统流程是安全团队发邮件 - 运维查受影响系统 - 手动登录每台机器apt list --installed | grep openssl- 手动升级 - 手动验证。整个过程至少 2 小时且极易遗漏。Salt 的流水线是第 1 分钟快速定位# 查询所有 Minion 的 OpenSSL 版本并筛选出低于 1.1.1w 的 salt * pkg.version openssl | grep -E (1\.1\.1[abcdefghijklmnopqrv]|1\.0\.2) -A 1 # 或用 Salt 的内置比较更精准 salt -C Gos:Ubuntu or Gos:CentOS pkg.version_cmp openssl 1.1.1w第 3 分钟编写修复状态# /srv/salt/security/cve-2023-1234.sls # --- 步骤1更新包索引Ubuntu/CentOS或 pkgdbFreeBSD--- update_packages: pkg.refresh_db: - refresh: True {% if grains[os] FreeBSD %} - db: True {% endif %} # --- 步骤2升级 openssl 包 --- upgrade_openssl: pkg.installed: - name: - {{ grains[os] FreeBSD and security/openssl or grains[os] in [Ubuntu, Debian] and openssl or openssl }} - version: - {{ grains[os] FreeBSD and 3.0.12 or grains[os] in [Ubuntu, Debian] and 3.0.12-0ubuntu1~22.04.1 or 3.0.12-1.el7 }} - require: - pkg: update_packages # --- 步骤3重启所有依赖 OpenSSL 的服务 --- restart_dependent_services: cmd.run: - name: - {{ grains[os] FreeBSD and service nginx restart service postgresql restart or grains[os] in [Ubuntu, Debian] and systemctl restart nginx postgresql or systemctl restart nginx postgresql }} - require: - pkg: upgrade_openssl第 5 分钟靶向执行# 只对刚才查询出的脆弱主机执行 salt -C Gos:Ubuntu and Gkernelrelease:5.4.* state.apply security.cve-2023-1234 # 或用更细粒度的 Targeting基于 Pillar salt -I security:cve_2023_1234_vulnerable:true state.apply security.cve-2023-1234