使用 OpenSSL 签发 HTTPS 证书:私有 CA 与直接自签名
本文讲清楚两种做法:
- 先创建私有 CA,再由 CA 签发服务器证书;
- 不建立 CA,直接生成自签名服务器证书。
示例域名为 www.linux.com,证书 SAN 中只加入这个准确域名。
重要:这两种方式默认都不会被公网浏览器信任。公网网站应使用受信任的公开 CA,例如通过 ACME/Let's Encrypt 申请证书。本文方案适合开发、测试和可控的内网环境。
一、先理解证书、私钥和 CA
1. 服务器私钥
私钥证明服务器持有证书对应的身份凭据,文件通常以 .key 结尾。私钥不能发给客户端、不能提交到 Git,也不应通过聊天或邮件传输。
2. 服务器证书
证书通常以 .crt 或 .pem 结尾,包含服务器公钥、域名、有效期、用途和签发者等信息。服务器证书本身可以公开。
3. CA
CA 是“证书签发者”。私有 CA 包含:
-
ca.key:CA 私钥,是整个信任体系最敏感的文件; -
ca.crt:CA 证书,需要安装到客户端的信任库中。
客户端信任 ca.crt 后,就可以信任该 CA 签发的所有合规服务器证书。
4. CSR
CSR(Certificate Signing Request,证书签名请求)包含待签发证书的公钥和身份信息。服务器先生成自己的私钥和 CSR,CA 使用 CA 私钥为 CSR 签名。CA 不需要也不应取得服务器私钥。
二、两种方案的核心区别
| 对比项 | 私有 CA 签发 | 直接自签名 |
|---|---|---|
| 信任关系 | 客户端信任一次 CA,即可信任它签发的多个证书 | 客户端通常需要逐张信任服务器证书 |
| 适用场景 | 内网、多服务、多域名、长期管理 | 单机测试、临时环境、少量服务 |
| 文件关系 | CA 证书/私钥与服务器证书/私钥相互独立 | 服务器证书用自己的私钥给自己签名 |
| 扩展性 | 较好,可以持续签发和轮换服务器证书 | 较差,证书变化后客户端往往要重新信任 |
| 风险集中点 | CA 私钥泄露会危及该 CA 下所有证书 | 单个服务器私钥泄露通常只影响该证书 |
| 浏览器默认信任 | 否,需要导入私有 CA | 否,需要导入该自签名证书 |
“没有 CA 签发”严格来说仍然存在数字签名,只是服务器证书由自己的私钥签署,证书的 Issuer 和 Subject 通常相同,因此称为自签名证书。
三、域名匹配规则
证书验证主要查看 SAN(Subject Alternative Name),不能只依赖 CN。
- 本文证书中的
www.linux.com只匹配www.linux.com; - 它不匹配
linux.com、api.linux.com或其他子域名; - 如需覆盖多个域名,应把每个域名分别加入 SAN;
- 通配符必须是最左侧完整标签,不能写成
api*.example.com。
四、方案一:创建私有 CA,再签发服务器证书
执行
chmod +x issue-with-private-ca.sh
./issue-with-private-ca.sh www.linux.com 365
第一个参数是需要签发的准确域名;第二个参数是服务器证书有效天数,默认 365 天。
脚本首次运行会创建 private-ca/ca.key 和 private-ca/ca.crt。以后再次运行会复用该 CA,并签发新的服务器证书。输出位于:
private-ca/
├── ca.crt
├── ca.key
└── ca.srl
issued/www.linux.com/
├── www.linux.com.crt
├── www.linux.com.csr
└── www.linux.com.key
签发过程
脚本依次完成:
- 创建 CA 私钥;
- 创建带有
CA:TRUE和keyCertSign用途的自签名根 CA 证书; - 创建独立的服务器私钥;
- 使用服务器私钥生成 CSR;
- 使用 CA 私钥签署 CSR;
- 在服务器证书中写入 SAN、
CA:FALSE和serverAuth; - 使用 CA 证书验证签发结果。
手工对应命令
创建 CA 私钥和十年期根证书。使用独立配置文件可以避免系统 openssl.cnf 自动加入扩展,造成重复的 Basic Constraints:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out ca.key
cat > ca.cnf <<'EOF'
[req]
prompt=no
distinguished_name=dn
x509_extensions=v3_ca
[dn]
C=CN
O=AIStackTest
OU=Private CA
CN=AIStackTest Root CA
[v3_ca]
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
EOF
openssl req -x509 -new -sha256 -days 3650 \
-key ca.key \
-config ca.cnf \
-out ca.crt
创建服务器私钥和 CSR:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
-out www.linux.com.key
openssl req -new -sha256 \
-key www.linux.com.key \
-subj "/C=CN/O=AIStackTest/CN=www.linux.com" \
-out www.linux.com.csr
创建 server.ext:
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
subjectAltName=@alt_names
[alt_names]
DNS.1=www.linux.com
签发并验证:
openssl x509 -req -sha256 -days 365 \
-in www.linux.com.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile server.ext \
-out www.linux.com.crt
openssl verify -CAfile ca.crt www.linux.com.crt
生产化的私有 PKI 还应维护证书序列号、签发数据库、撤销列表或 OCSP,并尽量让根 CA 离线保存。本文脚本是适合开发和内网测试的简化流程。
五、方案二:直接生成自签名服务器证书
配套脚本:issue-self-signed.sh
执行
chmod +x issue-self-signed.sh
./issue-self-signed.sh www.linux.com 365
输出位于:
self-signed/www.linux.com/
├── www.linux.com.crt
└── www.linux.com.key
签发过程
脚本创建服务器私钥,然后直接使用该私钥签署服务器证书。证书明确设置 CA:FALSE,用途为 TLS 服务器认证,并在 SAN 中包含 www.linux.com。
手工对应命令
在支持 -addext 的 OpenSSL 中,可以使用:
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 365 \
-keyout www.linux.com.key \
-out www.linux.com.crt \
-subj "/C=CN/O=AIStackTest/CN=www.linux.com" \
-addext "basicConstraints=critical,CA:FALSE" \
-addext "keyUsage=critical,digitalSignature,keyEncipherment" \
-addext "extendedKeyUsage=serverAuth" \
-addext "subjectAltName=DNS:www.linux.com"
验证时把证书本身作为信任锚:
openssl verify \
-CAfile www.linux.com.crt \
www.linux.com.crt
这只能说明证书的签名结构可以验证,并不表示操作系统或浏览器已经信任它。
六、部署和验证
Nginx 示例
server {
listen 443 ssl;
server_name www.linux.com;
ssl_certificate /absolute/path/www.linux.com.crt;
ssl_certificate_key /absolute/path/www.linux.com.key;
}
私有 CA 方案中,如果只有根 CA 和服务器证书,Nginx 的 ssl_certificate 使用服务器证书即可;不要把 CA 私钥部署到服务器。更完整的层级会使用中间 CA,并向服务器配置“服务器证书 + 中间证书”链。
检查证书内容
openssl x509 -in www.linux.com.crt \
-noout -subject -issuer -serial -dates \
-ext basicConstraints \
-ext keyUsage \
-ext extendedKeyUsage \
-ext subjectAltName
检查证书与私钥是否匹配
下面两条命令的 SHA-256 输出必须相同:
openssl x509 -in www.linux.com.crt -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl sha256
openssl pkey -in www.linux.com.key -pubout -outform DER \
| openssl sha256
检查实际 HTTPS 服务
如果域名尚未解析,可以把 127.0.0.1 换成服务器 IP:
curl --resolve www.linux.com:443:127.0.0.1 \
--cacert ca.crt \
https://www.linux.com/
直接自签名方案把 --cacert ca.crt 换成服务器的 .crt 文件。仅为临时排错可使用 curl -k;它跳过身份验证,不应成为正式配置。
七、客户端为什么仍提示“不安全”
证书签发成功不等于客户端信任成功:
- 私有 CA 方案需要把
ca.crt导入客户端信任库; - 直接自签名方案需要逐张信任服务器证书;
- 客户端访问的主机名必须包含在 SAN 中;
- 系统时间必须处于证书有效期内;
- 服务端必须返回正确的服务器证书和必要的中间证书链。
在企业或内网环境中,通常通过设备管理、组策略或镜像统一分发私有 CA。不要让用户随意导入来源不明的根证书,因为受信任的根 CA 有能力为任意域名签发证书。
八、安全建议
- CA 私钥和服务器私钥都使用权限
600保存。 - CA 私钥尽量离线保存,不要部署到 Web 服务器。
- 不要提交任何
.key文件到版本库。 - 测试完成后,安全删除不再使用的私钥,并从客户端信任库移除测试 CA。
- CA 私钥一旦泄露,应停止使用该 CA,移除客户端对旧 CA 的信任并重新建立 CA;仅重新签发服务器证书无法消除风险。
- 公网生产服务使用公开受信任 CA。私有 CA 不会因为域名真实存在就自动获得公网信任。
九、如何选择
- 只有一个临时测试服务:直接自签名最省事。
- 有多台机器、多个服务或需要定期轮换:使用私有 CA。
- 面向公网用户:使用公开受信任 CA,不选上述两种私有信任方案。
十、完整脚本一:使用私有 CA 签发
将下面内容保存为 issue-with-private-ca.sh:
#!/usr/bin/env bash
set -euo pipefail
umask 077
domain="${1:-www.linux.com}"
days="${2:-365}"
if ! command -v openssl >/dev/null 2>&1; then
echo "错误:未找到 openssl。" >&2
exit 1
fi
if [[ ! "$domain" =~ ^[A-Za-z0-9.-]+$ ]] || [[ "$domain" == .* ]] || [[ "$domain" == *. ]]; then
echo "错误:域名格式不合法:$domain" >&2
exit 1
fi
if [[ ! "$days" =~ ^[0-9]+$ ]] || (( days < 1 )); then
echo "错误:有效天数必须是正整数。" >&2
exit 1
fi
base_dir="${CERT_OUTPUT_DIR:-$PWD}"
ca_dir="$base_dir/private-ca"
server_dir="$base_dir/issued/$domain"
mkdir -p "$ca_dir" "$server_dir"
ca_key="$ca_dir/ca.key"
ca_cert="$ca_dir/ca.crt"
server_key="$server_dir/$domain.key"
server_csr="$server_dir/$domain.csr"
server_cert="$server_dir/$domain.crt"
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/openssl-private-ca.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT
if [[ -e "$ca_key" || -e "$ca_cert" ]]; then
if [[ ! -f "$ca_key" || ! -f "$ca_cert" ]]; then
echo "错误:CA 私钥和证书必须同时存在:$ca_dir" >&2
exit 1
fi
echo "复用现有私有 CA:$ca_cert"
else
echo "创建新的私有根 CA……"
openssl genpkey \
-algorithm RSA \
-pkeyopt rsa_keygen_bits:3072 \
-out "$ca_key"
ca_config="$tmp_dir/ca.cnf"
printf '%s\n' \
'[req]' \
'prompt=no' \
'distinguished_name=dn' \
'x509_extensions=v3_ca' \
'' \
'[dn]' \
'C=CN' \
'O=AIStackTest' \
'OU=Private CA' \
'CN=AIStackTest Root CA' \
'' \
'[v3_ca]' \
'basicConstraints=critical,CA:TRUE,pathlen:0' \
'keyUsage=critical,keyCertSign,cRLSign' \
'subjectKeyIdentifier=hash' \
'authorityKeyIdentifier=keyid:always,issuer' > "$ca_config"
openssl req -x509 -new -sha256 -days 3650 \
-key "$ca_key" \
-config "$ca_config" \
-out "$ca_cert"
fi
# 确认 CA 证书与 CA 私钥匹配。
cert_pub="$tmp_dir/cert.pub.der"
key_pub="$tmp_dir/key.pub.der"
openssl x509 -in "$ca_cert" -pubkey -noout \
| openssl pkey -pubin -outform DER -out "$cert_pub"
openssl pkey -in "$ca_key" -pubout -outform DER -out "$key_pub"
if ! cmp -s "$cert_pub" "$key_pub"; then
echo "错误:CA 证书与 CA 私钥不匹配。" >&2
exit 1
fi
# 防止意外覆盖已有服务器证书和私钥。
if [[ -e "$server_key" || -e "$server_csr" || -e "$server_cert" ]]; then
echo "错误:目标证书文件已经存在,拒绝覆盖:$server_dir" >&2
echo "请备份/移走旧目录,或设置新的 CERT_OUTPUT_DIR。" >&2
exit 1
fi
openssl genpkey \
-algorithm RSA \
-pkeyopt rsa_keygen_bits:2048 \
-out "$server_key"
openssl req -new -sha256 \
-key "$server_key" \
-subj "/C=CN/O=AIStackTest/CN=$domain" \
-out "$server_csr"
ext_file="$tmp_dir/server.ext"
printf '%s\n' \
'basicConstraints=critical,CA:FALSE' \
'keyUsage=critical,digitalSignature,keyEncipherment' \
'extendedKeyUsage=serverAuth' \
'subjectKeyIdentifier=hash' \
'authorityKeyIdentifier=keyid,issuer' \
'subjectAltName=@alt_names' \
'' \
'[alt_names]' \
"DNS.1=$domain" > "$ext_file"
openssl x509 -req -sha256 -days "$days" \
-in "$server_csr" \
-CA "$ca_cert" \
-CAkey "$ca_key" \
-CAcreateserial \
-extfile "$ext_file" \
-out "$server_cert"
chmod 600 "$ca_key" "$server_key"
chmod 644 "$ca_cert" "$server_csr" "$server_cert"
openssl verify -CAfile "$ca_cert" "$server_cert"
openssl x509 -in "$server_cert" -noout \
-subject -issuer -serial -dates -ext subjectAltName
echo
echo "签发完成:"
echo " CA 证书: $ca_cert"
echo " CA 私钥: ${ca_key}(请离线保护,不要部署到 Web 服务器)"
echo " 服务器证书:$server_cert"
echo " 服务器私钥:$server_key"
赋予执行权限并运行:
chmod +x issue-with-private-ca.sh
./issue-with-private-ca.sh www.linux.com 365
如需把结果写到指定目录:
CERT_OUTPUT_DIR=/opt/my-certificates \
./issue-with-private-ca.sh www.linux.com 365
再次签发前,需要备份或移走旧的 issued/www.linux.com 目录。脚本会拒绝覆盖旧私钥。
十一、完整脚本二:直接生成自签名证书
将下面内容保存为 issue-self-signed.sh:
#!/usr/bin/env bash
set -euo pipefail
umask 077
domain="${1:-www.linux.com}"
days="${2:-365}"
if ! command -v openssl >/dev/null 2>&1; then
echo "错误:未找到 openssl。" >&2
exit 1
fi
if [[ ! "$domain" =~ ^[A-Za-z0-9.-]+$ ]] || [[ "$domain" == .* ]] || [[ "$domain" == *. ]]; then
echo "错误:域名格式不合法:$domain" >&2
exit 1
fi
if [[ ! "$days" =~ ^[0-9]+$ ]] || (( days < 1 )); then
echo "错误:有效天数必须是正整数。" >&2
exit 1
fi
base_dir="${CERT_OUTPUT_DIR:-$PWD}"
server_dir="$base_dir/self-signed/$domain"
mkdir -p "$server_dir"
server_key="$server_dir/$domain.key"
server_cert="$server_dir/$domain.crt"
if [[ -e "$server_key" || -e "$server_cert" ]]; then
echo "错误:目标证书文件已经存在,拒绝覆盖:$server_dir" >&2
echo "请备份/移走旧目录,或设置新的 CERT_OUTPUT_DIR。" >&2
exit 1
fi
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/openssl-self-signed.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT
config_file="$tmp_dir/server.cnf"
printf '%s\n' \
'[req]' \
'prompt=no' \
'distinguished_name=dn' \
'x509_extensions=server_ext' \
'' \
'[dn]' \
'C=CN' \
'O=AIStackTest' \
"CN=$domain" \
'' \
'[server_ext]' \
'basicConstraints=critical,CA:FALSE' \
'keyUsage=critical,digitalSignature,keyEncipherment' \
'extendedKeyUsage=serverAuth' \
'subjectKeyIdentifier=hash' \
'authorityKeyIdentifier=keyid,issuer' \
'subjectAltName=@alt_names' \
'' \
'[alt_names]' \
"DNS.1=$domain" > "$config_file"
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days "$days" \
-keyout "$server_key" \
-out "$server_cert" \
-config "$config_file"
chmod 600 "$server_key"
chmod 644 "$server_cert"
openssl verify -CAfile "$server_cert" "$server_cert"
openssl x509 -in "$server_cert" -noout \
-subject -issuer -serial -dates -ext subjectAltName
echo
echo "自签名证书已生成:"
echo " 服务器证书:$server_cert"
echo " 服务器私钥:$server_key"
echo "客户端必须显式信任该证书;它不会自动获得公网浏览器信任。"
赋予执行权限并运行:
chmod +x issue-self-signed.sh
./issue-self-signed.sh www.linux.com 365
如需把结果写到指定目录:
CERT_OUTPUT_DIR=/opt/my-certificates \
./issue-self-signed.sh www.linux.com 365
十二、最短操作清单
私有 CA 方案
chmod +x issue-with-private-ca.sh
./issue-with-private-ca.sh www.linux.com 365
# Nginx 使用 issued/... 下的 .crt 和 .key。
# 客户端信任 private-ca/ca.crt。
# private-ca/ca.key 不得部署到 Web 服务器或客户端。
直接自签名方案
chmod +x issue-self-signed.sh
./issue-self-signed.sh www.linux.com 365
# Nginx 使用 self-signed/... 下的 .crt 和 .key。
# 客户端需要显式信任该 .crt。