私有 CA 与排错

用 OpenSSL 搭建私有 CA:根证书、中间证书与 mTLS

作者: · 最后核实 实测 macOS · OpenSSL 3.6.1

私有证书颁发机构(CA)用来给内网服务、测试环境和双向 TLS 签发证书,不用付费,也不会把内网主机名暴露在证书透明度日志里。下列命令用原生 OpenSSL 3.6.1 从空目录搭建两级 CA(离线根证书 + 在线中间证书),再签发服务器证书和客户端证书。示例里部分密码直接写在命令中,生产环境请改用交互输入或密码文件。

准备目录和配置文件

openssl ca 用索引文件、序列号文件和 CRL 编号文件记录状态,先建好它们,再用一个配置文件描述这个 CA。

mkdir -p pki/root pki/ica/newcerts pki/certs && cd pki
touch ica/index.txt && echo 1000 > ica/serial && echo 1000 > ica/crlnumber
chmod 700 root ica
# ca.cnf
[ ca ]
default_ca = intermediate_ca

[ intermediate_ca ]
dir               = ./ica
database          = $dir/index.txt
serial            = $dir/serial
crlnumber         = $dir/crlnumber
new_certs_dir     = $dir/newcerts
certificate       = $dir/ica.crt
private_key       = $dir/ica.key
default_md        = sha256
default_days      = 397
default_crl_days  = 7
policy            = policy_loose
copy_extensions   = copy
unique_subject    = no
crl_extensions    = crl_ext

[ policy_loose ]
countryName             = optional
organizationName        = optional
commonName              = supplied

[ v3_intermediate ]
basicConstraints        = critical, CA:TRUE, pathlen:0
keyUsage                = critical, keyCertSign, cRLSign
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always
nameConstraints         = critical, permitted;DNS:example.com, permitted;DNS:.example.com

[ server_cert ]
basicConstraints        = critical, CA:FALSE
keyUsage                = critical, digitalSignature
extendedKeyUsage        = serverAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid
crlDistributionPoints   = URI:http://pki.example.com/ica.crl

[ client_cert ]
basicConstraints        = critical, CA:FALSE
keyUsage                = critical, digitalSignature
extendedKeyUsage        = clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid

[ crl_ext ]
authorityKeyIdentifier  = keyid:always
$ find . -maxdepth 2
./root
./ica
./ica/newcerts
./ica/serial
./ica/index.txt
./ica/crlnumber
./certs
./ca.cnf

copy_extensions = copy 会把 CSR 里配置文件没有设置的扩展(比如 SAN)复制过来,而基本约束、密钥用法、EKU 这些涉及安全的扩展由 server_cert、client_cert 固定下来。对别人提交的 CSR 千万不要用 copyall,否则对方可以夹带 CA:TRUE。

生成根 CA 私钥

用加密保存的 ECDSA P-384 私钥,签完中间证书后就离线保管。

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -aes-256-cbc -out root/root.key
head -1 root/root.key
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----

genpkey 输出 PKCS#8 格式,指定加密算法后会加密保存。如果有客户端不支持 ECDSA,可以改用更保守的 RSA 4096(-algorithm RSA -pkeyopt rsa_keygen_bits:4096)。其他密钥类型见生成密钥。

自签名根证书

根证书需要 basicConstraints=critical,CA:TRUE 和 keyUsage=keyCertSign,cRLSign,有效期常见设为 10 年。

openssl req -x509 -new -key root/root.key -sha384 -days 3650 \
  -subj "/C=CN/O=Example Corp/CN=Example Root CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -out root/root.crt
openssl x509 -in root/root.crt -noout -subject -dates -ext basicConstraints,keyUsage
subject=C=CN, O=Example Corp, CN=Example Root CA
notBefore=Nov  9 18:28:37 2026 GMT
notAfter=Nov  6 18:28:37 2036 GMT
X509v3 Basic Constraints: critical
    CA:TRUE
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

req -x509 从 OpenSSL 1.1.1 起支持 -addext。CN 起一个辨识度高的名字,运维同事在信任库列表里看到的就是它。

创建并签发中间 CA

生成中间 CA 的私钥和 CSR,再用根证书按 v3_intermediate 段签发。

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -aes-256-cbc -out ica/ica.key
openssl req -new -key ica/ica.key -subj "/C=CN/O=Example Corp/CN=Example Intermediate CA" -out ica/ica.csr
openssl x509 -req -in ica/ica.csr -CA root/root.crt -CAkey root/root.key -days 1825 -sha384 \
  -extfile ca.cnf -extensions v3_intermediate -out ica/ica.crt
openssl x509 -in ica/ica.crt -noout -subject -issuer -ext basicConstraints,nameConstraints
Certificate request self-signature ok
subject=C=CN, O=Example Corp, CN=Example Intermediate CA
subject=C=CN, O=Example Corp, CN=Example Intermediate CA
issuer=C=CN, O=Example Corp, CN=Example Root CA
X509v3 Basic Constraints: critical
    CA:TRUE, pathlen:0
