使用 OpenSSL 签发 HTTPS 证书

使用 OpenSSL 签发 HTTPS 证书:私有 CA 与直接自签名

本文讲清楚两种做法:

  1. 先创建私有 CA,再由 CA 签发服务器证书
  2. 不建立 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 签发”严格来说仍然存在数字签名,只是服务器证书由自己的私钥签署,证书的 IssuerSubject 通常相同,因此称为自签名证书

三、域名匹配规则

证书验证主要查看 SAN(Subject Alternative Name),不能只依赖 CN。

  • 本文证书中的 www.linux.com 只匹配 www.linux.com
  • 它不匹配 linux.comapi.linux.com 或其他子域名;
  • 如需覆盖多个域名,应把每个域名分别加入 SAN;
  • 通配符必须是最左侧完整标签,不能写成 api*.example.com

四、方案一:创建私有 CA,再签发服务器证书

配套脚本:issue-with-private-ca.sh

执行

chmod +x issue-with-private-ca.sh
./issue-with-private-ca.sh www.linux.com 365

第一个参数是需要签发的准确域名;第二个参数是服务器证书有效天数,默认 365 天。

脚本首次运行会创建 private-ca/ca.keyprivate-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

签发过程

脚本依次完成:

  1. 创建 CA 私钥;
  2. 创建带有 CA:TRUEkeyCertSign 用途的自签名根 CA 证书;
  3. 创建独立的服务器私钥;
  4. 使用服务器私钥生成 CSR;
  5. 使用 CA 私钥签署 CSR;
  6. 在服务器证书中写入 SAN、CA:FALSEserverAuth
  7. 使用 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 有能力为任意域名签发证书。

八、安全建议

  1. CA 私钥和服务器私钥都使用权限 600 保存。
  2. CA 私钥尽量离线保存,不要部署到 Web 服务器。
  3. 不要提交任何 .key 文件到版本库。
  4. 测试完成后,安全删除不再使用的私钥,并从客户端信任库移除测试 CA。
  5. CA 私钥一旦泄露,应停止使用该 CA,移除客户端对旧 CA 的信任并重新建立 CA;仅重新签发服务器证书无法消除风险。
  6. 公网生产服务使用公开受信任 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。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容