目录
Please enable Javascript to view the contents

PKI-自签发证书说明

 ·  ☕ 10 分钟

重要

自签证书和合法证书最核心的区别不是加密能力,而是客户端是否默认信任签发 CA。

合法 CA 的根证书通常已预装在操作系统或浏览器中;自建 CA 需要手动导入并信任。信任建立后,证书链校验逻辑一致。

本文按三层证书链说明自签发流程:

根 CA
  ↓ 签发
中间 CA
  ↓ 签发
服务端证书
类型信任来源说明
根 CA 证书手动导入客户端信任库是整条自建证书链的信任锚
中间 CA 证书由根 CA 签发用于签发服务端证书,降低根 CA 私钥暴露风险
服务端证书由中间 CA 签发证书必须包含 SAN(Subject Alternative Name),否则域名/IP 校验可能失败
CA 私钥签发证书的根权限根 CA / 中间 CA 私钥必须严格保护,不能放进 K8s Secret、Pod 或镜像

1.简介

内网环境、测试环境、私有域名,经常需要自己签发 TLS 证书。

常见场景:

  • 本地开发环境开启 HTTPS;
  • 内网服务通过 Ingress 暴露;
  • 私有域名没有公网 DNS 或不方便申请公网证书;
  • 需要验证 TLS、mTLS、证书链等配置。

2.原理

2.1 前置理论:签名原语

签名原语只有一种操作:

1
2
签名:SHA256(原文) → 私钥签名 → 签名值
验签:SHA256(原文) → 公钥验证签名值

验签通过,同时证明:原文未被篡改 + 签名方持有对应私钥。

证书体系中每一次签名,都是这个原语的调用。差异只在:谁签、签什么数据、验签发生在哪个环节。

2.2 签发流水线

CSR(Certificate Signing Request,证书签名请求)不是运行时证书链的一部分,而是申请证书时交给 CA 的材料。

证书链中,每一轮签发都调用签名原语两次:

步骤签名方私钥签什么验签方验签时机
CSR 自签名申请者subject.keyCSR 正文CA签发流水线内
CA 签发CAissuer.key证书正文客户端TLS 握手时

两次签名机制完全一样,差异只在参数和验签环节的归属。

CSR 自签名时,公钥在 CSR 正文内,所以公钥无法被替换——篡改公钥会导致正文哈希变化,验签失败。因此验签通过就意味着:CSR 未被篡改,且申请人持有对应私钥。

证书签发中的签名没有特殊语义——它就是一个签名原语的两次调用。

PKI-SIGN-LOOP

上图以"签发服务端证书"为例。抽象到证书链中,每一轮都可以看成:签发者 issuer申请者 subject 签发一张证书。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
函数 生成_CSR(申请者):
  input:
      申请者 = {私钥, 公钥, 主体信息}

  csr.正文 = 申请者.主体信息 + 申请者.公钥
  csr.摘要 = SHA256(csr.正文)
  csr.签名 = USE(申请者.私钥).SIGN(csr.摘要)
  csr = csr.正文 + csr.签名

  output:
      csr

函数 签发_CRT(csr, 签发者):
  input:
      csr = {正文, 签名}
      签发者 = {私钥, 证书, 主体信息, 签发策略}

  申请者.公钥 = csr.正文[申请者.公钥]
  申请者.主体信息 = csr.正文[申请者.主体信息]
  csr.摘要 = SHA256(csr.正文)

  IF USE(申请者.公钥).VERIFY(csr.摘要, csr.签名) == false:
      RETURN ERROR("CSR 签名无效")

  IF CHECK(申请者.主体信息, 签发者.签发策略) == false:
      RETURN ERROR("申请信息不符合签发策略")

  cert.正文 = 申请者.公钥
            + 申请者.主体信息
            + 签发者.主体信息
            + 有效期
            + 扩展信息

  cert.摘要 = SHA256(cert.正文)
  cert.签名 = USE(签发者.私钥).SIGN(cert.摘要)
  cert = cert.正文 + cert.签名

  output:
      cert

函数 LOOP(根CA, 申请者列表):
  input:
      根CA = {私钥, 证书, 主体信息, 签发策略}
      申请者列表 = [中间CA..., 服务端]

  签发者 = 根CA
  证书链 = []

  FOR 申请者 IN 申请者列表:
      csr = 生成_CSR(申请者)
      cert = 签发_CRT(csr, 签发者)

      申请者.证书 = cert
      证书链.APPEND(cert)

      IF 申请者.类型 == "CA":
          签发者 = 申请者
          CONTINUE

      IF 申请者.类型 == "服务端":
          RETURN {
              服务端私钥: 申请者.私钥,
              服务端证书: 申请者.证书,
              证书链: 证书链
          }
