本文介绍如何把 acme.sh 已签发的 ZeroSSL ECC 证书自动复制到 Db2 服务器,再使用 OpenSSL 和 IBM GSKit 转换、导入 Db2 可识别的 KDB 密钥数据库,最后配置 Db2 TLS。

示例域名为 example.com,Db2 实例用户为 db2inst1,服务器为 db2.example.com(域名和IP地址皆可)。请按实际环境替换。

1. 工作原理

1. acme.sh 从 ZeroSSL 获取或续期证书;
2. SSH deploy hook 将服务器私钥、服务器证书和中间 CA 复制到 Db2 主机;
3. 远程执行 update-db2-cert.sh
4. 脚本从系统信任库提取 USERTrust ECC 根证书;
5. OpenSSL 将私钥、服务器证书和中间证书打包为 PKCS#12;
6. GSKit 创建 KDB,导入个人证书及完整信任链;
7. 更新 Db2 Database Manager Configuration 并重启服务。

证书链通常为:

example.com 服务器证书
  └─ ZeroSSL ECC DV SSL CA 2(中间 CA)
       └─ USERTrust ECC Certification Authority(根 CA)

KDB 中必须具备带私钥的服务器证书(personal certificate),以及中间 CA、根 CA(signer certificate)。

2. 前置条件

Db2 服务器需要安装 IBM Db2、GSKit、OpenSSL 和 trust(通常来自 p11-kit):

command -v db2
command -v gsk9certutil_64
command -v openssl
command -v trust

创建证书目录和密码文件:

# 这里假定你创建db2实例是使用的用户的默认用户组是db2iadm1
sudo install -d -m 700 -o db2inst1 -g db2iadm1 /home/db2inst1/ssl
sudo -u db2inst1 sh -c 'umask 077; printf "%s\n" "替换为高强度密码" > /home/db2inst1/ssl/.kdb_password'

建议私钥、KDB、stash 和密码文件权限为 600.sth 虽不是明文密码文件,仍应按敏感凭据保护。

如果脚本通过 systemd 管理 Db2,需给 db2inst1 最小化 sudo 权限。先确认服务单元和 systemctl 路径,再通过 visudo 配置,例如:

db2inst1 ALL=(root) NOPASSWD: /usr/bin/systemctl stop db2inst1, /usr/bin/systemctl start db2inst1

3. 配置 SSL 服务端口

本文使用服务名 db2cs_db2inst1。确保 /etc/services 中有对应条目:

db2cs_db2inst1  50001/tcp

验证并开放防火墙端口:

getent services db2cs_db2inst1

4. 部署脚本

将以下内容保存为 /home/db2inst1/ssl/update-db2-cert.sh

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

SSL_DIR="/home/db2inst1/ssl"
KDB="$SSL_DIR/db2.kdb"
P12="$SSL_DIR/db2.p12"
ACME_DOMAIN_NAME="example.com" # 换成你自己的域名
ROOT_CA="$SSL_DIR/usertrust-ecc-root.pem"
KEY_FILE="$SSL_DIR/$ACME_DOMAIN_NAME.key.pem"
CERT_FILE="$SSL_DIR/$ACME_DOMAIN_NAME.pem"
CA_FILE="$SSL_DIR/ca.pem"
PASSWORD_FILE="$SSL_DIR/.kdb_password"
CERT_LABEL="CN=$ACME_DOMAIN_NAME"
DB2_SSL_SERVICE="db2cs_db2inst1"
SYSTEMD_UNIT="db2inst1"

# SSH 非交互会话通常不会自动加载 Db2 环境。
. /home/db2inst1/sqllib/db2profile
cd "$SSL_DIR"

TMP_DIR=""
cleanup() {
    rm -f "$P12"
    if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
        rm -rf "$TMP_DIR"
    fi
}
trap cleanup EXIT

for cmd in openssl trust gsk9certutil_64 db2; do
    command -v "$cmd" >/dev/null 2>&1 || {
        echo "错误:找不到命令 $cmd" >&2
        exit 1
    }
done

