4. 批量吊销脚本编写:Shell脚本批量读取CSV、循环调用吊销命令

说实话,很多运维兄弟拿到证书吊销需求时,第一反应是「一条一条手动敲」。几十个证书还好说,一旦上了几百上千,手敲不仅累,还容易漏掉或输错序列号。我早期在数据中心就吃过这个亏——手动吊销漏了一条,结果那个证书被审计揪出来,差点背了个事故。

所以,批量吊销脚本不是「锦上添花」,而是「保命工具」。这一章我们就来写一个靠谱的 Shell 脚本,让它替我们干脏活累活。

4.1 脚本的整体思路

先想清楚我们要做什么。说白了就三步:

  1. 读一个 CSV 文件,里面存着所有待吊销证书的信息
  2. 逐行解析,提取序列号或证书文件路径
  3. 调用 OpenSSL 或 CA 系统的吊销命令,把证书废掉

嗯,逻辑不复杂。但坑往往藏在细节里——比如 CSV 里混了空行、序列号大小写不一致、或者吊销命令返回了非零退出码却没被捕获。我们写脚本时得把这些都考虑进去。

核心原则:脚本要能「无人值守跑完」,但也要「出错就停,别闯祸」。

4.2 CSV 文件格式设计

CSV 的格式决定了脚本的解析逻辑。我个人习惯用这种结构:

字段 说明 示例
serial 证书序列号(十六进制,可带冒号) 0A:1B:2C:3D:4E:5F
reason 吊销原因码(0-5) 1(keyCompromise)
date 吊销时间(可选,留空则用当前时间) 2025-04-01
comment 备注(仅用于记录,脚本忽略) 员工离职,设备回收

CSV 文件示例:

serial,reason,date,comment
0A:1B:2C:3D:4E:5F,1,2025-04-01,员工A离职
1A:2B:3C:4D:5E:6F,2,,设备被盗
2A:3B:4C:5D:6E:7F,0,2025-04-02,证书到期前撤销

小技巧:序列号建议统一用大写十六进制,中间带冒号。这样跟 OpenSSL 的显示格式一致,避免大小写匹配问题。我曾在项目里因为小写序列号匹配不上,排查了半天才发现是大小写惹的祸。

4.3 核心脚本实现

