镜像签名与验证
镜像签名,说白了就是给你的容器贴上一枚防伪标签。别人拿到你的 SIF 文件,能立刻知道这镜像到底是不是你本人发布的,有没有被人动过手脚。我刚开始接触 Singularity 时觉得这步有点多余,直到有一次在集群上跑了一个从网上下载的镜像,跑出来的结果怎么都对不上……后来一查,镜像被中间人篡改了,里面偷偷塞了几个高危包。从那以后,我个人的习惯是:凡是生产环境用的镜像,必须先签名,再分发。
SIF 格式
要搞懂签名,得先了解 SIF 的结构。SIF 全称是 Singularity Image Format,它可不是一个简单的 squashfs 文件系统压缩包,而是一个自带元数据的分层容器格式。SIF 文件从头部到尾部大致长这样:
| 偏移 / 分区 | 内容说明 |
|---|---|
| Data Descriptors 区域 | 描述后续数据块的元信息,比如类型、偏移、哈希值 |
| 容器分区(镜像根文件系统) | 实际的 squashfs 文件系统,承载操作环境和软件 |
| 签名分区 | 可以存放一个或多个数字签名块,用于验证镜像完整性和来源 |
| 其他元数据分区 | JSON 定义、环境变量、标签等 |
SIF 的签名分区是独立于根文件系统的。这意味着你可以先构建好 squashfs,再单独对镜像进行签名,无需重新打包整个文件。这个设计很聪明——我在项目中经常需要给同一个基础镜像打上不同环境的签名(开发版、测试版、发布版),只需要追加签名分区就行,不用每次复制整个 squashfs。
签名原理
Singularity 的签名体系基于 非对称加密(公钥/私钥),具体使用的是 OpenPGP(GnuPG 的实现)。大致流程:
- 签名者用私钥对 SIF 文件的某个关键区域(通常是 Data Descriptors 和 squashfs 分区)计算哈希,然后加密这个哈希。
- 加密后的哈希被写入 SIF 文件尾部的签名分区,同时附带签名者的公钥指纹和签名创建时间。
- 验证者从镜像中提取签名分区,用对应的公钥解密哈希,再对文件实际内容重新计算一次哈希。
- 两个哈希完全一致,说明文件在签名之后没有被修改过。
为什么要用非对称?你想想看,如果是对称加密,你分发镜像时还得把它藏起来的密钥一并交给别人,那和没锁有什么区别。非对称加密下,私钥只保存在签名者手里,公钥可以公开挂到 keyserver 上或者嵌入镜像内,一切公开透明。
singularity overlay 挂载了可写层,那原来的签名就失效了——因为镜像本身其实已经变了。这一点在容器编排时要特别小心。
使用 sif 签名工具
实际签个名,需要 singularity sign 和 singularity verify 两个命令。它们依赖 GnuPG 生成的密钥对。Singularity 会把密钥存储在 ~/.singularity/sypgp/ 目录下,和系统的 GPG 密钥圈隔离开——这样你可以在同一台机器上管理容器专用的密钥,不影响个人 GPG 密钥。
1. 生成密钥对
singularity key newpair --key-type RSA --key-length 4096 --name "My Container Key" --email "me@example.com"
如果你不想在交互界面输密码,可以用 --password 参数指定(不过我个人不建议在脚本里硬编码密码,用环境变量或者密码管理器更安全)。执行完后,Singularity 会自动生成一对公私钥,并存到 ~/.singularity/sypgp/ 里。
2. 列出已有密钥
singularity key list
输出会显示公钥指纹、算法、创建时间。你需要记住公钥 ID,后面签名和验证时要用。
3. 签名镜像
singularity sign -k 0xABCDEF1234567890 mycontainer.sif
这里的 -k 后面跟的是公钥 ID 或者指纹。如果本地只有一个密钥,可以省略 -k,Singularity 会自动选择默认密钥。但我的建议是永远显式指定——你无法保证几个月后本地不会增加新的密钥,届时默认密钥变了,签名就会对不上。
4. 验证镜像
singularity verify mycontainer.sif
验证时你需要有签名者的公钥。Singularity 默认会先查询你的本地密钥圈,如果没找到,它会尝试从 keys.openpgp.org 拉取。如果你想强制使用本地密钥,可以用:
singularity verify --local mycontainer.sif
如果镜像有多个签名,singularity verify 默认会检查所有签名是否都有效。你只需要一个签名有效就能通过验证?不,默认是 所有签名必须通过。这在多人协作时是个重要的安全策略:只要有一个人的签名无效,整个镜像就算不可信。
5. 导出公钥(供他人导入)
singularity key export --secret 0xABCDEF1234567890 > mykey.asc
注意 --secret 会导出私钥,一般只在本机备份使用;给别人用应该导出公钥:
singularity key export --public 0xABCDEF1234567890 > mykey.pub.asc
对方拿到公钥文件后,执行 singularity key import mykey.pub.asc 导入本地即可验证你的签名。
--secret 的私钥文件发给了同事,他误以为是公钥并导入。虽然功能上也能验证签名(因为私钥包含了公钥信息),但私钥暴露出去了。从那以后我做了一个脚本,在导出时自动检查文件内容是否包含 -----BEGIN PGP PRIVATE KEY BLOCK-----,如果是就直接报错退出,防止手滑。
签名策略建议
- 镜像发布前必须签名:哪怕是内部测试环境,养成签名的习惯,一旦镜像外流还能追踪来源。
- 签名密钥分级管理:用独立的签名密钥做容器签名,不要和你的个人 GPG 邮箱混在一起。我个人的做法是每季度轮换一次容器签名密钥,旧的撤销并上传到 keyserver。
- 配合 CI/CD:在 GitLab CI 或 GitHub Actions 里对构建产物自动签名,私钥用项目变量保管,不要写入仓库。
说到底,签名就是花几秒敲一条命令,但能省去以后几个小时甚至几天的排查时间。集群管理员、审计人员、还有明年的你,都会感谢现在这个愿意签名的你。