
上周帮朋友看一台跑了好几年的业务机现象挺怪同一个 rpm 包在 A 机器上装完一切正常在 B 机器上装完服务能起来但某个模块一被调用就崩。两边rpm -q的输出一字不差包名、版本、架构全对得上。最后是rpm -V把问题揪出来的——B 机器上那个 so 文件的大小和时间戳都跟 rpm 数据库里的记录对不上说明它被人动过。这就是典型的包装对了文件不对也正好是 RPM 校验这件事里最容易被忽略的那一层。很多人对 RPM 校验的理解停留在rpm -ivh没报错就等于没问题实际上这套机制一共有三层包文件自身的摘要校验内容有没有在传输中损坏、包来源的签名校验这个包是不是厂商签的、有没有被换过、以及装完之后文件与数据库的一致性校验系统里的文件有没有被事后改动。三层用的命令不同、信任模型不同、踩的坑也完全不同。这篇就把这三层从头到尾拆开讲包括rpm -K的输出怎么读、公钥导入到底动了什么、rpm -V那九位字符每一位什么意思、/etc下的假阳性怎么过滤、以及怎么把校验做成能定期跑的巡检脚本。不管你是刚接触 rpm 的新手还是已经能背出参数的老运维应该都能捞到点东西。1. 从包没坏但机器不对说起RPM 校验的三个层次1.1 摘要对不上和签名对不上是两类完全不同的故障先把概念理清楚不然后面看输出一定会晕。**摘要digest**是一段内容的指纹是对文件字节做哈希运算得到的一串定长值**签名signature**则是对摘要严格说是对包头做非对称加密得到的密文只有持有对应公钥的人才能验证它确实出自某个私钥持有者。这个区别带来的后果是摘要能证明内容没被改动过但它证明不了这个包是谁做的。任何人改完包重新算一遍摘要摘要依然是对得上的。签名不一样私钥不在你手上你改了包就没办法重新签出合法的签名。所以在信任链上签名是根摘要是枝叶——rpm 的签名实际上覆盖了包头而包头里记录了负载payload的摘要一层套一层。我见过不止一次这样的场景同事从某个第三方站点下了一个 rpmsha256sum算出来的值和页面标的值一模一样就放心装了。问题是页面上那个值本身可能就是这个站点自己算的页面被改摘要值也跟着改这种校验只防传输损坏不防人为投毒。要判断来源只有签名这一条路。1.2 一只手上数得过来的几种校验算法各用在哪校验和这件事在计算机里用得极广从串口协议帧到压缩包目录到处都有它的影子。搞 RPM 校验之前最好把几类算法的定位分清不然很容易问出为什么这里不用 CRC32这种问题。算法输出长度典型用途能否对抗人为篡改CRC3232 位传输误码检测、压缩包目录、串口/工业总线帧尾校验、文件系统元数据不能线性运算改一个字节可以算出对应修正量MD5128 位老版本 rpm 的文件摘要、部分下载站公布的校验值已经不建议用于对抗性场景SHA1160 位老版本的 rpm 包头摘要逐步淘汰中SHA256256 位现代 rpm 的包头与负载摘要、仓库元数据可以差别在哪CRC32 的设计目标是发现随机误码它对偶发位翻转极其敏感但对刻意构造的修改几乎没有抵抗力攻击者完全可以在保持 CRC 不变的前提下改内容。MD5 和 SHA1 都属于密码学哈希理论上应该有抗碰撞和抗原像的能力只是随着研究推进MD5 和 SHA1 的抗碰撞性都已经被打破了。SHA256 目前还是可靠的。在 RPM 生态里老包用的是 MD5 文件摘要这也是rpm -V输出里那个5字符的来源现代发行版普遍换到了 SHA256但那个位置还是习惯性叫5这个历史包袱后面会再提一次。顺手说一句工具层面的东西因为经常有人在群里问sha 校验命令是什么。Linux 上最顺手的是 coreutils 提供的这几个sha256sum package.rpm # 算 SHA256 md5sum package.rpm # 算 MD5 cksum package.rpm # 默认就是 POSIX CRC32 cksum -a sha256 package.rpm # coreutils 8.32 之后支持 -a 指定算法如果手上只有 Windows 或者临时想算个 CRC32 对照一下用 Python 一行就够import zlib data open(package.rpm, rb).read() print(fcrc32 {zlib.crc32(data):08x})不过要提醒一句CRC32 用来做文件完整性对照是很不靠谱的选择它的碰撞门槛低到令人发指随便塞几个字节就能把 CRC32 拉回原值。文件完整性这件事老老实实用 SHA256。1.3 一个 rpm 文件里到底有几个地方会被校验打开一个 rpm 文件它的物理结构大致是引导段lead 签名头signature header 包头header 负载cpio 压缩包。校验覆盖的位置主要有四个签名头里记录的包头摘要签名的对象用来确认包头没被改。包头里记录的负载摘要和负载大小体现在rpm -K输出里的Payload SHA256 digest。包头里记录的文件列表和每个文件的摘要这是rpm -V的依据安装时会写进本地的 rpm 数据库。整个包文件本身的摘要仓库元数据里会记录dnf 下载完之后会算一遍做比对。这四层是递进的签名验过 → 包头可信 → 包头里的负载摘要可信 → 负载内容可信 → 解包出来的每个文件可信。理解了这条链就能理解为啥rpm -K通过了基本就可以放心——它验证的是整条链的根。2. 装机之前rpm -K 与公钥导入这一条完整链路2.1 简洁输出和详细输出分别看什么rpm -K等价于--checksig是装机前的第一道闸门。默认输出很简洁rpm -K vsftpd-3.0.5-1.el8.x86_64.rpm # vsftpd-3.0.5-1.el8.x86_64.rpm: digests signatures OK结尾的OK就代表摘要和签名都通过了。一旦有任何一个环节不对这里会变成NOT OK或者NOKEY后面会分开讲怎么处理。想看细节就加-v输出会展开成逐项判定这个才是有诊断价值的部分rpm -K -v vsftpd-3.0.5-1.el8.x86_64.rpm # vsftpd-3.0.5-1.el8.x86_64.rpm: # Header V3 RSA/SHA256 Signature, key ID 8483c65d: OK # Header SHA256 digest: OK # Header SHA1 digest: OK # Payload SHA256 digest: OK # MD5 digest: OK逐行看第一行是签名说明这个包头是用哪个 key ID 对应的私钥签的、验签结果如何第二三行是包头自身的摘要现代包一般会有 SHA256 和 SHA1 两行老包可能只有一种第四行是负载摘要这一项过了意味着压缩包内容没被动过最后一行MD5 digest是包裹在整个包上的旧式摘要主要是为了兼容老客户端新版本 rpm 也可能不输出这行。我一般只看两处签名那行的 key ID 是不是我认得的发行版签名密钥以及最终的判定结果。中间那几行摘要只要不是BAD基本不用操心。2.2 rpmkeys --import 与 rpm --import公钥到底进了哪里验签失败最常见的原因不是包有问题而是本地没导入对应的公钥。发行版会把自己的 GPG 公钥放在 ISO 或者官网同时也会随基础系统预置在/etc/pki/rpm-gpg/目录里。导入动作是这样的# 老写法现在依然可用 rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-发行版名 # 新写法语义更清晰等价 rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-发行版名导入之后公钥去了哪里很多人以为是写进了/etc/pki/rpm-gpg某个文件其实不是。rpm 把公钥当成一个特殊的已安装包写进了自己的数据库包名形如gpg-pubkey-keyid-时间戳。你可以这样查rpm -qa gpg-pubkey* # gpg-pubkey-8483c65d-5ccc5b19 rpm -qi gpg-pubkey-8483c65d-5ccc5b19 # Name : gpg-pubkey # Version : 8483c65d # Release : 5ccc5b19 # ... # Description : # -----BEGIN PGP PUBLIC KEY BLOCK----- # ...这一点非常关键因为它意味着两件事第一公钥可以用rpm -e删掉rpm -e gpg-pubkey-8483c65d-5ccc5b19删掉之后依赖这个 key 的包就再也验不过了第二公钥的增删会影响 rpm 数据库所以在做系统基线快照的时候gpg-pubkey*这些条目要单独放行否则巡检脚本会一直报一堆莫名其妙的差异。注意导入公钥之前一定要核对指纹。从非官方渠道拿到的公钥导进去等于把一个不认识的私钥持有者加进了信任名单所有用它签的包都会被判定为合法。指纹核对建议走官方发布的渠道用gpg --show-keys或者gpg --with-fingerprint看一眼完整指纹再导。2.3 NOKEY、NOT OK、BAD 三条处理路径rpm -K的失败结果其实分成好几类处理方式完全不同混在一起处理只会浪费时间。输出关键字实际含义该怎么处理NOKEY签名算法认出来了key ID 也读出来了但本地没这个公钥核对 key ID 与官方公布的一致后导入对应公钥再验NOT OK/BAD摘要或签名对不上包内容确实被改过或下载损坏不要装重新下载并核对来源(none)/ 无签名段这个包压根没签名只能靠下载来源和公布的摘要值判断风险自担Header V4 RSA/SHA256 Signature, key ID xxx: OK但digests BAD签名没问题但负载摘要失败包在签完之后被改动过或传输中损坏重下NOKEY是最容易被误判成这是坏包的一类。我以前就干过这事从内网自建仓库下包rpm -K报 NOKEY第一反应是仓库同步出了问题折腾半小时才发现只是新换的签名密钥没导进本地。另外补一个实操细节rpm -i安装时对这两类失败的处理强度是不一样的。摘要对不上通常直接报错中断安装而签名对不上尤其是 NOKEY在某些版本和配置下只给一行 warning 就继续装了。所以不要指望rpm -i帮你把关养成习惯装之前先手动跑一遍rpm -K。2.4 校验等级宏与 --nodigest/--nosignature 的应急边界rpm 4.14 之后引入了一个%_pkgverify_level宏用来控制默认做几层校验取值一般是all、digest、signature、none。它决定了 rpm 在读写包文件时的严格程度。我个人的建议是保持默认通常是all或digest除非有明确理由否则不要动它。配套的还有两个开关rpm -i --nodigest --nosignature some.rpm这两个参数的作用是跳过摘要和签名检查。注意这里是包文件层面的跳过不是rpm -V里检查已安装文件内容的那个开关那个叫--nofiledigest后面会讲。这两个东西经常被搞混我在排查现场见过有人为了关掉文件内容比对敲了--nodigest结果什么都没变还以为是 rpm 有 bug。--nodigest --nosignature的正经用途只有一个应急恢复。比如系统崩了要从救援介质里装一个包而这个救援介质的签名链跟当前系统对不上这时候用它先把环境拉起来。用完一定要记得后面重新走一遍正规校验因为跳过之后这个包在后续的rpm -V里是没有摘要记录可比的。3. 装机之后rpm -V 那九个字符逐位拆解3.1 SM5DLUGTP 每一位在说什么rpm -V 包名是装机之后最常用的校验手段它拿本地文件的实际状态跟 rpm 数据库里安装时写下的记录做逐项比对。输出格式长这样rpm -V openssh-server # S.5....T. c /etc/ssh/sshd_config # .M....... /usr/libexec/openssh/sftp-server # missing /usr/share/man/man8/sshd.8.gz第一列是九个字符不足补点每一位对应一种属性差异位置字符比对的属性常见触发原因1S文件大小被替换、被追加内容、被截断2M权限位或文件类型chmod、文件被换成软链接或目录35文件内容摘要内容被编辑或替换4D主次设备号设备节点被重建5L软链接指向ln -sf改了目标6U属主chown7G属组chgrp8T修改时间被编辑、被 touch、日志轮转9Pcapabilitiessetcap / getcap 变更第二位是空的还是显示成M之外的值取决于 rpm 版本早期版本这一列只报权限和文件类型差异。第九位的P是后来加的用来覆盖 capabilities 这条现代 Linux 上越来越重要的属性。重点说说第三位5。这个名字来自 MD5因为老版本 rpm 用的就是 MD5 做文件摘要。新版本换成了 SHA256但字符位置没变还是那个5。所以看到5不要理解成MD5 校验失败它现在代表的是文件内容摘要与安装时记录不符算法是什么取决于构建这个包时用的默认值。判断优先级上我的经验是5和S同时出现基本可以确定文件被换过只有T出现多半是正常的配置修改或日志写入只有M或U/G出现通常是有人手工调过权限。3.2 把 rpm -Va 的输出压成真正要看的一小撮单包校验好办麻烦的是全量扫描rpm -Va在一台装了几年、跑了几十上百个包的机器上这条命令输出几千行是常态直接看等于没看。需要做的是把它压下来。一个常见的错误做法是用 grep 粗暴过滤rpm -Va | grep -v c / # 不推荐为什么不行因为rpm -V的输出字段是九位标志 属性串 路径中间用空格分隔。当属性串为空的时候格式是标志 空格 路径当属性串是c的时候格式是标志 空格 c 空格 路径。这个grep -v c /不但会把合法的配置变更过滤掉还会误伤路径里恰好包含c的行——虽然概率不高但线上做巡检脚本误报和漏报一样致命。正确做法是按字段解析。rpm 输出的字段分隔用\s就够稳写个正则import re, subprocess VERIFY_LINE re.compile(r^(?Pflags[SMDLUGTP5.]{9})\s(?Pattr\S*)\s(?Ppath/\S)$) def collect(): out subprocess.run([rpm, -Va], capture_outputTrue, textTrue, checkFalse) rows [] for line in out.stdout.splitlines(): m VERIFY_LINE.match(line) if m: rows.append(m.groupdict()) elif line.startswith(missing): rows.append({flags: MISSING, attr: , path: line.split()[-1]}) return rows拿到结构化的数据之后就可以按标志组合分级了高危flags里同时含S和5说明大小和内容都变了文件被整体替换的概率极高。中危只含5内容变了但大小没变可能是原地编辑或二进制补丁。低危只含T时间戳变化内容没动过。权限类只含U、G、M、P属性层面的调整。缺失missing行包里的文件不在了可能是被人删了也可能是发行版升级后遗留。按这个分级跑一遍一台常规业务机的待确认条目通常能从几千行压到几十行人工审起来才现实。3.3 为什么 /etc 下的文件永远在报 T 和 5刚上手的人几乎都会问同一个问题我什么都没动为什么rpm -V报了一堆/etc下的文件答案有三层。第一层配置文件本来就会变。装完之后改个端口、调个日志级别、调个连接数上限都是正常的运维动作rpm -V当然会报。这不是故障这是设计如此——rpm 记录了安装时的原始状态任何改动都会被如实报告出来。第二层有些包在安装脚本里自己就在改文件。比如某个服务的%post脚本会往/etc下追加内容、生成机器相关的标识符装完那一刻就已经和包里的原始文件不一样了。第三层日志和运行时会话文件也会被标进去。有些包的日志文件、pid 文件是包的一部分用%ghost声明的占位文件运行起来就会写内容T必然飘。处理方式有几种按我的偏好排序解析输出后按attr c过滤掉配置项这是最稳的rpm 本身在较新版本里提供了--noconfig开关可以直接让-V忽略配置文件用之前先在你的发行版上测一下是否支持再就是维护一份显式的白名单把已知会变动的路径列进去。3.4 missing、not installed 和文件不属于任何包rpm -V输出里有一条容易和别的概念混起来的missing。它的含义是这个包在其文件列表里声明了这个文件但文件系统上找不到。和它容易混的是两种完全不同的情况package xxx is not installed这个包根本没装rpm -V自然没法比对。file /xxx is not owned by any package用rpm -qf /path查某个文件属于哪个包时回答是不属于任何包。后两种情况要分开对待。missing才是需要关注的——文件被删了。但也不是所有missing都有问题比如包升级之后旧版本留下的某些文件被清理了或者管理员手工删了某个确认无用的文件都会留下missing记录。判断方法是结合文件路径的重要性/usr/bin、/usr/lib64下的missing要重点看/usr/share/doc、/usr/share/man下的基本可以忽略。顺带提一句反向定位的命令组合排查时用得极多rpm -qf /usr/sbin/sshd # 这个文件属于哪个包 rpm -ql openssh-server # 这个包装了哪些文件 rpm -Vf /usr/sbin/sshd # 只校验这一个文件所在的包 rpm -qc openssh-server # 只看这个包的配置文件rpm -Vf在只怀疑个别文件的时候很省事不用扫全量。4. 从仓库到本地一条完整的下载校验链4.1 repomd.xml 是整条链的信任根用 dnf/yum 从仓库装包很多人以为rpm 帮我验过签名了就行其实在到达 rpm 之前软件仓库这一层已经做了好几道校验。把这条链走一遍很有价值因为出问题的时候你就知道该在哪一环查。链路的起点是仓库根目录下的repomd.xml。这个文件记录了仓库里各个元数据文件的位置、大小和摘要比如primary.xml.gz的 SHA256 和字节数。它的地位相当于元数据的元数据是整条链的信任根。因此正规的仓库会再给repomd.xml配一个 GPG 签名通常叫repomd.xml.asc。拉包时的顺序是这样的下载repomd.xml如果配置了repo_gpgcheck1同时下载repomd.xml.asc并用本地公钥验签。从repomd.xml里读出primary.xml.gz的 SHA256 和 size下载后先算摘要、再比大小两项都对才继续。从解压出来的primary.xml里找到目标包读出它的 checksum 和 checksum type通常是 SHA256。下载 rpm 文件本地算一遍摘要与元数据里的值比对不一致直接丢弃重下。交给 rpm 去做签名和摘要校验也就是rpm -K那一套。四道关任何一道不过都会中断。这也是为什么从仓库里装的包比从某个论坛链接下的包要放心得多——不是包本身有多大差别是校验链的完整度差了一整个数量级。4.2 gpgcheck、repo_gpgcheck、localpkg_gpgcheck 分别管什么这三个开关名字很像管的事情完全不一样配置错了就会出现看着配了但没生效的情况。配置项位置实际作用gpgcheck仓库文件/etc/yum.repos.d/*.repo或/etc/dnf/dnf.conf安装从仓库下载的包时校验包签名repo_gpgcheck仓库文件校验仓库元数据repomd.xml的签名不是包签名localpkg_gpgcheck/etc/dnf/dnf.conf用dnf install ./xxx.rpm装本地 rpm 文件时是否校验签名典型的仓库文件配置长这样[internal-base] nameInternal Base baseurlhttps://repo.internal.example.com/baseos/$basearch/ enabled1 gpgcheck1 repo_gpgcheck1 gpgkeyhttps://repo.internal.example.com/keys/RPM-GPG-KEY-internal metadata_expire3600这里最容易踩的坑是给baseurl换成https之后以为传输加密了就万事大吉把gpgcheck关掉了。加密只保护传输过程不保护源头的可信度——如果仓库服务器本身被攻陷https 一点用都没有。签名校验才是这条链上唯一能对抗源端投毒的手段。另一个坑是repo_gpgcheck1打开后仓库突然全部报错。最常见原因是本地没有仓库元数据签名用的那把公钥而gpgkey里通常只写了包签名的那把。遇到这种情况先把repo_gpgcheck临时关掉看是否能恢复确认是公钥问题之后再去补公钥。别一上来就怀疑网络。4.3 手动核对和官网公布的摘要比对有些场景不走仓库比如从官网直接下资源包、从内网文件服务器拷包。这时候唯一的凭据就是发布方公布的摘要值。流程很简单但有几个细节值得说。sha256sum vsftpd-3.0.5-el8.x86_64.rpm拿到输出之后和发布页面上公布的值逐字符比对。这里有两个常见失误一是把十六进制的大小写搞错比对的时候先统一转换成小写二是下载时不小心多下了个.asc或者.sig文件比例的对象搞错了白忙一场。# 统一成小写再比避免大小写造成的误判 sha256sum vsftpd-3.0.5-el8.x86_64.rpm | awk {print tolower($1)}比较正式的发布方还会同时提供 SHA 和 MD5 两套值这主要是历史习惯验证时优先用 SHA256 那套。MD5 那套留着当交叉参考就行别单独依赖它做安全判断。4.4 rpm2cpio 拆包复核当 -K 都通过但你还是不放心确实会遇到这种情况rpm -K全部 OK但你需要确认包里到底装了什么或者要跟另一个版本的文件做逐字节比对。这时候用rpm2cpio把它拆开看。# 只列内容不解压 rpm2cpio package.rpm | cpio -t # 解到当前目录下的临时文件夹 mkdir -p /tmp/rpmx cd /tmp/rpmx rpm2cpio /path/to/package.rpm | cpio -idmv # 只看安装脚本不落盘 rpm -qp --scripts package.rpm # 只看包头信息 rpm -qpi package.rpm rpm -qp --qf %{NAME} %{VERSION} %{RELEASE} %{PAYLOADDIGEST} %{PAYLOADDIGESTALGO}\n package.rpmrpm -qp --qf这条值得多看一眼它能直接把包里的负载摘要打出来用来和另一个来源的同名包做比对非常方便。不过要注意%{PAYLOADDIGEST}这类标签在较新的 rpm 上才有老的发行版上可能输出空值用之前先试一下。拆包之后能做的事情就多了拿某个二进制和线上的版本做cmp看某个配置文件里到底写了什么默认值检查%post脚本里是不是做了什么意料之外的事。这一步在排查第三方包、做合规审查的时候几乎是必做动作。5. 把校验做成例行动作脚本、白名单和 rpmdb 修复5.1 rpm -V 没有一切正常这种明确信号得自己判断rpm -Va在一台完全正常的机器上也可能输出很多东西因为配置文件本来就被改过。它不会像很多工具那样给你一句all good也没有一个稳定的退出码可以直接拿来判断。关于退出码不同发行版、不同 rpm 版本在遇到差异时返回什么都不完全一致有的版本返回 0有的返回非 0。所以我在脚本里从来不依赖rpm -Va的返回码一律解析标准输出。这个习惯来自一次教训早期的巡检脚本用if [ $? -eq 0 ]判断结果连续几个月一个告警都没出后来手工跑了一次才发现输出里全是差异只是退出码恰好是 0。脚本结构大致分三步采集、过滤、分级输出。#!/usr/bin/env python3 定期巡检解析 rpm -Va 输出按白名单过滤并分级 import re import subprocess import json from pathlib import Path WHITELIST_FILE Path(/etc/rpmverify/whitelist.json) VERIFY_LINE re.compile(r^(?Pflags[SMDLUGTP5.]{9})\s(?Pattr\S*)\s(?Ppath/\S)$) def load_whitelist(): if not WHITELIST_FILE.exists(): return {prefix: [], exact: [], pkg_attr_config: True} return json.loads(WHITELIST_FILE.read_text()) def scan(): proc subprocess.run([rpm, -Va], capture_outputTrue, textTrue) result [] for line in proc.stdout.splitlines(): if line.startswith(missing): result.append({flags: MISSING, attr: , path: line.split()[-1]}) continue m VERIFY_LINE.match(line) if m: result.append(m.groupdict()) return result def severity(flags): if flags MISSING: return high if S in flags and 5 in flags: return high if 5 in flags: return medium if set(flags) set(.T): return low return low def main(): wl load_whitelist() rows [] for item in scan(): path item[path] if any(path.startswith(p) for p in wl.get(prefix, [])): continue if path in set(wl.get(exact, [])): continue if wl.get(pkg_attr_config) and item[attr] c: continue item[severity] severity(item[flags]) rows.append(item) rows.sort(keylambda r: {high: 0, medium: 1, low: 2}[r[severity]]) for r in rows: print(f{r[severity]:6} {r[flags]:9} {r[path]}) print(f\n合计 {len(rows)} 条待确认) if __name__ __main__: main()白名单文件长这样{ prefix: [/var/log/, /var/lib/, /var/spool/, /run/], exact: [/etc/machine-id, /etc/resolv.conf], pkg_attr_config: true }这个脚本我基本没怎么改过跑在不同发行版上都能用因为只依赖rpm -Va的输出格式。5.2 白名单怎么维护才不会变成摆设白名单是这类巡检脚本的生命线维护不好就两个下场要么太松重要告警被吞掉要么太严天天报一堆没用的最后没人看。我的原则是只加确定会动的路径不加看着像是会动的路径。具体分三类确定会变的目录/var/log、/var/lib、/var/spool、/run、/tmp。这些地方本来就不该指望文件内容和安装时一致。配置文件默认通过attr c这个条件统一放行不单独加路径。因为配置文件太多太杂逐个加根本加不完。机器相关的标识/etc/machine-id、/etc/resolv.conf、/etc/hosts这类按精确路径单列。有个反直觉的点值得强调/etc下的白名单要慎用。配置文件的变更虽然正常但很多攻击手法恰恰就是在/etc下动手脚比如往ld.so.preload里塞东西、改crontab、改sudoers。如果图省事把整个/etc加进白名单等于把最需要盯的地方给放行了。我的做法是配置文件只放行T和5这类内容差异的告警一旦出现U、G、M这种权限属性变化即使路径在/etc下也要报出来——因为正常的运维改配置不会同时把属主和权限也改了。5.3 rpmdb 出问题时先分级再决定要不要 rebuilddb还有一类校验失败跟文件没关系是本地的 rpm 数据库自己出了问题。典型现象是rpm -qa # error: rpmdb: BDB0113 Thread/process ... failed: BDB1507 ... # error: cannot open Packages database in /var/lib/rpm或者更隐蔽一点查询某个包时莫名其妙地报段错误、返回不完整结果。这时候要先判断是数据库损坏还是别的问题。ls -l /var/lib/rpm/ # RHEL8 之后是 rpmdb.sqlite之前是 Packages、Name、Requires 等一堆文件 rpm -qa --qf %{NAME}\n | wc -l # 看能不能正常读如果确认是数据库损坏处理方式是重建索引# 先备份万一失败还能回退 cp -a /var/lib/rpm /var/lib/rpm.bak.$(date %s) # 新版本 rpmdb --rebuilddb # 老版本 rpm --rebuilddb # 重建完检查包数量是否合理 rpm -qa | wc -l两点提醒。第一重建索引不影响已安装的文件只重建数据库——这一点很多新手会误解以为要重装系统。第二重建之前一定要留备份。我见过一次因为磁盘满了导致重建过程中途失败数据库直接不可读的案例最后是靠 rpmdb 的备份目录恢复的折腾了快两个小时。跑rebuilddb之前先确认/var/lib所在分区有足够空间这一步花不到十秒能省掉很多麻烦。6. 给别人发包私有 rpm 的签名与内部校验闭环6.1 rpmsign --addsign 的准备工作如果你需要把内部构建的 rpm 分发给别的机器签名这件事必须做。不签名的包在装了gpgcheck1的机器上根本装不上硬关校验又等于把整条信任链拆了。给包签名的命令是rpmsign --addsign package.rpm但这个命令背后有一堆准备工作。它默认会去找 GPG 的私钥所以得先有一对能用的密钥# 生成密钥对按提示填信息注意密钥用途要支持签名 gpg --full-generate-key # 查看本地私钥列表拿到 key ID gpg --list-secret-keys --keyid-format LONG然后要让 rpm 知道用哪把密钥、以及怎么调用 gpg。传统做法是配置~/.rpmmacros%_signature gpg %_gpg_name 你的 key ID %_gpg_path ~/.gnupg%_gpg_name这一项填错的话rpmsign会报找不到密钥或者更糟——静默地签成了另一个身份。签完之后立刻用rpm -K -v验一遍确认签的是预期的那把密钥这个动作我每次都做rpmsign --addsign package.rpm rpm -K -v package.rpm # 检查输出的 key ID 是不是你准备的那把顺带说一个不太为人知但很有用的参数--delsign可以移除已有签名。在重新签名之前用它清一下避免签名层层叠加。6.2 自建仓库时元数据也要签只给单个 rpm 签名还不够。如果内部走的是自建仓库repomd.xml也得签否则客户端开了repo_gpgcheck就用不了。createrepo_c生成元数据之后用 gpg 对repomd.xml做分离签名detached signature生成repomd.xml.asccreaterepo_c /var/www/repo/baseos cd /var/www/repo/baseos/repodata gpg --detach-sign --armor repomd.xml # 生成 repomd.xml.asc这里有两个容易忽略的细节。第一每次createrepo_c重新生成元数据之后都要重签因为repomd.xml的内容变了旧签名自然失效。如果这一步忘了客户端会报元数据签名无效但错误信息不会直接告诉你你忘了重签得自己反应过来。我的做法是把这两条命令写进同一个发布脚本中间不给人手工介入的机会。第二repomd.xml.asc的位置和命名要严格对应。有些客户端找的是repomd.xml.asc有些找的是repomd.xml.key具体取决于版本和配置。部署完之后用客户端实测一遍最保险。6.3 内部校验的最小闭环把前面所有东西串起来一个内部发包的最小闭环大概是这样的。构建侧编译产出 rpm →rpmsign --addsign签名 →rpm -K -v自检 → 放进仓库 →createrepo_c生成元数据 → 对repomd.xml分离签名 → 发布。客户端侧导入内部公钥 → 仓库配置打开gpgcheck和repo_gpgcheck→ 装包 → 装完跑一次rpm -V存基线。再加上定期巡检每天凌晨跑一遍全量rpm -Va按第 5 节那套脚本过滤分级有高危条目就告警。这个闭环看起来步骤不少但每一环都有明确目的拆掉任何一环都会留下缺口。真要说哪一步最容易被省略大概是最后的定期巡检——因为签名和下载校验都是装的时候做的装完之后文件会不会被改只有巡检能发现。最后分享一个我自己一直在用的小习惯给关键业务机做资产基线时除了记录包清单顺手把rpm -Va的输出存一份到与业务机隔离的位置。日常巡检只对比今天和昨天的差异而这份离线基线可以在怀疑系统被改动过的时候用来比对现在和部署时的差异。因为rpm -V比的是本地数据库如果攻击者连数据库一起改了rpm -V是发现不了的。多一份离线的、装机时刻的原始记录成本几乎为零真到要用的时候价值很大。这个思路和 AIDE、tripwire 那类工具是同一个道理只不过用 rpm 自带的输出做基线部署成本更低也更容易解释清楚。