下面这个脚本是我在实际项目中打磨过的版本。它做了几件事:

  • 检查 CSV 文件是否存在、格式是否正确
  • 跳过表头行
  • 忽略空行和注释行(以 # 开头)
  • 逐行解析,调用 openssl ca -revoke 命令
  • 记录成功和失败的数量,最后汇总报告
#!/bin/bash
# ============================================
# 批量吊销证书脚本
# 用法: ./batch_revoke.sh <csv_file>
# CSV 格式: serial,reason,date,comment
# ============================================

set -euo pipefail

CSV_FILE="${1:-}"
CA_DIR="/etc/ssl/ca"
CA_KEY="${CA_DIR}/private/ca.key.pem"
CA_CERT="${CA_DIR}/certs/ca.cert.pem"
CA_DB="${CA_DIR}/index.txt"

if [[ -z "$CSV_FILE" ]]; then
    echo "错误: 请指定 CSV 文件路径"
    echo "用法: $0 <csv_file>"
    exit 1
fi

if [[ ! -f "$CSV_FILE" ]]; then
    echo "错误: 文件 $CSV_FILE 不存在"
    exit 1
fi

# 计数器
SUCCESS=0
FAILED=0
TOTAL=0

echo "开始批量吊销证书..."
echo "CSV 文件: $CSV_FILE"
echo "CA 目录: $CA_DIR"
echo "----------------------------------------"

# 逐行读取 CSV,跳过表头
tail -n +2 "$CSV_FILE" | while IFS=',' read -r serial reason date comment; do
    # 跳过空行和注释行
    [[ -z "$serial" || "$serial" == \#* ]] && continue

    # 去除首尾空白
    serial=$(echo "$serial" | xargs)
    reason=$(echo "$reason" | xargs)
    date=$(echo "$date" | xargs)

    # 如果日期为空,使用当前时间
    if [[ -z "$date" ]]; then
        date=$(date +%Y%m%d%H%M%S)
    fi

    TOTAL=$((TOTAL + 1))
    echo "[$TOTAL] 正在吊销: $serial (原因: $reason)"

    # 执行吊销命令
    if openssl ca -config "${CA_DIR}/openssl.cnf" \
                  -revoke "${CA_DIR}/newcerts/${serial}.pem" \
                  -crl_reason "$reason" \
                  -crl_hold "$date" \
                  -keyfile "$CA_KEY" \
                  -cert "$CA_CERT" \
                  -passin pass:your_ca_password 2>> revoke_error.log; then
        echo "  ✓ 吊销成功"
        SUCCESS=$((SUCCESS + 1))
    else
        echo "  ✗ 吊销失败,请查看 revoke_error.log"
        FAILED=$((FAILED + 1))
    fi
done

echo "----------------------------------------"
echo "批量吊销完成。"
echo "总计: $TOTAL | 成功: $SUCCESS | 失败: $FAILED"

注意:脚本中的 CA 密码是硬编码的,生产环境绝对不要这么干!建议用环境变量或密钥管理服务来传递密码。我曾经见过有人把密码写在脚本里然后传到 Git 仓库,那画面太美我不敢看。

4.4 脚本的关键点解析

为什么这个脚本能干活?我们来拆解几个关键设计:

4.4.1 错误处理

set -euo pipefail 这行很重要。它让脚本在遇到任何错误时立即退出,避免「一个吊销失败,后面的还在傻乎乎地继续」。但注意,我们在循环里用了 if ... then ... else 来捕获单个吊销的结果,所以即使某个证书吊销失败,脚本也不会整体退出——这才是我们想要的行为。

4.4.2 日志记录

我把错误输出重定向到了 revoke_error.log,而不是直接扔到终端。为什么?因为批量跑的时候,终端输出刷得飞快,你根本来不及看。跑完后去翻日志,一目了然。

4.4.3 序列号路径映射

脚本假设证书文件存放在 ${CA_DIR}/newcerts/${serial}.pem。这是 OpenSSL 默认 CA 的目录结构。如果你的环境不一样,记得改路径。我建议在脚本开头定义一个变量,方便统一修改。

4.5 脚本执行流程

为了让你更直观地理解整个流程,我画了一张图:

批量吊销脚本执行流程 批量吊销脚本执行流程图 开始 检查 CSV 文件是否存在 文件有效? 退出并报错 逐行读取 CSV(跳过表头) 还有行? 结束 调用 openssl ca -revoke 记录成功/失败,继续下一行 循环直到所有行处理完毕 最后输出汇总统计

4.6 脚本的增强与变体

基础版本能跑,但生产环境往往需要更多功能。这里分享几个我常用的增强点:

4.6.1 支持多种吊销方式

有些场景下,你拿到的不是序列号,而是证书文件本身。可以加一个参数来切换模式:

# 根据参数决定吊销方式
if [[ "$MODE" == "serial" ]]; then
    openssl ca -revoke "${CA_DIR}/newcerts/${serial}.pem" ...
elif [[ "$MODE" == "file" ]]; then
    openssl ca -revoke "$cert_file" ...
fi

4.6.2 生成吊销列表(CRL)

吊销完证书后,别忘了生成新的 CRL。我习惯在脚本末尾自动执行:

# 所有吊销完成后,生成新的 CRL
echo "正在生成新的 CRL..."
openssl ca -gencrl -out "${CA_DIR}/crl/ca.crl.pem" \
           -config "${CA_DIR}/openssl.cnf" \
           -keyfile "$CA_KEY" \
           -cert "$CA_CERT" \
           -passin pass:your_ca_password
echo "CRL 已生成: ${CA_DIR}/crl/ca.crl.pem"

4.6.3 干跑模式(Dry Run)

这个功能我强烈建议加上。在真正执行吊销前,先跑一遍干跑模式,只打印要执行的命令,不实际执行。这样能提前发现 CSV 里的错误:

if [[ "$DRY_RUN" == "true" ]]; then
    echo "[干跑] 将会吊销: $serial (原因: $reason)"
    continue
fi

我的习惯:每次批量吊销前,先开干跑模式检查一遍。确认无误后,再切到正式模式。这个习惯帮我避免过至少三次「误吊销」事故。

4.7 常见问题与避坑

写这个脚本时,有几个坑我踩过,你注意避开:

  • CSV 里有多余空格:序列号前后多了空格,会导致文件路径拼接错误。用 xargs 或 sed 去掉首尾空白。
  • 序列号格式不一致:有的带冒号(0A:1B),有的不带(0A1B)。建议在脚本里统一格式化,比如去掉冒号再比较。
  • 密码泄露风险:前面提过,别硬编码密码。用 openssl ca -passin env:CA_PASS 从环境变量读取。
  • 并发问题:如果证书数量巨大(比如上万),逐行吊销会很慢。可以考虑用 xargs -P 做并行吊销,但要注意 CA 数据库的锁冲突。

曾经有一次:我在一个客户现场跑批量吊销,CSV 里混了一条「序列号为空」的记录。脚本没做空值检查,结果 openssl ca -revoke 报了个奇怪的错误,卡住了整个流程。从那以后,我所有脚本都加了「空行跳过」和「字段非空校验」。

4.8 小结

批量吊销脚本的核心就三件事:读 CSV、调命令、记日志。但把这三件事做扎实了,就能避免很多低级错误。这一章的脚本你可以直接拿去改改用,但记得根据你的 CA 环境调整路径和参数。

嗯,脚本写好了,下一步就是测试。我建议先在测试环境跑一遍,确认没问题再上生产。毕竟证书吊销这事,开弓没有回头箭。