for file in "$KEY_FILE" "$CERT_FILE" "$CA_FILE" "$PASSWORD_FILE"; do
    [[ -s "$file" ]] || {
        echo "错误:文件不存在或为空:$file" >&2
        exit 1
    }
done

PASSWORD="$(<"$PASSWORD_FILE")"
[[ -n "$PASSWORD" ]] || { echo "错误:KDB 密码为空" >&2; exit 1; }

# 至少保证证书在未来 24 小时内仍有效。
openssl x509 -in "$CERT_FILE" -checkend 86400 -noout

# ECC/RSA 均适用:比较公钥摘要,确认私钥与证书匹配。
CERT_PUBKEY_HASH="$(openssl x509 -in "$CERT_FILE" -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256)"
KEY_PUBKEY_HASH="$(openssl pkey -in "$KEY_FILE" -pubout -outform DER | openssl sha256)"
[[ "$CERT_PUBKEY_HASH" == "$KEY_PUBKEY_HASH" ]] || {
    echo "错误:证书与私钥不匹配" >&2
    exit 1
}

echo "[1/10] 停止 Db2"
sudo systemctl stop "$SYSTEMD_UNIT" || true

echo "[2/10] 提取 USERTrust ECC 根 CA"
TMP_DIR="$(mktemp -d)"
trust extract --format=pem-bundle --filter=ca-anchors "$TMP_DIR/ca-bundle.pem"

awk -v dir="$TMP_DIR" '
/BEGIN CERTIFICATE/ { n++ }
n > 0 { print > (dir "/cert-" n ".pem") }
' "$TMP_DIR/ca-bundle.pem"

FOUND=0
for cert in "$TMP_DIR"/cert-*.pem; do
    if openssl x509 -in "$cert" -subject -noout 2>/dev/null | grep -Fq "USERTrust ECC Certification Authority"; then
        cp "$cert" "$ROOT_CA"
        FOUND=1
        break
    fi
done
[[ "$FOUND" -eq 1 ]] || { echo "错误:未找到 USERTrust ECC 根 CA" >&2; exit 1; }

echo "[3/10] 删除旧 KDB"
rm -f "$KDB" "${KDB%.kdb}.rdb" "${KDB%.kdb}.crl" "${KDB%.kdb}.sth" "$P12"

echo "[4/10] 创建 KDB"
gsk9certutil_64 -keydb -create -db "$KDB" -pw "$PASSWORD" -type kdb -stash

echo "[5/10] 创建 PKCS#12"
openssl pkcs12 -export \
    -inkey "$KEY_FILE" \
    -in "$CERT_FILE" \
    -certfile "$CA_FILE" \
    -name "$CERT_LABEL" \
    -out "$P12" \
    -passout pass:"$PASSWORD"

echo "[6/10] 导入服务器证书"
gsk9certutil_64 -cert -import \
    -file "$P12" -type p12 -pw "$PASSWORD" \
    -target "$KDB" -target_type kdb -target_pw "$PASSWORD"

echo "[7/10] 导入中间 CA"
# PKCS#12 导入后中间证书可能已存在;如版本行为不同,可保留此显式导入。
if ! gsk9certutil_64 -cert -list -db "$KDB" -pw "$PASSWORD" | grep -Fq "ZeroSSL ECC DV SSL CA 2"; then
    gsk9certutil_64 -cert -add -db "$KDB" -pw "$PASSWORD" \
        -file "$CA_FILE" -label "ZeroSSL ECC DV SSL CA 2"
fi

echo "[8/10] 导入根 CA"
if ! gsk9certutil_64 -cert -list -db "$KDB" -pw "$PASSWORD" | grep -Fq "USERTrust ECC Certification Authority"; then
    gsk9certutil_64 -cert -add -db "$KDB" -pw "$PASSWORD" \
        -file "$ROOT_CA" -label "USERTrust ECC Certification Authority"
fi

echo "[9/10] 验证 KDB"
gsk9certutil_64 -cert -list -db "$KDB" -pw "$PASSWORD"
gsk9certutil_64 -cert -details -db "$KDB" -pw "$PASSWORD" -label "$CERT_LABEL"
chmod 600 "$KDB" "${KDB%.kdb}.sth" "$PASSWORD_FILE" "$KEY_FILE"