X509v3 Name Constraints: critical
    Permitted:
      DNS:example.com
      DNS:.example.com

pathlen:0 表示这张中间证书只能签发终端证书,不能再签下级 CA。做完这一步就把 root/root.key 转移到离线介质(加密 U 盘、离线机器)保管,只有签新的中间证书或根 CRL 时才拿出来。

用 openssl ca 签发服务器证书

openssl ca 会把每张证书记进 index.txt,之后吊销、生成 CRL 都靠它。

openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
  -keyout certs/www.key -subj "/CN=www.example.com" \
  -addext "subjectAltName=DNS:www.example.com,DNS:example.com" -out certs/www.csr
openssl ca -config ca.cnf -extensions server_cert -in certs/www.csr -out certs/www.crt -notext -batch
openssl x509 -in certs/www.crt -noout -ext subjectAltName,extendedKeyUsage
Using configuration from ca.cnf
Enter pass phrase for ./ica/ica.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'www.example.com'
Certificate is to be certified until Dec 11 18:29:09 2027 GMT (397 days)

Write out database with 1 new entries
Database updated
X509v3 Extended Key Usage: 
    TLS Web Server Authentication
X509v3 Subject Alternative Name: 
    DNS:www.example.com, DNS:example.com

-noenc 是 OpenSSL 3.0 引入的写法,取代已弃用的 -nodes,让服务器私钥不加密,Web 服务器才能直接加载。-notext 防止 openssl ca 在 PEM 前面写一大段可读文本,否则宝塔面板等界面粘贴时容易出错。客户端只看 SAN,所有域名都要写进去,CSR 的更多写法见生成 CSR。

不用 CA 数据库直接签 CSR

openssl x509 -req -CA 可以直接签发,但必须加 -copy_extensions copyall,否则 CSR 里的 SAN 会丢。

openssl x509 -req -in certs/api.csr -CA ica/ica.crt -CAkey ica/ica.key -CAcreateserial -days 90 \
  -copy_extensions copyall -extfile ca.cnf -extensions server_cert -out certs/api.crt
openssl x509 -in certs/api.crt -noout -serial -ext subjectAltName
Certificate request self-signature ok
subject=CN=api.example.com
serial=06AE478770F418A3316C8ACA2EA61D012B75A715
X509v3 Subject Alternative Name: 
    DNS:api.example.com

x509 和 req 的 -copy_extensions 是 OpenSSL 3.0 新增的;1.1.1 上只能把 SAN 写进 -extfile 指定的段里。copyall 会照单全收 CSR 里请求的所有扩展,只对自己生成的 CSR 使用。这种方式签出的证书不在 index.txt 里,openssl ca -revoke 无从得知。

签发 mTLS 用的客户端证书

客户端证书使用 clientAuth EKU,主体里一般写用户名或服务名。

openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
  -keyout certs/alice.key -subj "/O=Example Corp/CN=alice" -out certs/alice.csr
openssl ca -config ca.cnf -extensions client_cert -days 365 -in certs/alice.csr -out certs/alice.crt -notext -batch
openssl x509 -in certs/alice.crt -noout -subject -ext extendedKeyUsage
Write out database with 1 new entries
Database updated
subject=O=Example Corp, CN=alice
X509v3 Extended Key Usage: 
    TLS Web Client Authentication

公网 CA 正在取消 clientAuth:Chrome 根证书计划自 2026-06-15 起开始把 clientAuth 从公网 TLS 体系中剔除(2027-03-15 起签发的证书只能声明 serverAuth),Let's Encrypt 的 Generation Y 证书也已不含该用途。mTLS 客户端证书交给私有 CA 签发才是正路,详见 mTLS 原理和客户端认证 EKU 被移除。

拼好证书链并验证

服务器发送服务器证书加中间证书,客户端信任根证书。

cat certs/www.crt ica/ica.crt > certs/www.fullchain.pem
openssl verify -show_chain -CAfile root/root.crt -untrusted ica/ica.crt -purpose sslserver certs/www.crt
openssl verify -CAfile root/root.crt -untrusted ica/ica.crt -purpose sslclient certs/alice.crt
certs/www.crt: OK
Chain:
depth=0: CN=www.example.com (untrusted)
depth=1: C=CN, O=Example Corp, CN=Example Intermediate CA (untrusted)
depth=2: C=CN, O=Example Corp, CN=Example Root CA
certs/alice.crt: OK

www.fullchain.pem 配给 Nginx 的 ssl_certificate。做 mTLS 的服务器,把客户端 CA 配置(Nginx 的 ssl_client_certificate 加 ssl_verify_client on)指向中间证书和根证书。更多检查方法见验证证书链。

把客户端证书打包成 PKCS#12

浏览器、手机和 Windows 导入客户端证书时,用的是带密码的 .p12 文件。