签发循环只有一个核心规则:**签发者用自己的私钥签名;被签发者的公钥写进被签发证书。**只有被签发者是 CA 时,才会进入下一轮继续签发。
展开:CSR 与证书签发数据流
[生成服务端密钥对]
        |
        +--> [server.pub 公钥]
        |            |
        |            | 进入 CSR 主体信息
        |            v
        |        [CSR 主体信息]
        |        [申请者信息 Subject/SAN + server.pub]
        |            |
        |            | SHA256
        |            v
        |        [CSR 主体信息摘要] ----+
        |                              |
        +--> [server.key 私钥] --------+-- 签名 --> [CSR signatureValue]
                                                         |
                                                         | 组装
                                                         v
[CSR = 申请者信息 Subject/SAN + server.pub + CSR signatureValue]
                                       |
                                       | 提交给上级 CA
                                       v
[上级 CA 验证 CSR]
        |
        | 1. 读取 CSR 主体信息、server.pub、CSR signatureValue
        | 2. 重新计算 CSR 主体信息摘要
        | 3. 使用 server.pub 验证 CSR signatureValue
        v
[验证通过:CSR 未被篡改,且申请人持有 server.key]
        |
        | CA 继续校验域名 / SAN / 用途 / 有效期 / 策略
        v
[生成证书主体信息]
[证书主体信息 = server.pub + Subject/SAN + Issuer/Validity + KeyUsage]
        |
        | SHA256
        v
[证书主体信息摘要] -------------+
                              |
[issuer.key 上级 CA 私钥] ------+-- 签名 --> [CA signatureValue]
                                                        |
                                                        | 组装
                                                        v
[server.crt = 证书主体信息 + CA signatureValue]

2.3 证书链如何组成

证书链本质上是一条信任路径:客户端从服务端证书一路向上验证,直到找到自己已经信任的根 CA。

常见结构:

1
2
3
4
5
根 CA(根证书,客户端已信任)
  ↓ 签发
中间 CA(降低根 CA 暴露风险)
  ↓ 签发
服务端证书(绑定域名/IP)

自建 CA 的简单场景通常只有两层:

1
2
3
自建根 CA(root-ca.crt,手动导入客户端信任)
  ↓ 签发
服务端证书(example.domain.crt)

服务端通常需要返回:

1
服务端证书 + 中间 CA 证书

客户端本地需要提前信任:

1
根 CA 证书

本例使用三层链:客户端只需要信任 root-ca.crt;服务端需要提供 example.domain.crt + intermediate-ca.crt 组成的完整证书链(fullchain)。

2.4 证书链如何校验

签发是"从上往下签"——每层 CA 用自己私钥签发下级证书。校验是"从下往上验"——客户端用上级公钥逐层验证下级证书签名。

校验每一步都是在执行 2.1 中的验签:重新计算证书主体摘要,用上级证书中的公钥验证签名值。

步骤操作使用的公钥
1验证 server.crt:SHA256(server.crt 正文) → 用公钥验签名intermediate-ca.crt 中的公钥
2验证 intermediate-ca.crt:SHA256(intermediate-ca.crt 正文) → 用公钥验签名root-ca.crt 中的公钥
3检查 root-ca.crt 是否在本地信任库
4检查服务端证书 SAN / 有效期 / 吊销状态

注意:步骤 3 不是验签,是本地查找——根 CA 的信任来自"是否已导入",不是通过更上级证书证明出来的。

签发:从上往下,签发者用私钥签名;校验:从下往上,客户端用上级公钥验签。直到追溯到来客户端已信任的根 CA,整条链成立。

3.实操

3.1 目标与文件关系

本例会生成以下文件:

文件可以理解成用途是否需要保密
root-ca.key根 CA 私钥签发中间 CA必须保密
root-ca.crt根 CA 证书导入客户端信任库可公开
intermediate-ca.key中间 CA 私钥签发服务端证书必须保密
intermediate-ca.csr中间 CA 证书申请交给根 CA 签发可公开
intermediate-ca.crt中间 CA 证书组成服务端完整证书链(fullchain)可公开
example.domain.key服务端私钥Nginx / Ingress 握手时使用必须保密
example.domain.csr服务端证书申请交给中间 CA 签发可公开
example.domain.crt服务端证书服务端 HTTPS 证书可公开
example.domain.fullchain.crt服务端证书链服务端证书 + 中间 CA 证书可公开
example.domain.ext服务端证书扩展写 SAN、用途等扩展字段可公开
业务服务只需要服务端证书链和服务端私钥;根 CA / 中间 CA 私钥只用于签发证书,不应该放进业务 Pod、Ingress Secret 或镜像。

