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

> 用 OpenSSL 3.x 一步步搭建内网私有 CA：根证书与中间证书、签发服务器和客户端证书、名称约束、吊销与 CRL，以及导入各平台信任库。

Source: https://ssl.sh/zh/cookbook/private-ca/
Last updated: 2026-11-10

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

## 准备目录和配置文件

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

```bash
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
```

```ini
# 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
```

```text
$ 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 私钥，签完中间证书后就离线保管。

```bash
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -aes-256-cbc -out root/root.key
head -1 root/root.key
```

```text
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`）。其他密钥类型见[生成密钥](https://ssl.sh/zh/cookbook/generate-keys/)。

## 自签名根证书

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

```bash
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
```

```text
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` 段签发。

```bash
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
```

```text
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 都靠它。

```bash
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
```

```text
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](https://ssl.sh/zh/cookbook/csr/)。

## 不用 CA 数据库直接签 CSR

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

```bash
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
```

```text
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，主体里一般写用户名或服务名。

```bash
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
```

```text
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 原理](https://ssl.sh/zh/learn/mtls/)和[客户端认证 EKU 被移除](https://ssl.sh/zh/errors/client-auth-eku-removed/)。

## 拼好证书链并验证

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

```bash
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
```

```text
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`）指向中间证书和根证书。更多检查方法见[验证证书链](https://ssl.sh/zh/cookbook/verify-chains/)。

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

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

```bash
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
```

```text
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 命令](https://ssl.sh/zh/cookbook/pkcs12/)。

## 把 CA 限制在自己的域名内

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

```bash
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
```

```text
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。

```bash
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
```

```text
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 命令](https://ssl.sh/zh/cookbook/ocsp-crl/)。

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

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

```bash
# 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
```

```text
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](https://ssl.sh/zh/errors/python-certificate-verify-failed/)。给 Apple 设备用的服务器证书有效期不要超过 825 天，更推荐短有效期加自动续期。
