跳到主要内容

Grpcs TLS 自签证书

· 阅读需 8 分钟

由于微服务通讯需要进行数据加密以保证内网通讯安全。故此记录一下自签证书的生成过程。

先交代一下背景。gRPC 默认走明文的 HTTP/2,服务间的请求体在内网中是可以被直接抓包读到的。生产环境里即便是内网,也不能假设网络绝对可信——一旦某台机器被攻破,横向嗅探流量的成本非常低。给 gRPC 加上 TLS 是最直接的加固手段。而内网服务没必要(也往往没办法)去公共 CA 申请证书,自建一套根证书、给每个服务签发证书,是更常见的做法。这套流程用 OpenSSL 就能完成,但细节不少,尤其是 Go 侧对证书字段的校验规则,值得完整记录一遍。

以下是在 Linux 或 MacOS 系统上使用 OpenSSL 命令行工具生成自签名证书的步骤。这个过程将创建一个新的根证书(CA),然后使用这个根证书签名一个新的服务器证书。

这里的证书体系是两层结构:根证书自己给自己签名,充当信任锚点;服务证书由根证书签发,客户端只要预先信任根证书,就能验证任何由它签出的服务证书。这也是为什么客户端侧只需要分发 ca.crt,而不需要逐个分发每个服务的证书。

先简明说明整体流程:

根证书:

1、生成根私钥

2、通过根私钥生成根证书

服务证书

1、生成服务私钥

2、通过服务私钥生成服务CSR

3、通过服务.csr 文件 + 根证书 + 根证书 key 签名出服务证书.crt文件

生成根证书的私钥

# 生成 2048 位 RSA 私钥,作为根 CA 的私钥
openssl genrsa -out ca.key 2048

这个命令将生成一个新的 RSA 私钥,长度为 2048 位。这个私钥将被保存到名为 ca.key 的文件中。

这把私钥是整个信任链的根,谁拿到它就能签发出被所有客户端信任的证书。妥善保管,不要提交到代码仓库。

生成根证书

# 用根私钥自签一张 X.509 根证书,有效期 365 天
openssl req -new -x509 -days 365 -key ca.key -out ca.crt

这个命令将生成一个新的 X.509 根证书,使用 SHA-256 算法进行签名,有效期为 365 天 (可自己调整日期)。这个证书将被保存到名为 ca.crt 的文件中。

-x509 参数表示直接输出自签名证书,而不是签名请求,这正是"根证书自己给自己签名"的含义。

在执行这个命令时,OpenSSL 会提示你输入一些信息,这些信息将被包含在证书中。在大多数情况下,你可以直接按 Enter 键接受默认值。

Subject Name 或 Common Name 需要填写微服务端的名称。

根证书准备好之后,接下来为具体的服务生成证书。每个服务都重复下面三步即可。

生成服务器证书的私钥

# 为服务生成 2048 位 RSA 私钥
openssl genrsa -out [service-name].key 2048

这个命令与第一个命令类似,生成的私钥将被保存到名为 [service-name].key 的文件中。[service-name] 为你自己的服务名称。

生成服务器证书的证书签名请求(CSR)

# 基于服务私钥生成证书签名请求(CSR)
openssl req -new -key [service-name].key -out [service-name].csr

这个命令将生成一个新的证书签名请求(CSR),这个请求包含了服务器证书的公钥和一些附加信息。这个请求将被保存到名为 server.csr 的文件中。

CSR 本身不是证书,它只是"我是谁、我的公钥是什么"的一份申请材料,需要交给 CA 签名后才变成可用的证书。

同样,在执行这个命令时,OpenSSL 会提示你输入一些信息,这些信息将被包含在 CSR 中。

使用根证书签名服务器证书

# 用根证书 + 根私钥对 CSR 签名,得到服务证书
openssl x509 -req -in [service-name].csr -CA ca.crt -CAkey ca.key -CAcreateserial -out [service-name].crt -days 365

这个命令将使用根证书对服务器证书进行签名,生成的证书将被保存到名为 server.crt 的文件中。

其中 -CAcreateserial 会生成一个 .srl 序列号文件,用于记录该 CA 已签发证书的序号,后续再签发时会自动递增,无需手动处理。