echo "[10/10] 配置 Db2 TLS"
db2 update dbm cfg using SSL_SVR_KEYDB "$KDB"
db2 update dbm cfg using SSL_SVR_STASH "${KDB%.kdb}.sth"
db2 update dbm cfg using SSL_SVR_LABEL "$CERT_LABEL"
db2 update dbm cfg using SSL_SVCENAME "$DB2_SSL_SERVICE"
db2set DB2COMM=TCPIP,SSL

sudo systemctl start "$SYSTEMD_UNIT"
echo "完成"

db2无法使用fullchain证书公钥,必须将证书链层层拆开

赋予执行权限:

chown db2inst1:db2iadm1 /home/db2inst1/ssl/update-db2-cert.sh
chmod 700 /home/db2inst1/ssl/update-db2-cert.sh

Shell 续行符 \ 后不能有空格。复制脚本时请勿把显示层的空格带到反斜杠后面。

5. acme.sh Multi Deploy 配置

首先在db2的证书存储路径先创建证书的同名文件占位,防止acme脚本失败(它会备份旧的证书,如果路径里找不到证书同名文件,会直接报错)

cd /home/db2inst1/ssl/
touch example.com.key.pem" # 将“example.com”换成你的域名
touch example.com.pem" # 将“example.com”换成你的域名

接下来,推荐使用 multideploy 的 delployhook 来部署证书。

在 acme.sh 的配置存储路径中新建multideploy.yml
touch ~/.acme.sh/example.com_ecc/multideploy.yml 将“example.com”换成你的域名

示例节选:

version: "1.0"

services:
  - name: "db2"
    hook: "ssh"
    environment:
      DEPLOY_SSH_USER: "db2inst1"
      DEPLOY_SSH_SERVER: "db2.example.com" # 将db2.example.com换成你的db2服务器的域名/IP地址
      DEPLOY_SSH_KEYFILE: "/home/db2inst1/ssl/example.com.key.pem" # 将“example.com”换成你的域名
      DEPLOY_SSH_CERTFILE: "/home/db2inst1/ssl/example.com.pem" # 将“example.com”换成你的域名
      DEPLOY_SSH_CAFILE: "/home/db2inst1/ssl/ca.pem"
      DEPLOY_SSH_REMOTE_CMD: "/home/db2inst1/ssl/update-db2-cert.sh"
      DEPLOY_SSH_USE_SCP: "yes"

脚本写完之后,部署证书:

acme.sh --deploy -d example.com --deploy-hook multideploy # 将“example.com”换成你的域名

注意:原配置中的 DEPLOY_SSH_USE_SCP: "yes 缺少右侧双引号,会导致 YAML 解析失败。

这些路径表示证书和脚本的远端存储位置。acme.sh 在续期成功后复制证书并执行远程命令。首次自动部署前,建议先手工运行脚本验证 sudo、Db2 profile、GSKit 和服务名均正确。

6. 标签为什么必须一致

SSL_SVR_LABEL 必须指向 KDB 中包含私钥的个人证书标签,而不是 CA 标签。PKCS#12 导入时标签可能来自 friendlyName,也可能被工具按证书主题生成。

脚本通过以下参数固定标签:

openssl pkcs12 -export -name "CN=example.com" ...

随后 Db2 使用同一个值:

db2 update dbm cfg using SSL_SVR_LABEL "CN=example.com"

务必以实际列表为准:

gsk9certutil_64 -cert -list -db /home/db2inst1/ssl/db2.kdb -stashed

如果 GSKit 版本未保留 friendlyName,应把 CERT_LABELSSL_SVR_LABEL 改为 KDB 中实际显示的个人证书标签。

7. 验证部署

查看 Db2 配置:

db2 get dbm cfg | grep -i SSL
db2set -all | grep DB2COMM

查看监听端口:

ss -lntp | grep 50001

从客户端检查握手和证书链:

openssl s_client \
  -connect db2.example.com:50001 \
  -servername example.com # 换成你申请证书的域名\
  -showcerts \
  -verify_return_error </dev/null