3.2 生成根 CA

生成根 CA 私钥:

1
openssl genrsa -out root-ca.key 4096

生成根 CA 根证书:

1
2
3
4
5
6
openssl req -x509 -new -nodes \
  -key root-ca.key \
  -sha256 \
  -days 3650 \
  -subj "/CN=hex-root-ca" \
  -out root-ca.crt

查看根 CA 证书:

1
openssl x509 -in root-ca.crt -text -noout

3.3 生成中间 CA 并签发其证书

生成中间 CA 私钥:

1
openssl genrsa -out intermediate-ca.key 4096

生成中间 CA CSR:

1
2
3
4
openssl req -new -sha256 \
  -key intermediate-ca.key \
  -subj "/CN=hex-intermediate-ca" \
  -out intermediate-ca.csr

创建中间 CA 扩展文件:

1
2
3
4
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

保存为:

1
intermediate-ca.ext

使用根 CA 签发中间 CA 证书:

1
2
3
4
5
6
7
8
9
openssl x509 -req \
  -in intermediate-ca.csr \
  -CA root-ca.crt \
  -CAkey root-ca.key \
  -CAcreateserial \
  -out intermediate-ca.crt \
  -days 1825 \
  -sha256 \
  -extfile intermediate-ca.ext

验证中间 CA 证书:

1
openssl verify -CAfile root-ca.crt intermediate-ca.crt

预期输出:

1
intermediate-ca.crt: OK

3.4 生成服务端证书

生成服务端私钥:

1
openssl genrsa -out example.domain.key 4096

生成服务端 CSR:

1
2
3
4
openssl req -new -sha256 \
  -key example.domain.key \
  -subj "/CN=*.example.domain" \
  -out example.domain.csr

查看 CSR:

1
openssl req -in example.domain.csr -text -noout

创建服务端 SAN 扩展文件:

SAN(Subject Alternative Name,主体备用名称)用于声明证书可以匹配哪些域名或 IP。现代 TLS 客户端校验证书时,主要看 SAN,只写 CN 不够。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=@alt_names

[alt_names]
DNS.1=*.example.domain
DNS.2=example.domain
DNS.3=svc.example.domain
IP.1=127.0.0.1

保存为:

1
example.domain.ext
字段说明
basicConstraints=CA:FALSE表示该证书不是 CA 证书
keyUsage指定密钥用途
extendedKeyUsage=serverAuth表示用于服务端 TLS 认证
subjectAltName证书可匹配的 DNS / IP 列表

使用中间 CA 签发服务端证书:

1
2
3
4
5
6
7
8
9
openssl x509 -req \
  -in example.domain.csr \
  -CA intermediate-ca.crt \
  -CAkey intermediate-ca.key \
  -CAcreateserial \
  -out example.domain.crt \
  -days 825 \
  -sha256 \
  -extfile example.domain.ext
  • -CA intermediate-ca.crt:指定中间 CA 证书;
  • -CAkey intermediate-ca.key:指定中间 CA 私钥;
  • -CAcreateserial:生成序列号文件;
  • -extfile example.domain.ext:写入 SAN 等扩展;
  • -days 825:服务端证书有效期,建议不要过长。

查看服务端证书:

1
openssl x509 -in example.domain.crt -text -noout

重点确认 SAN:

1
2
X509v3 Subject Alternative Name:
    DNS:*.example.domain, DNS:example.domain, DNS:svc.example.domain, IP Address:127.0.0.1

生成服务端完整证书链(fullchain):

1
cat example.domain.crt intermediate-ca.crt > example.domain.fullchain.crt

3.5 验证证书链

使用根 CA 和中间 CA 验证服务端证书:

1
2
3
4
openssl verify \
  -CAfile root-ca.crt \
  -untrusted intermediate-ca.crt \
  example.domain.crt

预期输出:

1
example.domain.crt: OK

本地启动临时 TLS 服务测试:

1
2
3
4
openssl s_server \
  -cert example.domain.fullchain.crt \
  -key example.domain.key \
  -accept 8443

另一个终端验证:

1
2
3
4
openssl s_client \
  -connect 127.0.0.1:8443 \
  -CAfile root-ca.crt \
  -servername svc.example.domain

4.运维

4.1 配置客户端信任根 CA

Linux 系统需要把 root-ca.crt 加入系统信任列表。

Debian / Ubuntu:

1
2
sudo cp root-ca.crt /usr/local/share/ca-certificates/hex-root-ca.crt
sudo update-ca-certificates

