Shell脚本自动化签名与验证完整指南
目录导读
- 容器镜像安全面临的挑战
- 为什么需要给容器镜像签名
- Shell脚本实现镜像签名的基础架构
- 实战:用Shell脚本签名容器镜像(含代码)
- 验证签名完整性的自动化Shell脚本
- 与CI/CD流水线集成的最佳实践
- 常见问答(FAQ)
容器镜像安全面临的挑战
在云原生时代,容器镜像的分发和部署已成为DevOps流程的核心环节,镜像供应链攻击、中间人篡改、镜像仓库投毒等安全威胁日益严峻,根据[容器安全报告],超过60%的企业曾遇到过镜像被篡改或使用未经验证镜像的案例,传统依赖HTTPS传输安全已不足以应对复杂的攻击场景——因为镜像在构建后、存储前、拉取运行时仍存在被篡改的风险。

关键痛点:
- 镜像构建后:恶意代码可能在推送前被注入
- 镜像存储时:仓库漏洞或内部威胁可修改镜像内容
- 镜像拉取中:中间人攻击可替换为恶意版本
- 多环境部署:不同环境(开发/测试/生产)需确保镜像一致性
解决方案:采用数字签名 + 完整性校验的组合策略,通过Shell脚本自动化签名流程,既能避免人工操作的错误与延迟,又能嵌入到现有的CI/CD工具链中,本文将从零构建一套可落地的镜像签名脚本体系。
为什么需要给容器镜像签名
镜像签名不是可选项,而是安全基线,其核心价值在于:
| 安全维度 | 签名能解决的问题 |
|---|---|
| 来源验证 | 确认镜像来自受信任的发布者 |
| 完整性校验 | 未在传输过程中被篡改 |
| 抗抵赖性 | 发布者无法否认其签发行径 |
| 审计追踪 | 每次签名记录可对应特定构建或发布版本 |
业界标准:Docker Content Trust(基于Notary)和Sigstore的Cosign工具是目前主流方案,但许多企业受限于网络环境、私有化部署或定制需求,需要基于Shell脚本封装自定义签名流程,实现与内部CA(证书颁发机构)或硬件安全模块(HSM)的集成。
Shell脚本实现镜像签名的基础架构
1 签名流程概览
[构建镜像] → [生成摘要] → [用私钥签名摘要] → [将签名附加到镜像元数据] → [推送至仓库]
2 工具选择
| 工具 | 用途 | 理由 |
|---|---|---|
openssl |
密钥生成、签名、验证 | 系统原生,支持RSA/ECDSA |
docker 或 skopeo |
镜像操作与推送 | Skopeo更适合无守护进程签名 |
base64 / sha256sum |
编码与摘要计算 | POSIX标准,无需额外安装 |
jq |
JSON解析(可选) | 处理镜像清单文件 |
3 密钥管理要点
- 私钥保护:使用硬件安全模块(如YubiKey)或密钥管理服务(AWS KMS),次优方案是加密存储于受限目录(权限600)
- 公钥分发:通过内部PKI系统或环境变量注入验证脚本
- 签名有效期:建议结合证书链设置到期时间
实战:用Shell脚本签名容器镜像(含代码)
以下脚本实现:对本地Docker镜像进行摘要计算 → 使用RSA私钥签名 → 将签名写入镜像标签注解中。
脚本:sign_image.sh
#!/bin/bash
# 容器镜像签名脚本 v1.0
# 依赖:openssl, skopeo, docker
set -euo pipefail
# ---- 配置区域 ----
SIGN_PRIVATE_KEY="/secure/keys/cosign.key" # 私钥路径
SIGN_PUBLIC_KEY="/secure/keys/cosign.pub" # 公钥路径
SIGN_ALGORITHM="sha256" # 摘要算法
SIGN_TAG="signature" # 签名标签后缀
# ---- 函数定义 ----
usage() {
echo "用法: $0 <镜像名:标签> [额外注解]"
echo "示例: $0 my-registry.example.com/app:v1.0"
exit 1
}
generate_digest() {
local image_ref="$1"
# 使用skopeo获取镜像清单的sha256摘要(不拉取完整镜像)
skopeo inspect --raw "docker://${image_ref}" | sha256sum | awk '{print $1}'
}
sign_digest() {
local digest="$1"
echo -n "$digest" | openssl dgst -${SIGN_ALGORITHM} -sign "$SIGN_PRIVATE_KEY" -out /tmp/signature.bin
base64 -w0 /tmp/signature.bin
rm -f /tmp/signature.bin
}
annotate_image() {
local image_ref="$1"
local signature_b64="$2"
local annotation_key="signing.dev/${SIGN_TAG}"
# 通过skopeo添加注解(不修改原始镜像层)
skopeo copy --override-os linux --override-arch amd64 \
--add-annotation "${annotation_key}=${signature_b64}" \
"docker://${image_ref}" "docker://${image_ref}.signed"
# 可选:移除.signed标签后重命名回原始标签
skopeo copy "docker://${image_ref}.signed" "docker://${image_ref}"
skopeo delete "docker://${image_ref}.signed" || true
}
# ---- 主流程 ----
if [ $# -lt 1 ]; then
usage
fi
IMAGE_REF="$1"
echo "[INFO] 开始签名镜像: ${IMAGE_REF}"
# 1. 检查密钥存在
if [ ! -f "$SIGN_PRIVATE_KEY" ]; then
echo "[ERROR] 私钥不存在: ${SIGN_PRIVATE_KEY}"
exit 1
fi
# 2. 计算摘要
IMAGE_DIGEST=$(generate_digest "$IMAGE_REF")
echo "[INFO] 镜像摘要: ${IMAGE_DIGEST}"
# 3. 签名摘要
SIGNATURE=$(sign_digest "$IMAGE_DIGEST")
echo "[INFO] 签名(base64): ${SIGNATURE:0:32}...(截断显示)"
# 4. 将签名写入镜像元数据
annotate_image "$IMAGE_REF" "$SIGNATURE"
echo "[SUCCESS] 签名完成并推送到仓库"
执行示例:
chmod +x sign_image.sh ./sign_image.sh registry.internal:5000/nginx:1.25
脚本关键设计说明
- 使用
skopeo inspect --raw:避免拉取整个镜像,仅获取清单摘要,适合在构建流水线中快速执行 - 签名格式:将Base64编码的签名存入镜像注解(Annotation),不改变镜像层内容,兼容所有OCI标准仓库
.signed临时标签:通过原子操作避免推送中途失败导致的脏数据
验证签名完整性的自动化Shell脚本
拉取镜像后,验证脚本需:提取注解 → 计算摘要 → 用公钥验证签名。
脚本:verify_image.sh
#!/bin/bash
# 容器镜像签名验证脚本 v1.0
set -euo pipefail
PUBLIC_KEY="/secure/keys/cosign.pub"
# ---- 函数定义 ----
extract_signature() {
local image_ref="$1"
# 获取镜像注解中的签名值
skopeo inspect --raw "docker://${image_ref}" |
jq -r '.annotations["signing.dev/signature"] // empty'
}
compute_digest() {
local image_ref="$1"
skopeo inspect --raw "docker://${image_ref}" | sha256sum | awk '{print $1}'
}
verify_signature() {
local digest="$1"
local signature_b64="$2"
# 解码签名并验证
echo "$signature_b64" | base64 -d > /tmp/sig_verify.bin
echo -n "$digest" | openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature /tmp/sig_verify.bin
local verify_result=$?
rm -f /tmp/sig_verify.bin
return $verify_result
}
# ---- 主流程 ----
if [ $# -lt 1 ]; then
echo "用法: $0 <镜像名:标签>"
exit 1
fi
IMAGE_REF="$1"
echo "[INFO] 验证镜像签名: ${IMAGE_REF}"
# 1. 提取签名
SIGNATURE=$(extract_signature "$IMAGE_REF")
if [ -z "$SIGNATURE" ]; then
echo "[FAILED] 镜像未包含签名注解"
exit 2
fi
# 2. 计算摘要
DIGEST=$(compute_digest "$IMAGE_REF")
# 3. 验证签名
if verify_signature "$DIGEST" "$SIGNATURE"; then
echo "[SUCCESS] 镜像签名验证通过!"
echo " ${DIGEST:0:16}..."
exit 0
else
echo "[FAILED] 签名验证失败: 镜像可能被篡改或公钥不匹配"
exit 3
fi
验证流程:
# 在部署节点执行 ./verify_image.sh registry.internal:5000/nginx:1.25
与CI/CD流水线集成的最佳实践
1 GitLab CI / Jenkins Pipeline 集成
阶段嵌入点:
- 构建阶段后:执行
sign_image.sh签名 - 推送仓库前:将公钥作为受保护变量注入
- 部署前置:执行
verify_image.sh,失败则回滚
示例GitLab CI片段:
stages:
- build
- sign
- deploy
variables:
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
sign_image:
stage: sign
script:
- skopeo copy "docker://${IMAGE_TAG}" "docker://${IMAGE_TAG}_unsigned"
- sign_image.sh "${IMAGE_TAG}"
only:
- main
verify_and_deploy:
stage: deploy
script:
- verify_image.sh "${IMAGE_TAG}" || exit 1
- kubectl set image deployment/nginx nginx="${IMAGE_TAG}"
environment: production
2 与Kubernetes的准入控制器结合
通过kyverno或OPA Gatekeeper策略,在Pod创建时调用验证脚本:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-signature
spec:
validationFailureAction: enforce
rules:
- name: verify-signature
match:
any:
- resources:
kinds: ["Pod"]
validate:
exec:
image: "registry.example.com/verify-image:v1" # 封装verify脚本的容器
command: ["/verify_image.sh", "{{ request.object.spec.containers[0].image }}"]
常见问答(FAQ)
Q1:Shell脚本签名与Cosign/tuf-notary相比有何优劣?
A:Cosign提供更完善的密钥管理(如KMS集成)、OIDC身份认证和公钥透明日志(Rekor),适合开放生态,Shell脚本的优势在于:零依赖(仅需openssl)、可控性强(可对接内部CA和HSM)、适合离线或隔离网络环境(如军工、金融内部),建议:公有云场景用Cosign,私有强合规场景用Shell脚本定制。
Q2:签名后的镜像会变大吗?
A:不会,签名仅以注解形式附加(约1-2KB),不修改镜像层数据,OCI规范允许任意数量注解,对镜像存储和传输影响可忽略。
Q3:私钥泄露怎么办?
A:立即撤销私钥对应的证书(如有CA),并重新签发所有镜像,同时更新验证脚本中的公钥,建议采用短期证书(如30天有效),且在HSM中生成私钥。
Q4:脚本能否支持多架构镜像(manifest list)?
A:需要调整,上述脚本仅对单一架构镜像签名,对多架构清单(manifest list),需先对每个子清单(platform manifest)分别签名,再在父清单注解中打包所有子签名,可使用skopeo list命令枚举子镜像。
Q5:验证脚本在离线环境如何获取公钥?
A:建议通过ConfigMap或SealedSecret注入到节点(如Kubernetes Secret),脚本依赖公钥文件路径,可将公钥硬编码为环境变量或绑定卷挂载。
Q6:使用base64编码签名是否有长度限制?
A:RSA 2048位签名约256字节,base64后约344字符,OCI注解值最大为63KB,绰绰有余,ECDSA签名更短(约70-80字节)。
本文从架构设计、Shell脚本实现、CI/CD集成到生产验证,完整覆盖了容器镜像签名的自动化流程,这套基于原生工具链的方案,无需引入额外中间件即可为镜像提供“出厂级”安全验证,在实际落地时,建议结合自身密钥管理体系(如KMS、HSM)和策略引擎(如Kyverno)进一步增强,安全没有银弹,但签名验证是不可缺少的第一道防线——写下来,自动起来,执行起来。