Shell脚本如何签名容器镜像安全

wen 实用脚本 1

Shell脚本自动化签名与验证完整指南

目录导读


容器镜像安全面临的挑战

在云原生时代,容器镜像的分发和部署已成为DevOps流程的核心环节,镜像供应链攻击、中间人篡改、镜像仓库投毒等安全威胁日益严峻,根据[容器安全报告],超过60%的企业曾遇到过镜像被篡改或使用未经验证镜像的案例,传统依赖HTTPS传输安全已不足以应对复杂的攻击场景——因为镜像在构建后、存储前、拉取运行时仍存在被篡改的风险。

Shell脚本如何签名容器镜像安全

关键痛点

  • 镜像构建后:恶意代码可能在推送前被注入
  • 镜像存储时:仓库漏洞或内部威胁可修改镜像内容
  • 镜像拉取中:中间人攻击可替换为恶意版本
  • 多环境部署:不同环境(开发/测试/生产)需确保镜像一致性

解决方案:采用数字签名 + 完整性校验的组合策略,通过Shell脚本自动化签名流程,既能避免人工操作的错误与延迟,又能嵌入到现有的CI/CD工具链中,本文将从零构建一套可落地的镜像签名脚本体系。


为什么需要给容器镜像签名

镜像签名不是可选项,而是安全基线,其核心价值在于:

安全维度 签名能解决的问题
来源验证 确认镜像来自受信任的发布者
完整性校验 未在传输过程中被篡改
抗抵赖性 发布者无法否认其签发行径
审计追踪 每次签名记录可对应特定构建或发布版本

业界标准:Docker Content Trust(基于Notary)和Sigstore的Cosign工具是目前主流方案,但许多企业受限于网络环境、私有化部署或定制需求,需要基于Shell脚本封装自定义签名流程,实现与内部CA(证书颁发机构)或硬件安全模块(HSM)的集成。


Shell脚本实现镜像签名的基础架构

1 签名流程概览

[构建镜像] → [生成摘要] → [用私钥签名摘要] → [将签名附加到镜像元数据] → [推送至仓库]

2 工具选择

工具 用途 理由
openssl 密钥生成、签名、验证 系统原生,支持RSA/ECDSA
dockerskopeo 镜像操作与推送 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 集成

阶段嵌入点

  1. 构建阶段后:执行sign_image.sh签名
  2. 推送仓库前:将公钥作为受保护变量注入
  3. 部署前置:执行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的准入控制器结合

通过kyvernoOPA 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)进一步增强,安全没有银弹,但签名验证是不可缺少的第一道防线——写下来,自动起来,执行起来。

抱歉,评论功能暂时关闭!