RHEL / CentOS:

1
2
sudo cp root-ca.crt /etc/pki/ca-trust/source/anchors/hex-root-ca.crt
sudo update-ca-trust

验证 curl:

1
curl --cacert root-ca.crt https://svc.example.domain

如果已加入系统信任:

1
curl https://svc.example.domain

4.2 证书吊销

证书吊销要先判断泄露的是哪个私钥。

泄露对象影响范围处理方式
服务端私钥 example.domain.key只影响该服务域名吊销旧服务端证书,重新生成服务端私钥和证书
中间 CA 私钥影响该中间 CA 签发的所有证书吊销中间 CA,重新签发下级证书
根 CA 私钥 root-ca.key整个自建 CA 体系失效移除旧 CA 信任,重建 CA,重新签发所有证书

吊销机制主要有两类:

机制说明
CRL(Certificate Revocation List)CA 定期发布吊销列表,客户端下载后检查证书序列号
OCSP(Online Certificate Status Protocol)客户端在线查询某张证书是否有效

内网自建 CA 常见处理:

  1. 记录要吊销证书的 serial;
  2. 在 CA 侧吊销证书;
  3. 生成新的 CRL;
  4. 让客户端或网关加载新的 CRL;
  5. 重新签发并替换服务端证书。

如果使用 openssl ca 管理 CA,可以执行:

1
2
openssl ca -config openssl.cnf -revoke example.domain.crt
openssl ca -config openssl.cnf -gencrl -out ca.crl

查看 CRL:

1
openssl crl -in ca.crl -text -noout

需要注意:

  • 只生成 CRL 不够,客户端必须实际检查 CRL 才有意义;
  • 很多内网系统默认不检查 CRL/OCSP,需要在网关、客户端或运行时显式配置;
  • 如果是根 CA 私钥泄露,吊销单张证书没有意义,必须更换根 CA,并从所有客户端信任列表中移除旧 CA。

K8s 场景中,如果服务端私钥泄露,通常处理流程:

1
2
3
4
5
6
kubectl delete secret example-certs -n default
kubectl create secret tls example-certs \
  --cert=new.example.domain.fullchain.crt \
  --key=new.example.domain.key \
  -n default
kubectl rollout restart deploy/<ingress-or-app> -n default
服务端私钥泄露,重签服务端证书即可;CA 私钥泄露,整个信任根被破坏,必须重建 CA 并重新分发信任。

4.3 Kubernetes TLS Secret

Ingress 使用服务端证书链和服务端私钥:

1
2
3
4
kubectl create secret tls example-certs \
  --cert=example.domain.fullchain.crt \
  --key=example.domain.key \
  -n default

查看 Secret:

1
2
kubectl get secret example-certs -n default
kubectl describe secret example-certs -n default

Ingress 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example
  namespace: default
spec:
  tls:
    - hosts:
        - svc.example.domain
      secretName: example-certs
  rules:
    - host: svc.example.domain
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: example-service
                port:
                  number: 80

需要注意:

  • Secret 中不要放 CA 私钥;
  • Ingress Controller 需要能读取该 namespace 下的 Secret;
  • 浏览器是否信任取决于客户端是否导入 root-ca.crt,不是 K8s Secret 决定。

4.4 常见问题

问题原因处理
浏览器提示不安全客户端不信任自建 CA导入 root-ca.crt 到系统或浏览器信任列表
x509: certificate signed by unknown authority程序不信任 CA配置 CA 文件或加入系统信任
certificate is valid for ... not ...SAN 中没有访问域名/IP重新签发证书并补充 SAN
Ingress 仍使用默认证书Secret 名称/namespace 不匹配检查 Ingress tls.secretName 和 namespace
tls: private key does not match public key证书和私钥不是一对重新确认 crt/key 匹配

验证证书和私钥是否匹配:

1
2
openssl x509 -noout -modulus -in example.domain.crt | openssl md5
openssl rsa -noout -modulus -in example.domain.key | openssl md5

两条命令输出一致,说明证书和私钥匹配。

5.总结

  1. 自签证书和合法证书最核心的区别是:客户端默认不信任自建 CA;
  2. 服务端证书必须配置 SAN,不能只依赖 CN;
  3. CA 私钥必须离线或严格保护,不能放入业务 Secret、Pod、镜像;
  4. K8s Ingress Secret 只需要服务端证书链和服务端私钥;
  5. 排查证书问题优先看:证书链、SAN、Secret namespace、证书和私钥是否匹配。

6.参考

分享

Hex
作者
Hex
CloudNative Developer