CA 证书生成完整指南

6 阅读6分钟

本指南说明如何搭建私有 CA、按业务系统签发子系统证书,并配置 HTTPS 与客户端信任。证书目录按 CA 根各子系统 分目录存放,便于维护。

一、目录结构说明

按业务划分:CA 根 单独目录,每个子系统 一个子目录,避免混在一起。

/opt/private-ca/
├── ca/                              # CA 根证书与签发状态
│   ├── private/
│   │   └── ca.key.pem                # CA 私钥(严格保密)
│   ├── certs/
│   │   └── ca.cert.pem              # CA 根证书(可分发)
│   ├── newcerts/                    # 签发时自动生成,可按 serial 保留或定期清理
│   ├── crl/                         # 证书撤销列表
│   ├── index.txt
│   ├── serial
│   ├── ca.conf
│   └── server.conf                  # 子系统 CSR 用模板
├── subsystems/                      # 各子系统证书(按名称分目录)
│   ├── <子系统1>/                   # 如 cas、permission、ooo-demo
│   │   ├── <名>.key.pem             # 私钥
│   │   ├── <名>.cert.pem            # 证书
│   │   ├── <名>-chain.pem           # 证书链( cert + CA,供 Nginx 等)
│   │   └── <名>.p12                 # PKCS12(供 Java/Tomcat/Spring Boot)
│   └── <子系统2>/
│       └── ...
└── generate-cert.sh                 # 签发脚本(同时生成 PEM + P12 + chain)
  • 不保留:CSR 与临时扩展文件在 /tmp 生成,签发后即删,不落盘。
  • newcerts/:OpenSSL 签发时自动写入,用于审计与撤销;无需手动维护,可按策略定期清理旧 serial 文件。

二、初始化 CA 环境

mkdir -p /opt/private-ca/ca/{private,certs,newcerts,crl}
mkdir -p /opt/private-ca/subsystems
cd /opt/private-ca

touch ca/index.txt
echo 1000 > ca/serial

三、CA 配置文件

ca/ca.conf — CA 根证书与签发配置:

[ ca ]
default_ca      = CA_default

[ CA_default ]
database        = ./ca/index.txt
new_certs_dir   = ./ca/newcerts
serial          = ./ca/serial
policy          = policy_any
copy_extensions = none
unique_subject  = no
private_key     = ./ca/private/ca.key.pem
certificate     = ./ca/certs/ca.cert.pem