openssl pkcs12 -export -in certs/alice.crt -inkey certs/alice.key -certfile ica/ica.crt \
  -name "alice@example.com" -out certs/alice.p12
openssl pkcs12 -in certs/alice.p12 -info -noout | head -3
Enter Export Password:
Verifying - Enter Export Password:
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 16
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256

OpenSSL 3.0 起默认使用 AES-256-CBC + PBKDF2,MAC 为 SHA-256。较老的 Windows、老版本 Java 和一些嵌入式设备读不了这种文件,给它们导出时加 -legacy(RC2/3DES 加 SHA-1 MAC),或写成 -certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1。详见 PKCS#12 命令。

把 CA 限制在自己的域名内

中间证书里的 nameConstraints 会让其他域名的证书验证失败,即便 CA 真的签了它。

openssl ca -config ca.cnf -extensions server_cert -in certs/shop.csr -out certs/shop.crt -notext -batch
openssl verify -CAfile root/root.crt -untrusted ica/ica.crt certs/shop.crt
CN=shop.example.net
error 47 at 0 depth lookup: permitted subtree violation
error certs/shop.crt: verification failed

名称约束能在中间证书私钥泄露时控制损失,也更容易说服其他团队信任你的根证书。permitted;DNS:.example.com 匹配所有子域名,裸域要另加 permitted;DNS:example.com,内网 IP 可以写 permitted;IP:10.0.0.0/255.0.0.0。这个扩展要标记为 critical,让不认识它的客户端直接拒绝,而不是忽略约束。

吊销证书并发布 CRL

openssl ca -revoke 在 index.txt 里标记吊销,-gencrl 生成客户端下载的、带签名的 CRL。

openssl ca -config ca.cnf -revoke certs/alice.crt -crl_reason keyCompromise
openssl ca -config ca.cnf -gencrl -out ica/ica.crl
openssl verify -crl_check -CAfile root/root.crt -untrusted ica/ica.crt -CRLfile ica/ica.crl certs/alice.crt
Using configuration from ca.cnf
Revoking Certificate 1001.
Database updated
Using configuration from ca.cnf
O=Example Corp, CN=alice
error 23 at 0 depth lookup: certificate revoked
error certs/alice.crt: verification failed

default_crl_days = 7 表示 CRL 一周后过期,要用 cron 定时重新生成,并发布到 crlDistributionPoints 写的地址上。Nginx 用 ssl_crl 按 CRL 检查客户端证书,员工离职时吊销其证书即可。测试用的 OCSP 服务见 OCSP 与 CRL 命令。

在 Linux、macOS、Windows 和 Java 中信任根证书

每个平台有自己的信任库,只导入根证书,不要导入中间证书。

# Debian / Ubuntu(文件必须以 .crt 结尾)
sudo cp root/root.crt /usr/local/share/ca-certificates/example-root-ca.crt && sudo update-ca-certificates
# CentOS / Rocky / AlmaLinux 等 RHEL 系
sudo cp root/root.crt /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust
# macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root/root.crt
# Windows(管理员命令行)
certutil -addstore -f Root root.crt
# Java(当前 JDK 的 cacerts)
keytool -importcert -cacerts -alias example-root-ca -file root/root.crt -noprompt -storepass changeit
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
Certificate was added to keystore

基于 Debian/Ubuntu 或 RHEL 的发行版(包括不少国产发行版)一般沿用上面对应的命令。Firefox 默认用自己的信任库,需要打开 security.enterprise_roots.enabled 或通过企业策略导入系统根证书。容器要把根证书打进镜像。Python requests 默认使用 certifi 证书包,要设置 REQUESTS_CA_BUNDLE 指向你的根证书,否则会报 CERTIFICATE_VERIFY_FAILED。给 Apple 设备用的服务器证书有效期不要超过 825 天,更推荐短有效期加自动续期。

常见问题

直接用自签名证书不行吗?

自签名证书要在每台客户端上单独信任,换证时还得再挨个操作一遍。私有 CA 只需要信任一次根证书,之后签发、轮换、吊销多少张证书都不用再动客户端。只有一台测试机时,用自签名证书就够了。

私有 CA 签的证书能用在公网网站上吗?

不能。普通访客的浏览器不信任你的根证书,会看到 NET::ERR_CERT_AUTHORITY_INVALID。私有 CA 适合内网系统、服务间 mTLS、VPN、物联网设备和测试环境。

公网证书 200 天、47 天的有效期上限对私有 CA 有约束吗?

没有,CA/浏览器论坛的基线要求只约束公网信任的 CA。但客户端有自己的规则,比如 Apple 平台要求服务器证书不超过 825 天。内网同样建议用较短有效期配合自动续期。

资料来源

  1. OpenSSL 手册:openssl-ca
  2. OpenSSL 手册:x509v3_config
  3. OpenSSL 手册:openssl-x509
  4. RFC 5280:基本约束、名称约束与 CRL
  5. Apple:iOS 13 和 macOS 10.15 中受信任证书的要求