应重点确认:

  • 服务端返回 example.com 证书;
  • SAN 包含客户端实际使用的主机名;
  • 能构建到 USERTrust ECC 根 CA 的信任链;
  • 最终显示 Verify return code: 0 (ok)

客户端连接时必须使用 SSL 端口并启用 SSL。不同 Db2 驱动的参数名称不同,常见形式为 security=SSLsslConnection=true

8. 技术细节

8.1 PEM、PKCS#12 与 KDB

acme.sh 输出通常是 PEM:私钥与证书分别存储。Db2 服务端使用 GSKit KDB,服务器证书必须在 KDB 中与私钥关联。PKCS#12 是两者之间的通用中间格式,可同时携带私钥、服务器证书和附加证书链。

8.2 KDB 配套文件

创建 KDB 时可能出现:

  • db2.kdb:密钥数据库;
  • db2.sth:stash 文件,供 Db2 无人值守读取 KDB;
  • db2.rdb:请求数据库;
  • db2.crl:证书吊销相关数据库。

SSL_SVR_STASH 必须指向实际生成的 db2.sth

8.3 为什么不应只导入服务器证书

只有服务器证书和私钥时,GSKit 或客户端可能无法构建完整证书链。中间 CA 通常必须由服务端提供;根 CA主要用于建立信任。显式导入中间证书和根证书可减少不同 GSKit 版本、PKCS#12 导入行为和系统信任库差异造成的问题。

8.4 为什么比较公钥而不是 modulus

传统 RSA 脚本常比较 modulus,但 ECC 证书没有 RSA modulus。本文提取证书和私钥的 DER 公钥并计算 SHA-256,因而同时适用于 RSA 和 ECC。

8.5 重建 KDB 的取舍

本文每次续期都重建 KDB,优点是流程确定、不会残留旧标签;缺点是脚本失败期间服务可能处于停止状态,且会移除 KDB 中其他证书。若同一 KDB 还保存其他用途证书,应改为原位更新,或为 Db2 TLS 使用独立 KDB。

生产环境还可先构建临时 KDB,验证成功后再原子替换,以缩短中断并保留回滚能力。

9. 常见故障

db2: command not found

SSH 非交互会话没有加载实例环境。确认脚本包含:

. /home/db2inst1/sqllib/db2profile

找不到根证书

检查系统是否安装或更新 CA 信任库:

trust list | grep -A3 -B3 "USERTrust ECC"

也可将经过校验的根证书固定放入 SSL 目录,但不要从不可信 URL 临时下载后直接导入。

SSL_SVR_LABEL 不存在

列出 KDB,找到带私钥的个人证书标签,并同步更新 CERT_LABEL 与 Db2 配置。不要误用中间 CA 或根 CA 标签。

Db2 启动后未监听 SSL 端口

检查 /etc/servicesSSL_SVCENAMEDB2COMM、防火墙和 Db2 日志;修改 Database Manager Configuration 后通常需要重启实例。

客户端提示证书主机名不匹配

客户端连接名必须存在于证书 SAN。使用 IP 地址连接时,证书通常没有对应 IP SAN;应使用证书中的 DNS 名,并保证 DNS 或 hosts 解析正确。

脚本停止 Db2 后失败

当前脚本采取 fail-fast 策略。生产环境建议增加失败恢复 trap、保留上一套 KDB,或先在临时路径完成构建和验证,再切换文件并重启。

10. 安全建议

  • 不要把 .kdb_password、私钥、KDB、stash 提交到 Git;
  • 限制 SSL 目录权限,仅允许实例用户访问;
  • acme.sh 主机到 Db2 主机使用专用 SSH key,并限制 authorized_keys 权限;
  • sudo 只放行指定的 start/stop 命令;
  • 定期检查证书到期时间和自动部署日志;
  • 若无兼容性需求,可考虑仅启用 DB2COMM=SSL,避免同时暴露明文 TCP 监听;
  • 在生产变更前保留旧 KDB 和 Db2 配置,以便快速回滚。

完成以上配置后,后续 acme.sh 续期成功时即可自动复制新证书、重建 Db2 KDB 并使新证书生效。