本指南说明如何搭建私有 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 为例。
| 环境 | 命令 |
|---|---|
| Linux | sudo cp /opt/private-ca/ca/certs/ca.cert.pem /usr/local/share/ca-certificates/yourcompany.crtsudo update-ca-certificates |
| macOS | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /opt/private-ca/ca/certs/ca.cert.pem |
| Windows | Import-Certificate -FilePath "C:\path\to\ca.cert.pem" -CertStoreLocation Cert:\LocalMachine\Root |
| Java | keytool -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
文档中的 <名>、<子系统名> 请替换为实际子系统名称(如 manage、permission、ooo-demo)。