[ policy_any ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = optional
emailAddress            = optional

[ req ]
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = CN
stateOrProvinceName             = State or Province Name
stateOrProvinceName_default     = Beijing
localityName                    = Locality Name
localityName_default            = Beijing
0.organizationName              = Organization Name
0.organizationName_default      = YourCompany
organizationalUnitName          = Organizational Unit Name
organizationalUnitName_default  = IT Department
commonName                      = Common Name
commonName_max                  = 64
commonName_default              = Your Company Root CA
emailAddress                    = Email Address
emailAddress_default            = ca@yourcompany.com

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

unique_subject = no 允许多张证书共用一个 Subject(如多子系统共用同一 IP),避免 “already a certificate for” 报错。


四、生成 CA 根证书

cd /opt/private-ca

openssl genrsa -out ca/private/ca.key.pem 4096
chmod 400 ca/private/ca.key.pem

openssl req -config ca/ca.conf -x509 -days 7300 -sha256 -batch \
    -key ca/private/ca.key.pem \
    -out ca/certs/ca.cert.pem
chmod 444 ca/certs/ca.cert.pem

-batch 表示不交互,直接使用配置文件中的默认 DN 并生成证书,无需手动输入 yes。 有效期:CA 与子系统证书均为 7300 天(约 20 年),可按需改 -days


五、子系统请求配置

ca/server.conf — 仅用于生成子系统 CSR,SAN 由脚本在签发时注入:

[ req ]
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
req_extensions      = v3_req

[ req_distinguished_name ]
countryName_default             = CN
stateOrProvinceName_default     = Beijing
localityName_default            = Beijing
0.organizationName_default      = YourCompany
organizationalUnitName_default  = IT Department

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth

不要在此写 subjectAltName = @alt_names,否则 openssl req 会因缺少 [ alt_names ] 报错。


六、生成子系统证书(脚本)

脚本会:生成私钥与证书、同时生成 P12 与证书链,CSR 与临时配置仅在 /tmp 使用后删除。

generate-cert.sh(放在 /opt/private-ca/):

#!/bin/bash
# generate-cert.sh — 按子系统生成 PEM + P12 + 证书链,无冗余文件

SUBSYSTEM_NAME=$1
DOMAIN=$2
shift 2
EXTRA_DOMAINS=("$@")

if [ -z "$SUBSYSTEM_NAME" ] || [ -z "$DOMAIN" ]; then
    echo "用法: $0 <子系统名称> <主域名> [额外域名1] [额外域名2] ..."
    echo "示例: $0 cas 127.0.0.1 192.168.0.109"
    echo "示例: $0 permission 192.168.0.117 192.168.0.109"
    exit 1
fi

BASE=/opt/private-ca
cd "$BASE"
SUB_DIR=subsystems/${SUBSYSTEM_NAME}
mkdir -p "$SUB_DIR"

echo "生成证书: $SUBSYSTEM_NAME -> $SUB_DIR/"

# 临时文件仅用 /tmp,签发后删除
EXT_CNF=/tmp/${SUBSYSTEM_NAME}_ext.cnf
CSR_PEM=/tmp/${SUBSYSTEM_NAME}.csr.pem

# 私钥
openssl genrsa -out "$SUB_DIR/${SUBSYSTEM_NAME}.key.pem" 2048
chmod 400 "$SUB_DIR/${SUBSYSTEM_NAME}.key.pem"

# SAN 扩展
cat > "$EXT_CNF" << EOF
[ ext ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = ${DOMAIN}
DNS.2 = ${DOMAIN%%.*}
EOF
idx=3
for domain in "${EXTRA_DOMAINS[@]}"; do
    if [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        echo "IP.$((idx++)) = $domain" >> "$EXT_CNF"
    else
        echo "DNS.$((idx++)) = $domain" >> "$EXT_CNF"
    fi
done

# CSR
openssl req -config ca/server.conf -new -sha256 \
    -key "$SUB_DIR/${SUBSYSTEM_NAME}.key.pem" \
    -out "$CSR_PEM" \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=YourCompany/OU=IT Department/CN=${SUBSYSTEM_NAME}"

# 签发
openssl ca -config ca/ca.conf -days 7300 -md sha256 \
    -in "$CSR_PEM" \
    -out "$SUB_DIR/${SUBSYSTEM_NAME}.cert.pem" \
    -notext -batch \
    -extfile "$EXT_CNF" \
    -extensions ext

# 清理临时文件
rm -f "$EXT_CNF" "$CSR_PEM"

# 证书链(Nginx 等)
cat "$SUB_DIR/${SUBSYSTEM_NAME}.cert.pem" > "$SUB_DIR/${SUBSYSTEM_NAME}-chain.pem"
cat ca/certs/ca.cert.pem >> "$SUB_DIR/${SUBSYSTEM_NAME}-chain.pem"

# P12(Java/Tomcat/Spring Boot)
echo "请设置 P12 密码(用于 $SUB_DIR/${SUBSYSTEM_NAME}.p12):"
openssl pkcs12 -export -out "$SUB_DIR/${SUBSYSTEM_NAME}.p12" \
    -inkey "$SUB_DIR/${SUBSYSTEM_NAME}.key.pem" \
    -in "$SUB_DIR/${SUBSYSTEM_NAME}.cert.pem" \
    -CAfile ca/certs/ca.cert.pem \
    -chain

echo "完成: $SUB_DIR/"
echo "  - ${SUBSYSTEM_NAME}.key.pem  ${SUBSYSTEM_NAME}.cert.pem  ${SUBSYSTEM_NAME}-chain.pem  ${SUBSYSTEM_NAME}.p12"

使用示例:

chmod +x /opt/private-ca/generate-cert.sh

# 单 IP
./generate-cert.sh cas 127.0.0.1 192.168.0.109

# 多 IP / 多域名
./generate-cert.sh permission 192.168.0.117 192.168.0.109
./generate-cert.sh ooo-demo 192.168.0.117 192.168.0.109

生成后,该子系统目录下会有:<名>.key.pem<名>.cert.pem<名>-chain.pem<名>.p12,无需再单独导出。


七、证书导出与格式

脚本已生成 P12 与证书链,一般无需再导。若需其他格式:

JKS(可选):

keytool -importkeystore \
    -srckeystore /opt/private-ca/subsystems/<子系统名>/<名>.p12 \
    -srcstoretype PKCS12 \
    -destkeystore /path/to/<名>.jks \
    -deststoretype JKS

Nginx 直接使用同目录下的 <名>-chain.pem<名>.key.pem 即可。


八、HTTPS 配置示例

以下路径均以 subsystems/<子系统名>/ 为例。

场景证书链/证书私钥 / Keystore
Nginx<名>-chain.pem<名>.key.pem
Tomcat / Spring Boot<名>.p12

Nginx:

ssl_certificate     /opt/private-ca/subsystems/<子系统名>/<名>-chain.pem;
ssl_certificate_key /opt/private-ca/subsystems/<子系统名>/<名>.key.pem;

Spring Boot(文件在服务器上):

server.ssl.key-store=file:/opt/private-ca/subsystems/<子系统名>/<名>.p12
server.ssl.key-store-password=<P12密码>
server.ssl.key-store-type=PKCS12

客户端信任 CA: 使用 ca/certs/ca.cert.pem(见下一节)。


九、客户端信任私有 CA

以下路径以 /opt/private-ca/ca/certs/ca.cert.pem 为例。

环境命令
Linuxsudo cp /opt/private-ca/ca/certs/ca.cert.pem /usr/local/share/ca-certificates/yourcompany.crt
sudo update-ca-certificates
macOSsudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /opt/private-ca/ca/certs/ca.cert.pem
WindowsImport-Certificate -FilePath "C:\path\to\ca.cert.pem" -CertStoreLocation Cert:\LocalMachine\Root
Javakeytool -importcert -alias yourcompany-ca -file /opt/private-ca/ca/certs/ca.cert.pem -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit
浏览器Chrome: 证书管理 → 受信任的根证书颁发机构 → 导入;Firefox: 隐私与安全 → 证书 → 导入

十、常见问题与解决方案

1. PKIX path building failed

现象: unable to find valid certification path to requested target(如 CAS Client 访问 CAS Server 的 HTTPS)。

原因: 当前 JVM 的信任库(cacerts)里没有签发对方证书的 CA。

处理:运行应用的机器上,用运行应用的那份 Java 的 keytool,将 CA 导入该 JVM 的 cacerts:

keytool -importcert -alias yourcompany-ca -file /opt/private-ca/ca/certs/ca.cert.pem \
    -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit

或使用自定义 truststore,启动时加 -Djavax.net.ssl.trustStore=... -Djavax.net.ssl.trustStorePassword=...

2. 已导入 CA 仍报 PKIX

原因: 导入到了别的 Java 的 cacerts,或混淆了“服务器证书(keystore)”和“客户端信任库(truststore)”。P12 是服务端身份,客户端校验用的是 cacerts。

处理: 确认 which java 与 IDE/运行配置使用的 JDK,只在该 JDK 的 cacerts 中导入 CA,然后重启应用。

3. ERROR: There is already a certificate for .../CN=192.168.0.xxx

原因: 脚本里 CSR 的 CN 用了 ${DOMAIN}(IP),同一 IP 已签过一张。

处理: 脚本中 -subj 已改为 CN=${SUBSYSTEM_NAME},保证按子系统名区分。若仍报错,确认服务器上的脚本已更新为该写法;或在此 CA 的 ca.conf 中设置 unique_subject = no


十一、常用命令

PEM 证书:

# 查看证书主体与有效期
openssl x509 -in /opt/private-ca/subsystems/<名>/<名>.cert.pem -noout -subject -dates -ext subjectAltName

# 验证证书链(子系统证书是否被当前 CA 签发)
openssl verify -CAfile /opt/private-ca/ca/certs/ca.cert.pem /opt/private-ca/subsystems/<名>/<名>.cert.pem

P12(PKCS12):

# 查看 P12 中的别名与证书信息(会提示输入 P12 密码)
keytool -list -v -keystore /opt/private-ca/subsystems/<名>/<名>.p12 -storetype PKCS12

# 仅列出别名
keytool -list -keystore /opt/private-ca/subsystems/<名>/<名>.p12 -storetype PKCS12

keytool 查看 JVM 默认信任库(cacerts):

# 列出 cacerts 中所有证书别名(删除/导入前可先查看)
keytool -list -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit

# Mac JDK 8 示例(路径按实际 JDK 版本修改)
keytool -list -keystore /Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home/jre/lib/security/cacerts -storepass changeit

JDK 9+ 无 jre 子目录时,cacerts 路径为 $JAVA_HOME/lib/security/cacerts

keytool 删除证书:

# 从 JVM 默认信任库(cacerts)中按别名删除已导入的 CA
keytool -delete -alias yourcompany-ca \
    -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit

# 从自定义 truststore 中删除
keytool -delete -alias yourcompany-ca -keystore /path/to/truststore.jks -storepass <密码>

# 从 P12 中删除某别名(慎用,一般 P12 仅含一个密钥条目)
keytool -delete -alias <别名> -keystore /opt/private-ca/subsystems/<名>/<名>.p12 -storetype PKCS12

删除前可用 keytool -list -keystore ... 查看当前别名,-alias 需与导入时一致。JDK 9+ 无 jre 子目录时,cacerts 路径为 $JAVA_HOME/lib/security/cacerts

证书链:

# 查看 chain 中证书块数量(通常为 2:服务器证书 + CA)
grep -c "BEGIN CERTIFICATE" /opt/private-ca/subsystems/<名>/<名>-chain.pem

# 查看 chain 中第一张证书(服务器证书)主体与有效期
openssl x509 -in /opt/private-ca/subsystems/<名>/<名>-chain.pem -noout -subject -dates

# 以 PKCS7 形式列出 chain 中所有证书的 subject(便于核对整链)
openssl crl2pkcs7 -nocrl -certfile /opt/private-ca/subsystems/<名>/<名>-chain.pem 2>/dev/null \
  | openssl pkcs7 -print_certs -noout

CA 与撤销:

# 撤销子系统证书(路径为签发后的证书文件)
openssl ca -config /opt/private-ca/ca/ca.conf -revoke /opt/private-ca/subsystems/<名>/<名>.cert.pem

# 生成 CRL(PEM)
openssl ca -config /opt/private-ca/ca/ca.conf -gencrl -out /opt/private-ca/ca/crl/ca.crl.pem

文档中的 <名><子系统名> 请替换为实际子系统名称(如 managepermissionooo-demo)。