以上步骤生成的 rootCA.crtserver.crtserver.key 就可以用于 GRPC 加密通讯了,配置到对应的服务器端以及客户端。服务端加载自己的证书和私钥,客户端加载根证书用于校验服务端身份。

部分自定义内容

由于在GO更高的版本中,对证书内部分字段的值验证与获取有调整。(Go 1.15版本之后会获取证书alt_names字段信息进行服务名称验证)所以我们需要在生成证书时,指定部分自定义字段信息。

也就是说,只在 Common Name 里写服务名已经不够了:Go 的 TLS 客户端在握手时会拿连接的目标名称去匹配证书里的 SAN(Subject Alternative Name)字段,匹配不上就直接握手失败。所以证书必须显式携带 SAN 扩展。命令行交互不方便填这些扩展字段,改用配置文件的方式。

创建一个 san.cnf 文件内容如下:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

# 证书主体信息,替代交互式输入
[dn]
C = CN
ST = Beijing
L = Beijing
O = 'lynx'
OU = 'lynx'
emailAddress = 'lynx'
CN = [service-name]

# CSR 阶段使用的扩展:声明 SAN
[req_ext]
subjectAltName = @alt_names

# SAN 列表,Go 客户端校验的就是这里的 DNS 名称
[alt_names]
DNS.1 = [service-name]

# 签发证书阶段使用的扩展
[v3_ext]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment,digitalSignature
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names

配置里 prompt = no 让 OpenSSL 不再交互式提问,直接读取 [dn] 段的内容;extendedKeyUsage 同时声明了 serverAuthclientAuth,意味着这张证书既能用于服务端,也能在双向 TLS 场景下作为客户端证书使用。

然后在上面的生成证书文件的步骤中带上

在生成服务csr文件时添加 -extensions req_ext -config san.cnf 命令

# 生成 CSR 时引用配置文件,带上 req_ext 段声明的 SAN
openssl req -new -key [service-name].key -out [service-name].csr -extensions req_ext -config san.cnf

在生成服务证书时,也需要添加 -extensions v3_ext -extfile san.cnf 命令

# 签发时同样要带 v3_ext 扩展,否则 SAN 不会写入最终证书
openssl x509 -req -in [service-name].csr -CA ca.crt -CAkey ca.key -CAcreateserial -out [service-name].crt -days 365 -extensions v3_ext -extfile san.cnf

这样生成出来的证书文件就包含了我们指定字段的内容,以及部分证书描述信息。

注意

签发这一步的 -extensions v3_ext -extfile san.cnf 容易被漏掉。openssl x509 -req 默认不会把 CSR 里的扩展字段带进最终证书,如果只在 CSR 阶段声明了 SAN,签出来的证书里仍然没有 SAN。

证书内容验证

可通过命令,查看生成出来的证书是否确实包含字段信息

# 以文本形式打印证书内容,检查 SAN 等扩展字段
openssl x509 -in [service-name].crt -text -noout

输出中重点确认 X509v3 Subject Alternative Name 一节是否包含期望的 DNS 名称,以及有效期、签发者是否正确。建议把这一步纳入固定流程,比等到服务上线握手报错再排查省事得多。

踩坑与注意

1)客户端连接时使用的目标名称必须与证书 SAN 中的 DNS 名称一致。如果通过 IP 直连,SAN 里就需要用 IP.1 = ... 的形式声明 IP 条目,DNS 条目是匹配不上的。

2)证书有效期到期后 gRPC 握手会直接失败,而这类故障往往在到期当天才暴露。内网自签证书没人提醒续期,最好记录到期时间并提前更换。

3)ca.key 与各服务的 .key 文件都属于敏感材料,注意文件权限,且不要随镜像或仓库分发;客户端只需要 ca.crt

4)改动 san.cnf 后需要重新走 CSR 与签发两步,只重签证书而不重新生成 CSR,可能带着旧的主体信息。

小结

整个流程可以浓缩为一条信任链:根私钥生成根证书,服务私钥生成 CSR,根证书对 CSR 签名得到服务证书。在 Go 的 gRPC 场景下,关键点是通过 san.cnf 把 SAN 字段写进证书,并在 CSR 与签发两个阶段都显式指定扩展,最后用 openssl x509 -text 验证一遍。把这几步固化成脚本后,为新服务签发证书就是一分钟的事。

评论 / COMMENTS